├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── Makefile ├── conf.py ├── contributing.rst ├── index.rst ├── installation.rst ├── labdrivers.keithley.rst ├── labdrivers.lakeshore.rst ├── labdrivers.ni.rst ├── labdrivers.oxford.rst ├── labdrivers.quantumdesign.rst ├── labdrivers.srs.rst ├── make.bat ├── moduleapis.rst └── usage.rst ├── example_nbs ├── 2d_conductance.py ├── IV Curve - bnc2110 (DAC).ipynb ├── Instrument control, data manipulation, data visualization.ipynb ├── Isd vs Vbias vs Vgate - bnc2110 sr830 keithley2400.ipynb ├── Measure.ipynb ├── example_2d_gate_bias_conductance.ipynb └── example_realtimeplot.py ├── labdrivers ├── __init__.py ├── keithley │ ├── __init__.py │ └── keithley2400.py ├── labdrivers.py ├── lakeshore │ ├── __init__.py │ └── ls332.py ├── ni │ ├── __init__.py │ └── nidaq.py ├── oxford │ ├── __init__.py │ ├── ips120.py │ ├── itc503.py │ ├── mercuryips.py │ └── triton200.py ├── quantumdesign │ ├── QDInstrument.dll │ ├── __init__.py │ └── qdinstrument.py ├── srs │ ├── __init__.py │ └── sr830.py └── version.py ├── setup.py └── tests ├── test_keithley2400.py ├── test_sr830.py └── test_triton200.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.un~ 3 | *.py~ 4 | *~ 5 | *.swp 6 | *_build/* 7 | ALTERNATIVES 8 | 9 | venv/ 10 | .vs/ 11 | .vscode/ 12 | .ipynb_checkpoints/ 13 | example_nbs/.ipynb_checkpoints/ 14 | 15 | build/ 16 | dist/ 17 | *.egg-info 18 | 19 | .idea/ 20 | .idea* 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | If you don't want to try and figure out all the changes from the diff and commits, 4 | just read this. I wrote this to make the changes more robust. 5 | 6 | ## 0.9.6: Keithley read changes 7 | 8 | - Keithley 2400s should be able to properly read out what they're supposed to. 9 | The Keithley2400.read() function takes a variable number of arguments that are 10 | in the list: 'voltage', 'current', 'resistance', 'time'. The internal workings 11 | assume that the function returns a list which has a length which is some 12 | multiple of 5. Keithley2400.read() returns a tuple of lists, the first element 13 | in the tuple being the mean values of the requested values to be read, the second 14 | being the standard deviation (if there are multiple readings in the buffer). 15 | 16 | ## 0.9.5: Quantum Design package major update 17 | 18 | - New classes for PPMS, (PPMS) DynaCool, VersaLab, SVSM, and MPMS. The Dynacool class 19 | is just a rework of the old Dynacool class, but this time inherits from a general 20 | QdInstrument parent class. __init__.py has been updated for imports. 21 | - There are two new functions so that users can interface with the rotator module. A 22 | user should be able to get and set the position of the rotator now. However, it 23 | should be noted that this still requires testing (!!!). 24 | - Housekeeping: Lots of changes to whitespace/blank line issues in the SR830 package. 25 | Went ahead and cleaned up formatting issues in many other packages as well. 26 | - More housekeeping: Removed old, unused import statements. 27 | - Changed Nidaq output functions to the old form so that they take both the channel 28 | and the output value. It seemed extremely unwieldy to individually set the channel 29 | "state" and then write the output. 30 | - Implemented some `RuntimeWarning`s if a user didn't receive a response from the Mercury 31 | iPS. The response is an acknowledgment that a command was received. Some functions 32 | have a `RuntimeError` if it's a function that deals with the limits of the magnet (e.g. 33 | making sure that the magnet doesn't go to 20 T!!!). 34 | - Added some more instructions on usage in the README.md 35 | 36 | #### Proposed changes 37 | 38 | - For equipment that do not use cryogenics or vacuum pumps (e.g. NOT the Oxford Triton 39 | 200 or the Mercury iPS), the unit tests should ensure that what is being read out 40 | are the types that are expected (e.g. you expect floats from a resistance reading or 41 | an integer from the state of the lock-in sensitivity). Set functions could have a 42 | test where a query is made to see if the set function set what you expected. 43 | 44 | ## 0.9.1 45 | 46 | - Started keeping a changelog to communicate changes with a wider audience 47 | - Class names are now CamelCase and start with a capital letter 48 | - Took out enable/disable_remote functions in Sr830 and Keithley2400 because they just 49 | felt unnecessary. The instruments are now opened upon instantiation. 50 | 51 | #### Proposed changes 52 | 53 | - Quantum Design has a few other instruments such as a regular PPMS, 54 | an MPMS, and a VersaLab (whatever those are). The decompiled DLL 55 | file is actually really easy to read and interpret, so a future 56 | version of 0.9 should be seeing implementations of those. 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Mason Group, The University of Illinois at Urbana-Champaign 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Documentation Status](https://readthedocs.org/projects/labdrivers/badge/?version=latest)](http://labdrivers.readthedocs.org/en/latest/?badge=latest) 2 | 3 | ## labdrivers 4 | 5 | labdrivers is a Python module containing a collection of drivers for common research lab instruments. 6 | 7 | It contains a suite of instrument-specific drivers which can be used to interface measurement hardware with Python code, 8 | along with a set of Jupyter notebooks demonstrating example use cases. The drivers within the project are intended to 9 | be used 'manually', either in python scripts or in Jupyter notebooks. 10 | 11 | `labdrivers` is not a measurement framework with a GUI; if that's what you're looking 12 | for then you might want to check out one of the projects listed at 13 | [https://github.com/pyinstruments/pyinstruments](https://github.com/pyinstruments/pyinstruments). 14 | 15 | ### Usage 16 | 17 | The package structure is as follows (with the __init__.py omitted): 18 | 19 | labdrivers/ 20 | |-- keithley 21 | `-- keithley2400.py 22 | |-- lakeshore 23 | `-- ls332.py 24 | |-- ni 25 | `-- nidaq.py 26 | |-- oxford 27 | |-- itc503.py 28 | |-- ips120.py 29 | |-- mercuryips.py 30 | `-- triton200.py 31 | |-- quantumdesign 32 | `-- qdinstrument.py 33 | |-- srs 34 | `-- sr830.py 35 | 36 | As an example, you can connect to a Mercury iPS magnet: 37 | 38 | ```python 39 | from labdrivers.oxford import MercuryIps 40 | 41 | mercury_ps = MercuryIps(mode='ip', ip_address='123.456.789.0') 42 | ``` 43 | 44 | Several of the packages exploit the property and setter decorators to query the state of 45 | the equipment or to set the state of the machine. Of course, not everything can be 46 | considered an attribute of the system, so functions can and do exist, as shown below: 47 | 48 | ```python 49 | # Internally, these query the Mercury iPS and receive a response 50 | mercury_ps.x_magnet.field_setpoint = 1.0 51 | mercury_ps.x_magnet.field_ramp_rate = 0.05 52 | 53 | # These are functions that work in the usual way 54 | if mercury_ps.x_magnet.clamped(): 55 | mercury_ps.x_magnet.hold() 56 | 57 | mercury_ps.x_magnet.ramp_to_setpoint() 58 | 59 | with open('test.txt', 'a') as f: 60 | # Note the use of an attribute. There are no opening and closing 61 | # parenthesis after magnetic_field. Writing a () after magnetic_field 62 | # would give a TypeError. 63 | f.write('{}\n'.format(mercury_ps.x_magnet.magnetic_field)) 64 | ``` 65 | 66 | You should look through the documentation or the source code itself in order to determine 67 | the correct usage. 68 | 69 | ### Documentation 70 | 71 | For examples of how to use the drivers please see the Jupyter notebooks in the `example_nbs` folder. 72 | 73 | For the full driver API documentation, along with a description of the design decisions underlying 74 | the driver implementations please see the full documentation at 75 | [labdrivers.readthedocs.org](https://labdrivers.readthedocs.org). 76 | 77 | ### Installation 78 | 79 | You may install using: 80 | 81 | `pip install labdrivers` 82 | 83 | `pip` will also automatically install `pyvisa` and `PyDAQmx` if they are not already installed. 84 | 85 | #### Using the Quantum Design instruments package 86 | 87 | In order for `labdrivers` to read through the C# code of the Quantum Design DLL file, `pythonnet` is required. 88 | In principle, `pythonnet` could be included in the pip-installation but there have been many issues with the 89 | documentation not building and installation issues that have been traced back to installing this module. 90 | Note that `pythonnet` is compatible with **Python 3.6** at most (see its repository), and that it requires non-Python 91 | dependencies like .NET 4.0+. As of writing this, it is not entirely clear what else is required, 92 | though errors regarding Cython have been observed. It might not hurt to have other C/C++ related things installed, 93 | like Visual Studio (if you're on Windows). 94 | 95 | As the problems and solutions become clear, the documentation will be updated. 96 | 97 | ### Contributing new drivers 98 | 99 | Pull requests with new drivers are welcome! 100 | 101 | For a list of coding conventions used within `labdrivers`, along with some 102 | guiding design principles please see [here](http://labdrivers.readthedocs.org/en/latest/contributing.html). 103 | 104 | Also, Before submitting a request please make sure your code follows the PEP8 Python style guidelines (except for the 105 | one concerning maximum line length. 106 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | @echo " coverage to run coverage check of the documentation (if enabled)" 49 | 50 | .PHONY: clean 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | .PHONY: html 55 | html: 56 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 57 | @echo 58 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 59 | 60 | .PHONY: dirhtml 61 | dirhtml: 62 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 63 | @echo 64 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 65 | 66 | .PHONY: singlehtml 67 | singlehtml: 68 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 69 | @echo 70 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 71 | 72 | .PHONY: pickle 73 | pickle: 74 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 75 | @echo 76 | @echo "Build finished; now you can process the pickle files." 77 | 78 | .PHONY: json 79 | json: 80 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 81 | @echo 82 | @echo "Build finished; now you can process the JSON files." 83 | 84 | .PHONY: htmlhelp 85 | htmlhelp: 86 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 87 | @echo 88 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 89 | ".hhp project file in $(BUILDDIR)/htmlhelp." 90 | 91 | .PHONY: qthelp 92 | qthelp: 93 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 94 | @echo 95 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 96 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 97 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/labdrivers.qhcp" 98 | @echo "To view the help file:" 99 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/labdrivers.qhc" 100 | 101 | .PHONY: applehelp 102 | applehelp: 103 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 104 | @echo 105 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 106 | @echo "N.B. You won't be able to view it unless you put it in" \ 107 | "~/Library/Documentation/Help or install it in your application" \ 108 | "bundle." 109 | 110 | .PHONY: devhelp 111 | devhelp: 112 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 113 | @echo 114 | @echo "Build finished." 115 | @echo "To view the help file:" 116 | @echo "# mkdir -p $$HOME/.local/share/devhelp/labdrivers" 117 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/labdrivers" 118 | @echo "# devhelp" 119 | 120 | .PHONY: epub 121 | epub: 122 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 123 | @echo 124 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 125 | 126 | .PHONY: latex 127 | latex: 128 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 129 | @echo 130 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 131 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 132 | "(use \`make latexpdf' here to do that automatically)." 133 | 134 | .PHONY: latexpdf 135 | latexpdf: 136 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 137 | @echo "Running LaTeX files through pdflatex..." 138 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 139 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 140 | 141 | .PHONY: latexpdfja 142 | latexpdfja: 143 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 144 | @echo "Running LaTeX files through platex and dvipdfmx..." 145 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 146 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 147 | 148 | .PHONY: text 149 | text: 150 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 151 | @echo 152 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 153 | 154 | .PHONY: man 155 | man: 156 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 157 | @echo 158 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 159 | 160 | .PHONY: texinfo 161 | texinfo: 162 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 163 | @echo 164 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 165 | @echo "Run \`make' in that directory to run these through makeinfo" \ 166 | "(use \`make info' here to do that automatically)." 167 | 168 | .PHONY: info 169 | info: 170 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 171 | @echo "Running Texinfo files through makeinfo..." 172 | make -C $(BUILDDIR)/texinfo info 173 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 174 | 175 | .PHONY: gettext 176 | gettext: 177 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 178 | @echo 179 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 180 | 181 | .PHONY: changes 182 | changes: 183 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 184 | @echo 185 | @echo "The overview file is in $(BUILDDIR)/changes." 186 | 187 | .PHONY: linkcheck 188 | linkcheck: 189 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 190 | @echo 191 | @echo "Link check complete; look for any errors in the above output " \ 192 | "or in $(BUILDDIR)/linkcheck/output.txt." 193 | 194 | .PHONY: doctest 195 | doctest: 196 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 197 | @echo "Testing of doctests in the sources finished, look at the " \ 198 | "results in $(BUILDDIR)/doctest/output.txt." 199 | 200 | .PHONY: coverage 201 | coverage: 202 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 203 | @echo "Testing of coverage in the sources finished, look at the " \ 204 | "results in $(BUILDDIR)/coverage/python.txt." 205 | 206 | .PHONY: xml 207 | xml: 208 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 209 | @echo 210 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 211 | 212 | .PHONY: pseudoxml 213 | pseudoxml: 214 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 215 | @echo 216 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 217 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # labdrivers documentation build configuration file, created by 5 | # sphinx-quickstart on Fri Apr 15 13:20:44 2016. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import sys 17 | import os 18 | import mock 19 | 20 | # on_rtd is whether we are on readthedocs.org, this line of code grabbed from docs.readthedocs.org 21 | on_rtd = os.environ.get('READTHEDOCS', None) == 'True' 22 | 23 | if not on_rtd: # only import and set the theme if we're building docs locally 24 | import sphinx_rtd_theme 25 | html_theme = 'sphinx_rtd_theme' 26 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 27 | else: 28 | # otherwise, readthedocs.org uses their theme by default, so no need to specify it 29 | pass 30 | 31 | # If extensions (or modules to document with autodoc) are in another directory, 32 | # add these directories to sys.path here. If the directory is relative to the 33 | # documentation root, use os.path.abspath to make it absolute, like shown here. 34 | sys.path.insert(0, os.path.abspath('../')) 35 | 36 | # -- General configuration ------------------------------------------------ 37 | 38 | # If your documentation needs a minimal Sphinx version, state it here. 39 | #needs_sphinx = '1.0' 40 | 41 | # Add any Sphinx extension module names here, as strings. They can be 42 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 43 | # ones. 44 | extensions = [ 45 | 'sphinx.ext.autodoc', 46 | 'sphinx.ext.viewcode', 47 | 'sphinx.ext.napoleon', 48 | ] 49 | 50 | # Add any paths that contain templates here, relative to this directory. 51 | templates_path = ['_templates'] 52 | 53 | # The suffix(es) of source filenames. 54 | # You can specify multiple suffix as a list of string: 55 | # source_suffix = ['.rst', '.md'] 56 | source_suffix = '.rst' 57 | 58 | # The encoding of source files. 59 | #source_encoding = 'utf-8-sig' 60 | 61 | # The master toctree document. 62 | master_doc = 'index' 63 | 64 | # General information about the project. 65 | project = 'labdrivers' 66 | copyright = '2016, Mason Lab' 67 | author = 'Mason Lab' 68 | 69 | # The version info for the project you're documenting, acts as replacement for 70 | # |version| and |release|, also used in various other places throughout the 71 | # built documents. 72 | # 73 | # The short X.Y version. 74 | version = '0.1' 75 | # The full version, including alpha/beta/rc tags. 76 | release = '0.1' 77 | 78 | # The language for content autogenerated by Sphinx. Refer to documentation 79 | # for a list of supported languages. 80 | # 81 | # This is also used if you do content translation via gettext catalogs. 82 | # Usually you set "language" from the command line for these cases. 83 | language = None 84 | 85 | # There are two options for replacing |today|: either, you set today to some 86 | # non-false value, then it is used: 87 | #today = '' 88 | # Else, today_fmt is used as the format for a strftime call. 89 | #today_fmt = '%B %d, %Y' 90 | 91 | # List of patterns, relative to source directory, that match files and 92 | # directories to ignore when looking for source files. 93 | exclude_patterns = ['_build'] 94 | 95 | # The reST default role (used for this markup: `text`) to use for all 96 | # documents. 97 | #default_role = None 98 | 99 | # If true, '()' will be appended to :func: etc. cross-reference text. 100 | #add_function_parentheses = True 101 | 102 | # If true, the current module name will be prepended to all description 103 | # unit titles (such as .. function::). 104 | #add_module_names = True 105 | 106 | # If true, sectionauthor and moduleauthor directives will be shown in the 107 | # output. They are ignored by default. 108 | #show_authors = False 109 | 110 | # The name of the Pygments (syntax highlighting) style to use. 111 | pygments_style = 'sphinx' 112 | 113 | # A list of ignored prefixes for module index sorting. 114 | #modindex_common_prefix = [] 115 | 116 | # If true, keep warnings as "system message" paragraphs in the built documents. 117 | #keep_warnings = False 118 | 119 | # If true, `todo` and `todoList` produce output, else they produce nothing. 120 | todo_include_todos = False 121 | 122 | 123 | # -- Options for HTML output ---------------------------------------------- 124 | 125 | # The theme to use for HTML and HTML Help pages. See the documentation for 126 | # a list of builtin themes. 127 | #html_theme = 'sphinx_rtd_theme' 128 | 129 | # Theme options are theme-specific and customize the look and feel of a theme 130 | # further. For a list of options available for each theme, see the 131 | # documentation. 132 | #html_theme_options = {} 133 | 134 | # Add any paths that contain custom themes here, relative to this directory. 135 | #html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 136 | 137 | # The name for this set of Sphinx documents. If None, it defaults to 138 | # " v documentation". 139 | #html_title = None 140 | 141 | # A shorter title for the navigation bar. Default is the same as html_title. 142 | #html_short_title = None 143 | 144 | # The name of an image file (relative to this directory) to place at the top 145 | # of the sidebar. 146 | #html_logo = None 147 | 148 | # The name of an image file (relative to this directory) to use as a favicon of 149 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 150 | # pixels large. 151 | #html_favicon = None 152 | 153 | # Add any paths that contain custom static files (such as style sheets) here, 154 | # relative to this directory. They are copied after the builtin static files, 155 | # so a file named "default.css" will overwrite the builtin "default.css". 156 | html_static_path = ['_static'] 157 | 158 | # Add any extra paths that contain custom files (such as robots.txt or 159 | # .htaccess) here, relative to this directory. These files are copied 160 | # directly to the root of the documentation. 161 | #html_extra_path = [] 162 | 163 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 164 | # using the given strftime format. 165 | #html_last_updated_fmt = '%b %d, %Y' 166 | 167 | # If true, SmartyPants will be used to convert quotes and dashes to 168 | # typographically correct entities. 169 | #html_use_smartypants = True 170 | 171 | # Custom sidebar templates, maps document names to template names. 172 | #html_sidebars = {} 173 | 174 | # Additional templates that should be rendered to pages, maps page names to 175 | # template names. 176 | #html_additional_pages = {} 177 | 178 | # If false, no module index is generated. 179 | #html_domain_indices = True 180 | 181 | # If false, no index is generated. 182 | #html_use_index = True 183 | 184 | # If true, the index is split into individual pages for each letter. 185 | #html_split_index = False 186 | 187 | # If true, links to the reST sources are added to the pages. 188 | #html_show_sourcelink = True 189 | 190 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 191 | #html_show_sphinx = True 192 | 193 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 194 | #html_show_copyright = True 195 | 196 | # If true, an OpenSearch description file will be output, and all pages will 197 | # contain a tag referring to it. The value of this option must be the 198 | # base URL from which the finished HTML is served. 199 | #html_use_opensearch = '' 200 | 201 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 202 | #html_file_suffix = None 203 | 204 | # Language to be used for generating the HTML full-text search index. 205 | # Sphinx supports the following languages: 206 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 207 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' 208 | #html_search_language = 'en' 209 | 210 | # A dictionary with options for the search language support, empty by default. 211 | # Now only 'ja' uses this config value 212 | #html_search_options = {'type': 'default'} 213 | 214 | # The name of a javascript file (relative to the configuration directory) that 215 | # implements a search results scorer. If empty, the default will be used. 216 | #html_search_scorer = 'scorer.js' 217 | 218 | # Output file base name for HTML help builder. 219 | htmlhelp_basename = 'labdriversdoc' 220 | 221 | # -- Options for LaTeX output --------------------------------------------- 222 | 223 | latex_elements = { 224 | # The paper size ('letterpaper' or 'a4paper'). 225 | #'papersize': 'letterpaper', 226 | 227 | # The font size ('10pt', '11pt' or '12pt'). 228 | #'pointsize': '10pt', 229 | 230 | # Additional stuff for the LaTeX preamble. 231 | #'preamble': '', 232 | 233 | # Latex figure (float) alignment 234 | #'figure_align': 'htbp', 235 | } 236 | 237 | # Grouping the document tree into LaTeX files. List of tuples 238 | # (source start file, target name, title, 239 | # author, documentclass [howto, manual, or own class]). 240 | latex_documents = [ 241 | (master_doc, 'labdrivers.tex', 'labdrivers Documentation', 242 | 'Mason Lab', 'manual'), 243 | ] 244 | 245 | # The name of an image file (relative to this directory) to place at the top of 246 | # the title page. 247 | #latex_logo = None 248 | 249 | # For "manual" documents, if this is true, then toplevel headings are parts, 250 | # not chapters. 251 | #latex_use_parts = False 252 | 253 | # If true, show page references after internal links. 254 | #latex_show_pagerefs = False 255 | 256 | # If true, show URL addresses after external links. 257 | #latex_show_urls = False 258 | 259 | # Documents to append as an appendix to all manuals. 260 | #latex_appendices = [] 261 | 262 | # If false, no module index is generated. 263 | #latex_domain_indices = True 264 | 265 | 266 | # -- Options for manual page output --------------------------------------- 267 | 268 | # One entry per manual page. List of tuples 269 | # (source start file, name, description, authors, manual section). 270 | man_pages = [ 271 | (master_doc, 'labdrivers', 'labdrivers Documentation', 272 | [author], 1) 273 | ] 274 | 275 | # If true, show URL addresses after external links. 276 | #man_show_urls = False 277 | 278 | 279 | # -- Options for Texinfo output ------------------------------------------- 280 | 281 | # Grouping the document tree into Texinfo files. List of tuples 282 | # (source start file, target name, title, author, 283 | # dir menu entry, description, category) 284 | texinfo_documents = [ 285 | (master_doc, 'labdrivers', 'labdrivers Documentation', 286 | author, 'labdrivers', 'One line description of project.', 287 | 'Miscellaneous'), 288 | ] 289 | 290 | # Documents to append as an appendix to all manuals. 291 | #texinfo_appendices = [] 292 | 293 | # If false, no module index is generated. 294 | #texinfo_domain_indices = True 295 | 296 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 297 | #texinfo_show_urls = 'footnote' 298 | 299 | # If true, do not generate a @detailmenu in the "Top" node's menu. 300 | #texinfo_no_detailmenu = False 301 | 302 | # mock some modules that we don't need just to build the docs 303 | MOCK_MODULES = ['pandas', 'numpy','clr','visa','pyvisa','pyvisa.errors', 'QuantumDesign.QDInstrument', 'PyDAQmx'] 304 | for mod_name in MOCK_MODULES: 305 | sys.modules[mod_name] = mock.Mock() 306 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | ######################### 2 | Contributing new drivers 3 | ######################### 4 | 5 | Each driver is implemented as a class named after the corresponding instrument. 6 | The driver classes are intended to be used directly and usable by anyone who decides to use 7 | these for experiment. 8 | 9 | ^^^^^^^^^^^^^^^^^^ 10 | Coding conventions 11 | ^^^^^^^^^^^^^^^^^^ 12 | 13 | The modules in the `labdrivers` package follow the following conventions: 14 | 15 | - Class names are all CamelCase, e.g.:: 16 | 17 | Keithley2400 18 | Sr830 19 | Dynacool 20 | 21 | - Method names are in lower case (there are exceptions, 22 | like in the Quantum Design instruments classes), e.g.:: 23 | 24 | measure_single_point 25 | auto_gain 26 | set_output_voltage 27 | 28 | Depending on the future of the use of the `labdrivers` package, the coding conventions may be revised 29 | to maintain consistency. 30 | 31 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 32 | Driver design principles 33 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 34 | 35 | - Minimize the amount of 'internal state' maintained by the driver 36 | 37 | The only 'internal state' that an instance should keep are properties like an IP address 38 | or a GPIB resource name. Most classes use the property decorator, but they are used only 39 | to make a query and return the response directly to the user. 40 | 41 | E.g. do this:: 42 | 43 | def getOutput(self): 44 | return self.instrument.query("OUTPUT?") 45 | 46 | instead of this:: 47 | 48 | def setOutput(self, out_type, level): 49 | self.instrument.write("OUTPUT {out_type} {level}".format(out_type, level)) 50 | self.output = (out_type, level) 51 | 52 | def getOutput(): 53 | return self.output 54 | 55 | - Use property decorators when possible to avoid writing getter and setter methods 56 | 57 | - Use function names that are intuitive and minimize the amount of input required. 58 | 59 | E.g. do this:: 60 | 61 | def output_current(self, current): 62 | thing.output('current', current) 63 | 64 | def output_voltage(self, voltage): 65 | thing.output('voltage', voltage) 66 | 67 | It might be more "efficient" to just allow for two inputs, but generally it would be less confusing 68 | if there were only one input. This is a change from older versions of `labdrivers` (0.8.x and below?). 69 | 70 | ^^^^^^^^^^^^^^^^^^^^^^^ 71 | Documenting drivers 72 | ^^^^^^^^^^^^^^^^^^^^^^^ 73 | 74 | Each method in the driver should be documented using a the reStructuredText format. 75 | 76 | Example:: 77 | 78 | """ 79 | First line of documentation. 80 | 81 | :param thing1: Description of thing1 parameter 82 | :param thing2: Description of thing2 parameter 83 | :returns: Description of the returned data 84 | :raises SomeError: Description of when SomeError shows up 85 | """ 86 | 87 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to labdrivers' documentation! 2 | ====================================== 3 | 4 | labdrivers is a Python module containing a collection of drivers for common research lab instruments. 5 | 6 | It contains a suite of instrument-specific drivers which can be used to 7 | interface measurement hardware with Python code, along with a set of 8 | Jupyter notebooks demonstrating example use cases. 9 | The drivers within the project are intended to be used 'manually', either in python scripts or in Jupyter notebooks. 10 | 11 | Labdrivers is not a measurement framework with a GUI; if that's what you're looking for 12 | then you might want to check out one of the projects listed at https://github.com/pyinstruments/pyinstruments. 13 | 14 | Organization of the project 15 | ---------------------------- 16 | 17 | Drivers within the project are organized by instrument brand. Each 18 | instrument manufacturer has its own submodule under the :code:`labdrivers` namespace, and 19 | each individual instrument driver is in a file named after the class which 20 | implements the driver. 21 | 22 | Currently labdrivers contains drivers for the following instruments:: 23 | 24 | labdrivers/ 25 | |-- keithley 26 | `-- keithley2400.py 27 | |-- lakeshore 28 | `-- ls332.py 29 | |-- ni 30 | `-- nidaq.py 31 | |-- oxford 32 | |-- itc503.py 33 | |-- ips120.py 34 | |-- mercuryips.py 35 | `-- triton200.py 36 | |-- quantumdesign 37 | `-- qdinstrument.py 38 | |-- srs 39 | `-- sr830.py 40 | 41 | So for example, to load the driver for a Keithley 2400 SMU:: 42 | 43 | from labdrivers.keithley import keithley2400 44 | 45 | 46 | Documentation Table of Contents 47 | ------------------------------- 48 | 49 | .. toctree:: 50 | :maxdepth: 4 51 | 52 | installation 53 | usage 54 | moduleapis 55 | contributing 56 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | ############## 2 | Installation 3 | ############## 4 | 5 | Before attempting to use the drivers for the Quantum Design instruments, 6 | please be sure that you are using a version of Python which is 3.6.x 7 | and below. Additionally, please be sure that .NET 4.0+ is installed, 8 | as that allows for access to the classes within the compiled C# code. 9 | 10 | There are a couple ways to install labdrivers. 11 | 12 | Installing to the system Python Folder 13 | -------------------------------------- 14 | 15 | To install labdrivers to the default Python installation you 16 | can use `pip`:: 17 | 18 | > pip install labdrivers 19 | 20 | Note that this requires `git`, which can be found here: https://git-scm.com/. 21 | 22 | If you are running an outdated version of `labdrivers`, please upgrade:: 23 | 24 | > pip install --upgrade labdrivers 25 | 26 | If you prefer to install manually, download the labdrivers package from 27 | here: https://github.com/masonlab/labdrivers, then copy the `labdrivers` folder 28 | to:: 29 | 30 | (system python path)/lib/site-packages/ -------------------------------------------------------------------------------- /docs/labdrivers.keithley.rst: -------------------------------------------------------------------------------- 1 | ########################## 2 | labdrivers.keithley 3 | ########################## 4 | 5 | .. toctree:: 6 | :maxdepth: 1 7 | 8 | 9 | .. autoclass:: labdrivers.keithley.keithley2400.Keithley2400 10 | :members: 11 | :undoc-members: 12 | -------------------------------------------------------------------------------- /docs/labdrivers.lakeshore.rst: -------------------------------------------------------------------------------- 1 | ########################## 2 | labdrivers.lakeshore 3 | ########################## 4 | 5 | .. toctree:: 6 | :maxdepth: 1 7 | 8 | 9 | .. autoclass:: labdrivers.lakeshore.ls332.Ls332 10 | :members: 11 | :undoc-members: 12 | -------------------------------------------------------------------------------- /docs/labdrivers.ni.rst: -------------------------------------------------------------------------------- 1 | ########################## 2 | labdrivers.ni 3 | ########################## 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | 8 | 9 | .. autoclass:: labdrivers.ni.nidaq.Nidaq 10 | :members: 11 | :undoc-members: 12 | -------------------------------------------------------------------------------- /docs/labdrivers.oxford.rst: -------------------------------------------------------------------------------- 1 | ########################## 2 | labdrivers.oxford 3 | ########################## 4 | 5 | .. toctree:: 6 | :maxdepth: 4 7 | 8 | 9 | .. autoclass:: labdrivers.oxford.itc503.Itc503 10 | :members: 11 | :undoc-members: 12 | 13 | 14 | .. autoclass:: labdrivers.oxford.ips120.Ips120 15 | :members: 16 | :undoc-members: 17 | 18 | 19 | .. autoclass:: labdrivers.oxford.mercuryips.MercuryIps 20 | :members: 21 | :undoc-members: 22 | 23 | 24 | .. autoclass:: labdrivers.oxford.triton200.Triton200 25 | :members: 26 | :undoc-members: 27 | -------------------------------------------------------------------------------- /docs/labdrivers.quantumdesign.rst: -------------------------------------------------------------------------------- 1 | ########################## 2 | labdrivers.quantumdesign 3 | ########################## 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | 8 | 9 | .. autoclass:: labdrivers.quantumdesign.qdinstrument.QdInstrument 10 | :members: 11 | :undoc-members: 12 | 13 | 14 | .. autoclass:: labdrivers.quantumdesign.qdinstrument.Dynacool 15 | :members: 16 | :undoc-members: 17 | 18 | 19 | .. autoclass:: labdrivers.quantumdesign.qdinstrument.Ppms 20 | :members: 21 | :undoc-members: 22 | 23 | 24 | .. autoclass:: labdrivers.quantumdesign.qdinstrument.Svsm 25 | :members: 26 | :undoc-members: 27 | 28 | 29 | .. autoclass:: labdrivers.quantumdesign.qdinstrument.VersaLab 30 | :members: 31 | :undoc-members: 32 | 33 | 34 | .. autoclass:: labdrivers.quantumdesign.qdinstrument.Mpms 35 | :members: 36 | :undoc-members: 37 | -------------------------------------------------------------------------------- /docs/labdrivers.srs.rst: -------------------------------------------------------------------------------- 1 | ########################## 2 | labdrivers.srs 3 | ########################## 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | 8 | 9 | .. autoclass:: labdrivers.srs.sr830.Sr830 10 | :members: 11 | :undoc-members: 12 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | echo. coverage to run coverage check of the documentation if enabled 41 | goto end 42 | ) 43 | 44 | if "%1" == "clean" ( 45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 46 | del /q /s %BUILDDIR%\* 47 | goto end 48 | ) 49 | 50 | 51 | REM Check if sphinx-build is available and fallback to Python version if any 52 | %SPHINXBUILD% 1>NUL 2>NUL 53 | if errorlevel 9009 goto sphinx_python 54 | goto sphinx_ok 55 | 56 | :sphinx_python 57 | 58 | set SPHINXBUILD=python -m sphinx.__init__ 59 | %SPHINXBUILD% 2> nul 60 | if errorlevel 9009 ( 61 | echo. 62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 63 | echo.installed, then set the SPHINXBUILD environment variable to point 64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 65 | echo.may add the Sphinx directory to PATH. 66 | echo. 67 | echo.If you don't have Sphinx installed, grab it from 68 | echo.http://sphinx-doc.org/ 69 | exit /b 1 70 | ) 71 | 72 | :sphinx_ok 73 | 74 | 75 | if "%1" == "html" ( 76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 80 | goto end 81 | ) 82 | 83 | if "%1" == "dirhtml" ( 84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 88 | goto end 89 | ) 90 | 91 | if "%1" == "singlehtml" ( 92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 93 | if errorlevel 1 exit /b 1 94 | echo. 95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 96 | goto end 97 | ) 98 | 99 | if "%1" == "pickle" ( 100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 101 | if errorlevel 1 exit /b 1 102 | echo. 103 | echo.Build finished; now you can process the pickle files. 104 | goto end 105 | ) 106 | 107 | if "%1" == "json" ( 108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 109 | if errorlevel 1 exit /b 1 110 | echo. 111 | echo.Build finished; now you can process the JSON files. 112 | goto end 113 | ) 114 | 115 | if "%1" == "htmlhelp" ( 116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 117 | if errorlevel 1 exit /b 1 118 | echo. 119 | echo.Build finished; now you can run HTML Help Workshop with the ^ 120 | .hhp project file in %BUILDDIR%/htmlhelp. 121 | goto end 122 | ) 123 | 124 | if "%1" == "qthelp" ( 125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 129 | .qhcp project file in %BUILDDIR%/qthelp, like this: 130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\labdrivers.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\labdrivers.ghc 133 | goto end 134 | ) 135 | 136 | if "%1" == "devhelp" ( 137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. 141 | goto end 142 | ) 143 | 144 | if "%1" == "epub" ( 145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 149 | goto end 150 | ) 151 | 152 | if "%1" == "latex" ( 153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 157 | goto end 158 | ) 159 | 160 | if "%1" == "latexpdf" ( 161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 162 | cd %BUILDDIR%/latex 163 | make all-pdf 164 | cd %~dp0 165 | echo. 166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdfja" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf-ja 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "text" ( 181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 182 | if errorlevel 1 exit /b 1 183 | echo. 184 | echo.Build finished. The text files are in %BUILDDIR%/text. 185 | goto end 186 | ) 187 | 188 | if "%1" == "man" ( 189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 190 | if errorlevel 1 exit /b 1 191 | echo. 192 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 193 | goto end 194 | ) 195 | 196 | if "%1" == "texinfo" ( 197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 198 | if errorlevel 1 exit /b 1 199 | echo. 200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 201 | goto end 202 | ) 203 | 204 | if "%1" == "gettext" ( 205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 206 | if errorlevel 1 exit /b 1 207 | echo. 208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 209 | goto end 210 | ) 211 | 212 | if "%1" == "changes" ( 213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 214 | if errorlevel 1 exit /b 1 215 | echo. 216 | echo.The overview file is in %BUILDDIR%/changes. 217 | goto end 218 | ) 219 | 220 | if "%1" == "linkcheck" ( 221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 222 | if errorlevel 1 exit /b 1 223 | echo. 224 | echo.Link check complete; look for any errors in the above output ^ 225 | or in %BUILDDIR%/linkcheck/output.txt. 226 | goto end 227 | ) 228 | 229 | if "%1" == "doctest" ( 230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Testing of doctests in the sources finished, look at the ^ 234 | results in %BUILDDIR%/doctest/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "coverage" ( 239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of coverage in the sources finished, look at the ^ 243 | results in %BUILDDIR%/coverage/python.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "xml" ( 248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 252 | goto end 253 | ) 254 | 255 | if "%1" == "pseudoxml" ( 256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 257 | if errorlevel 1 exit /b 1 258 | echo. 259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 260 | goto end 261 | ) 262 | 263 | :end 264 | -------------------------------------------------------------------------------- /docs/moduleapis.rst: -------------------------------------------------------------------------------- 1 | ################# 2 | Driver APIs 3 | ################# 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | 8 | labdrivers.keithley 9 | labdrivers.lakeshore 10 | labdrivers.oxford 11 | labdrivers.quantumdesign 12 | labdrivers.srs 13 | labdrivers.ni 14 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | ################# 2 | Using the drivers 3 | ################# 4 | 5 | For examples of how to use the various instrument drivers please see 6 | the example Jupyter notebooks included in the github repository 7 | `(link) `_. 8 | -------------------------------------------------------------------------------- /example_nbs/2d_conductance.py: -------------------------------------------------------------------------------- 1 | ''' 2 | TODO: 3 | 4 | - Set up program to allow for mid-experiment parameter changes 5 | - Send a pull request to have the option of not resetting K2400 6 | ''' 7 | 8 | # database manipulation/operations 9 | import pandas as pd 10 | import numpy as np 11 | 12 | # data plotting 13 | import matplotlib.pyplot as plt 14 | import seaborn as sns 15 | 16 | # import drivers for experiment 17 | from labdrivers.ni import nidaq 18 | from labdrivers.keithley import keithley2400 19 | from labdrivers.srs import sr830 20 | 21 | # experimental constants 22 | current_preamp_sensitivity = -7 23 | lockin_voltage_amplitude = 0.100 24 | ac_division = 1.0 25 | dc_division = 1.0 26 | single_mode_conductance = 0.000077480917310 # in Siemens/inverse Ohms 27 | 28 | # set up instrument channels 29 | daq_output_channel = 'ao1' 30 | keithley_gpib_channel = 23 31 | lockin_gpib_channel = 8 32 | 33 | # initialize DAQ, K2400, SR830 34 | daq = bnc2110() 35 | keithley = keithley2400(keithley_gpib_channel) 36 | lockin = sr830(lockin_gpib_channel) 37 | 38 | # scanning parameters 39 | gate_min = -10.0 40 | gate_max = 10.0 41 | gate_step = 0.1 42 | bias_min = -5.0 43 | bias_max = 5.0 44 | bias_step = 0.1 45 | 46 | # establish the range lists 47 | gate_voltages = range(gate_min, gate_max + gate_step, 48 | gate_step) 49 | bias_voltages = range(bias_min, bias_max + bias_step, 50 | bias_step) 51 | 52 | # setting up data and meta-data 53 | columns = ['Gate Voltage (V)','Bias Voltage (mV)','dI/dV (2e^2/h)'] 54 | experimental_data = pd.DataFrame(columns=columns) 55 | 56 | # helper function to calculate differential conductance 57 | def get_diff_conductance(lockin_output): 58 | 59 | current = lockin_output * 10 ** current_preamp_sensitivity 60 | input_voltage = lockin.getAmplitude() / ac_division 61 | 62 | return current / input_voltage 63 | 64 | # experimental loops 65 | for gate in gate_voltages: 66 | 67 | keithley.setSourceDC(source='voltage', value=gate) 68 | 69 | for bias in bias_voltages: 70 | 71 | daq.setVoltageOutput(channel=daq_output_channel) 72 | lockin_reading = daq.getSinglePoint(1) 73 | data_to_append = [gate, bias / dc_division, 74 | get_diff_conductance(lockin_reading)] 75 | 76 | data_to_append = pd.DataFrame(data_to_append, 77 | columns=columns) 78 | experimental_data.append(data_to_append, 79 | ignore_index=True) -------------------------------------------------------------------------------- /example_nbs/IV Curve - bnc2110 (DAC).ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": { 7 | "collapsed": true 8 | }, 9 | "outputs": [], 10 | "source": [ 11 | "# required to make plots show up\n", 12 | "%matplotlib inline" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": 2, 18 | "metadata": { 19 | "collapsed": false 20 | }, 21 | "outputs": [], 22 | "source": [ 23 | "import numpy as np\n", 24 | "import matplotlib.pyplot as plt\n", 25 | "from labdrivers.ni import bnc2110" 26 | ] 27 | }, 28 | { 29 | "cell_type": "code", 30 | "execution_count": 5, 31 | "metadata": { 32 | "collapsed": false 33 | }, 34 | "outputs": [ 35 | { 36 | "data": { 37 | "text/plain": [ 38 | "" 39 | ] 40 | }, 41 | "execution_count": 5, 42 | "metadata": {}, 43 | "output_type": "execute_result" 44 | }, 45 | { 46 | "data": { 47 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAaMAAAEPCAYAAADvS6thAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJzt3Xu8VVW99/HPF++a8mg+oAfvN1CSBzAJ87YzL6AWZmZo\nHUWt6FGOlVqg+Rx3ZiexJC8cj5mKUCreKtGDgghL0xKQSyB3uqCikB1FxRu33/PHmOhqtzd7s/da\ne6699vf9eq0Xa841xpq/Odnw22PMMcdQRGBmZpanDnkHYGZm5mRkZma5czIyM7PcORmZmVnunIzM\nzCx3TkZmZpa73JORpH6SFkpaLGloA2VukrRE0mxJPbN920iaKmmWpLmSrioqv7OkiZIWSZogqWNr\nnY+ZmW2+XJORpA7ASOAkoDtwlqRudcr0B/aPiAOBwcCtABHxAfCZiOgF9AT6S+qTVRsGTIqIrsBk\n4PLWOB8zM2uevFtGfYAlEbEsItYCY4EBdcoMAMYARMRUoKOkztn2u1mZbYAtgSiqMzp7Pxo4rWxn\nYGZmLZZ3MuoCvFS0/XK2b1Nllm8sI6mDpFnACuCJiJielekUESsBImIF0KkMsZuZWYnknYxaJCI2\nZN10ewCfknRIQ0VbMSwzM9tMW+Z8/OXAXkXbe2T76pbZc1NlIuItSVOAfsB8YKWkzhGxUtJuwN/q\nO7gkJykzs2aICJXy+/JuGU0HDpC0t6StgYHAuDplxgHnAEjqC6zKksyuG0fJSdoOOAFYWFRnUPb+\nXODhhgKIiIp/XXXVVbnH4DgdZ1uOsy3E2JbiLIdcW0YRsV7SEGAiKTHeERELJA1OH8dtETFe0smS\nlgLvAOdl1XcHRmcj8joA90XE+Oyz4cD9ks4HlgFntuZ5mZnZ5sm7m46IeBzoWmffz+tsD6mn3lyg\ndwPf+TpwfAnDNDOzMsq7m86aoKamJu8QmsRxlpbjLJ22ECO0nTjLQeXq/2sLJEV7Pn8zs+aQRFTZ\nAAYzMzMnIzMzy5+TkZmZ5c7JyMzMcudkZGZmuXMyMjOz3DkZmZlZ7pyMzMwsd05GZmaWOycjMzPL\nnZORmZnlzsnIzMxy52RkZma5czIyM7MPzZsHa9e2/nFzT0aS+klaKGmxpKENlLlJ0hJJsyX1zPbt\nIWmypHmS5kq6uKj8VZJeljQze/VrrfMxM2urpkyBmhqYM6f1j53rSq/ZkuEjgc8CrwDTJT0cEQuL\nyvQH9o+IAyV9CrgV6AusAy6JiNmSPgbMkDSxqO6IiBjRqidkZtZGjR8P554LDzwAhx3W+sfPu2XU\nB1gSEcsiYi0wFhhQp8wAYAxAREwFOkrqHBErImJ2tn81sADoUlSvpAs/mZlVq4cegvPOg0ceSS2j\nPOSdjLoALxVtv8w/JpT6yiyvW0bSPkBPYGrR7iFZt97tkjqWKmAzs2oyZgwMGQITJkDfvvnFkWs3\nXSlkXXQPAt/KWkgAtwBXR0RIugYYAVxQX/3a2toP39fU1LTrNejNrH259Va45hqYPBkOPrjhcoVC\ngUKhUNZYFBFlPcAmDy71BWojol+2PQyIiBheVOZWYEpE3JdtLwSOjYiVkrYEHgUei4gbGzjG3sAj\nEdGjns8iz/M3M8vLiBFw880waRLsv//m1ZVERJT0VkjeLaPpwAFZwngVGAicVafMOOAi4L4sea2K\niJXZZ3cC8+smIkm7RcSKbPN04IVynYCZWSVYswauuAJmzICePaFXr/Tq1g222uqjchHwwx/C3XfD\n00/DnnvmF3OxXFtGkIZ2AzeS7l/dERHXShpMaiHdlpUZCfQD3gEGRcQsSUcCTwNzgcheV0TE45LG\nkO4hbQD+CgwuSmDFx3bLyMzavGXL4MwzYffd4ZvfTEOzZ81KrxdfhEMO+Sg5LVqUuuUmTYLOnZt3\nvHK0jHJPRnlyMjKztu6xx2DQIPjud+HSS0F1UsTq1Sk5zZ6dktPq1TByJHz8480/ppNRiTkZmVlb\ntW4dXHUVjB4N994LRx/deseuxntGZma2mVasgLPPhg4dYOZM6NQp74haLu/njMzMbDM89VSaIeGo\no9KzQdWQiMAtIzOzNiECrr8efvpTuOsu6FdlM246GZmZVbgI+N73Ukto+vTKGY5dSk5GZmYVbMOG\nNF3P889DoQC77JJ3ROXhZGRmVqHWrYMLLoC//CU9F7TTTnlHVD5ORmZmFWjNGvjKV+Ctt+Dxx2H7\n7fOOqLw8ms7MrMK89x584QtpxdVx46o/EYGTkZlZRVm9Gk49NXXJPfAAbLNN3hG1DicjM7MKsWoV\nnHgi7LMP/OpX/zjBabVzMjIzqwDLlsFxx8Hhh8MvfgFbbJF3RK3LycjMLEerV8OVV0Lv3jBwINxw\nQ5rmp71ph6dsZpa/DRvSJKddu6ZW0R//mB5srTvrdnvhod1mZq3s2Wfh299OXXEPPQR9++YdUf6c\njMzMWsmLL8LQofDMM3DttXDWWe2zS64+uV8GSf0kLZS0WNLQBsrcJGmJpNmSemb79pA0WdI8SXMl\nXVxUfmdJEyUtkjRBUsfWOh8zs7rmz08L3/XqlbrlFi5MD7Q6EX0k10shqQMwEjgJ6A6cJalbnTL9\ngf0j4kBgMHBr9tE64JKI6A4cAVxUVHcYMCkiugKTgcvLfjJmZkXefBNuuy11wR1/fBqmPXs21NbC\nDjvkHV3lyTsv9wGWRMSyiFgLjAUG1CkzABgDEBFTgY6SOkfEioiYne1fDSwAuhTVGZ29Hw2cVt7T\nMDNLgxIKBTjnHNh7b5g4Ef7f/0vdc9deW52zbZdK3veMugAvFW2/TEpQmyqzPNu3cuMOSfsAPYHn\nsl2dImIlQESskFQly0+ZWSVavx5+8pPUEvrYx9LkpiNGwK675h1Z25F3MmoxSR8DHgS+FRHvNFAs\nGqpfW1v74fuamhpqampKGZ6ZVbk1a+CrX4W//z1N39O7d/UNzy4UChQKhbIeQxEN/j9ddpL6ArUR\n0S/bHgZERAwvKnMrMCUi7su2FwLHRsRKSVsCjwKPRcSNRXUWADVZmd2y+gfXc/zI8/zNrG17/334\n0pdS8rn/fth227wjah2SiIiSpty87xlNBw6QtLekrYGBwLg6ZcYB58CHyWvVxi444E5gfnEiKqoz\nKHt/LvBwGWI3s3bsnXfShKY77JCeFWoviahccm0ZQRraDdxISox3RMS1kgaTWki3ZWVGAv2Ad4BB\nETFL0pHA08BcUjdcAFdExOOSdgHuB/YElgFnRsSqeo7tlpGZbbY334RTToEDD4Tbb29/88iVo2WU\nezLKk5ORmW2uv/8d+vVLQ7Zvuql9PitUjd10ZmZtxooVUFOTnhu6+eb2mYjKxZfSzKwJXnwRjjkm\nzaz94x9X34i5vLX5od1mZi3161+nKXoaEpHWGPrWt+A732m9uNoT3zNqx+dv1t69+y5cdBFMnQqn\nNTJPy2GHwRe/2DpxVbpy3DNyy8jM2qXFi+GMM6BHD5g2Lc2cYPnxPSMza3ceeACOPDK1in75Syei\nSuCWkZm1G2vWwGWXwaOPwuOPp643qwxORmbWLrz4Ipx5JnTuDDNmwM475x2RFXM3nZlVvcceg8MP\nTwMQfvtbJ6JK5JaRmVWt9evhqqvgrrvgwQfh6KPzjsga4mRkZlVp5Uo4++z0fsaM1D1nlcvddGZW\ndZ5+Og1OOPLItNqqE1Hlc8vIzKpGRFpxdcQIGDUK+vfPOyJrKicjM6sKb7wBgwal7rlp02CvvfKO\nyDaHu+nMrM2bMSN1y+2zT+qicyJqe9wyMrM264MP4MYbU9fcLbekJcCtbcq9ZSSpn6SFkhZLGtpA\nmZskLZE0W1Kvov13SFopaU6d8ldJelnSzOzVr9znYWatJyI9L9S9OzzzDPzhD05EbV2uLSNJHYCR\nwGeBV4Dpkh6OiIVFZfoD+0fEgZI+BfwX0Df7eBRwMzCmnq8fEREjynoCZtbq5sxJyzisXJlaQyee\nmHdEVgp5t4z6AEsiYllErAXGAgPqlBlAlmwiYirQUVLnbPsZ4I0GvttLX5lVkddeg29+E044Ic2k\nMHu2E1E1yTsZdQFeKtp+Odu3qTLL6ylTnyFZt97tkjq2LEwzy8uaNXD99XDIIbDttmkRvAsvhC19\nx7uqVOtf5y3A1RERkq4BRgAX1Fewtrb2w/c1NTXU1NS0Rnxm1gQvvJAmN91vP/jd76Bbt7wjap8K\nhQKFQqGsx8h1pVdJfYHaiOiXbQ8DIiKGF5W5FZgSEfdl2wuBYyNiZba9N/BIRPRo4BgNfu6VXs0q\n15gxcOmlqVV0zjl5R2PFqnGl1+nAAVnCeBUYCJxVp8w44CLgvix5rdqYiDKizv0hSbtFxIps83Tg\nhXIEb2al9/77cPHF6XmhKVPgE5/IOyJrDbkmo4hYL2kIMJF0/+qOiFggaXD6OG6LiPGSTpa0FHgH\nOG9jfUn3ADXAxyW9CFwVEaOA6yT1BDYAfwUGt+qJmVmz/OlPaSnwrl1h+nTYcce8I7LWkms3Xd7c\nTWdWOX7zGxg8OC35cOGFII+HrVjV2E1nZu3c2rUwbBg89FBaDrxPn7wjsjw4GZlZLiLS7AmXXw4d\nO6b55T7+8byjsrw4GZlZq3r55TRSbtQo2HpruOii9DBrh7yferRcORmZWdl98AGMGwd33glTp6Zn\nh+6+Gw4/3PeGLHEyMrOyWbQozR93zz3Qowecd166N7T99nlHZpXGDWMzK7nXX0/PCh11VLofNG0a\nPPkkfPWrTkRWPycjMyuZtWth5Mg0bc+6dbBgAVx9Ney7b96RWaVzN52ZlcSECWlphy5dYPJkz5xg\nm8fJyMxaZNEiuOQSWLw4zSP3uc95UIJtPnfTmVmzvPFGagkddRQcdxzMmwef/7wTkTWPk5GZbZZ1\n69IIuW7d4N13UxK69NL0zJBZc7mbzsya7IknUmuoU6f0vke9C7eYbT4nIzNr1OLFcNllMH8+/PSn\nMGCAu+OstNxNZ2YNWrUqdcF9+tNw9NGpS+6005yIrPScjMzsn6xbB7femu4Lvf12SkLf/S5ss03e\nkVm1yj0ZSeonaaGkxZKGNlDmJklLJM2W1Kto/x2SVkqaU6f8zpImSlokaYKkjuU+D7Nq8eST0Ls3\njB0Ljz8Ot90GnTvnHZVVu1yTkaQOwEjgJKA7cJakbnXK9Af2j4gDSSu2/lfRx6OyunUNAyZFRFdg\nMnB5GcI3qypLlqR7QV//OtTWpiW/e/bMOyprL/JuGfUBlkTEsohYC4wFBtQpMwAYAxARU4GOkjpn\n288Ab9TzvQOA0dn70cBpZYjdrCq8+WbqgjviiHRvaP58OP103xey1pV3MuoCvFS0/XK2b1NlltdT\npq5OEbESICJWAJ1aGKdZ1Vm/PnXBde2aHmB94QUYOhS23TbvyKw9ai9DuyPvAMwqyapVH03bM358\nukdklqcmJSNJ+wMvR8QHkmqAHsCYiFjVwuMvB/Yq2t4j21e3zJ6NlKlrpaTOEbFS0m7A3xoqWFtb\n++H7mpoaampqGo/arA37+9/hxBPTND433OAVVq1xhUKBQqFQ1mMoovFGg6TZwCeBfYDxwMNA94g4\nuUUHl7YAFgGfBV4FpgFnRcSCojInAxdFxCmS+gI3RETfos/3AR6JiEOL9g0HXo+I4dkIvZ0jYlg9\nx4+mnL9ZtXj1VTj++DRQ4Uc/8n0hax5JRERJf3qa+jvRhohYB3wBuDkivgvs3tKDR8R6YAgwEZgH\njI2IBZIGS/pGVmY88BdJS4GfAxdurC/pHuD3wEGSXpR0XvbRcOAESRsT3bUtjdWsrVu2DI45Br7y\nFfiP/3AissrS1JbRVOAG4PvA5yLiL5JeiIg2vWKJW0bWXixZAiecAN/+dnqZtUSeLaPzgCOAH2WJ\naF/gl6UMxMzK44UXoKYGrrzSicgqV5NaRtXKLSOrdjNmwCmnwIgRcPbZeUdj1SK3lpGkUyXNkvS6\npLckvS3prVIGYmal9eyz0L9/mmPOicgqXVPvGS0FTgfmVlNTwi0jqzYR8MwzcOedMG4c3HMPnFTf\nhFlmLZDnPaOXgBf8P7dZZVq+PI2QO+ggGDwYundP0/o4EVlb0dQZGL4HjJf0FPDBxp0RMaIsUZlZ\noz74ILV+Ro2C556DL30JfvUr6NPHw7at7WlqMvoRsBrYFvBK92Y5evdduP56uOkmOPRQOP98ePBB\n2H77vCMza76mJqN/aevPFJm1dRFpjaGhQ9MM23/4AxxwQN5RmZVGU5PReEknRsTEskZjZvWaNi09\nI/TBB3D33WkJcLNq0tTRdG8DO5DuF60FBERE7FTe8MrLo+ms0i1fDpdfDpMmpbnkzj3XE5ta/nIb\nTRcRO0ZEh4jYLiJ2yrbbdCIyq2TvvQc//CH06AF77AGLFsF55zkRWfVq8npGkroAexfXiYinyxGU\nWXsVAffdl+4L9ekDzz8P++6bd1Rm5dfU9YyGA18G5gPrs90BOBmZlcj06em+0LvvwpgxcOyxeUdk\n1nqaes9oEdAjIj5otHAb4ntGVgleeQWuuAImTIBrroFBg2CLLfKOyqxhec7A8Gdgq1Ie2Ky9e++9\nNCjh0ENht93SfaELLnAisvZpk910km4mdce9C8yW9CT/OAPDxeUNz6z6RMADD8D3vgeHHZaGbe+/\nf95RmeWrsXtGz2d/zgcKpMS0DnivVAFI6kdauK8DcEdEDK+nzE1Af+AdYFBEzN5UXUlXAV8H/pZ9\nxRUR8XipYjZrrhkz0n2ht95K0/h85jN5R2RWGTZ5z0jSVqSpgM4HlpGeL9oLGEX6D35tiw4udQAW\nk5YGfwWYDgyMiIVFZfoDQyLiFEmfAm6MiL6bqpslo7cbmzvP94ystbz6Knz/+zB+fBqyff757o6z\ntiuPe0bXATsD+0bEYRHRG9gP6Aj8pATH7wMsiYhlWWIbCwyoU2YAMAYgIqYCHSV1bkJdTxVpuXv/\nffjxj+ETn4Bdd033hb7+dScis7oaS0anAt+IiLc37oiIt4D/C5xSguN3IS1PsdHL2b6mlGms7hBJ\nsyXdLqljCWI1a7KINHnpwQfD1Knpdd110NE/iWb1aiwZRX39WBGxnnT/KA9NafHcAuwXET2BFYCX\nurBWEQG/+x3U1MDVV8Ptt8Nvf+sJTc0a09gAhvmSzomIMcU7JX0VWNhAnc2xnHQPaqM9sn11y+xZ\nT5mtG6obEa8V7f8F8EhDAdTW1n74vqamhpqamqbGbvahV15JD6qOGpWm7Pn2t9Mw7S2bPMeJWeUq\nFAoUCoWyHqOxAQxdgF+TRs/NyHZ/EtgO+EJE1E0cm3dwaQtgEWkQwqvANOCsiFhQVOZk4KJsAENf\n4IZsAEODdSXtFhErsvrfAQ6PiLPrOb4HMFizrVkDjz6alvh+9lk444w0MKFvXy9uZ9WtHAMYNvl7\nW5ZsPiXpOKB7tnt8RDxZioNHxHpJQ4CJfDQ8e4GkwenjuC0ixks6WdJS0tDu8zZVN/vq6yT1BDYA\nfwUGlyJeM4B581L32913wyGHpAR0332www55R2bWdjVpOqBq5ZaRbY7iaXu+9rU0bY8fVrX2qNVb\nRmaWpu0ZMSK9vv71NDx7Jy+gYlZSTkZmDag7bc/06bDffnlHZVadnIzM6jFz5kfT9tx1VxqqbWbl\n43UjzYr8z/+kAQmnnAL/+q9pLjknIrPyczIyy0ydmrrjdtwRFi70tD1mrcnddNbuRcDIkWkC09tu\ng9NOyzsis/bHycjatbfeSsO0ly6FP/zBQ7XN8uJuOmu35s6Fww+HnXeG3//eicgsT05G1i6NHg3H\nHQdXXgk//zlsu23eEZm1b+6ms3blvffg3/4NnnkGCgXo3r3RKmbWCtwysnZj6VI44gh45530AKsT\nkVnlcDKyduHXv4ZPfxq+8Q245540fNvMKoe76ayqrVkDQ4emBe7++7/TgAUzqzxORla1XnoJvvxl\n2GWXNJPCLrvkHZGZNcTddFaVJkxIraDPfx7GjXMiMqt0bhlZVVm/Hq6+Oi1+N3as55UzaytybxlJ\n6idpoaTFkoY2UOYmSUskzc5WcN1kXUk7S5ooaZGkCZI6tsa5WL6WLoWTToKnn/YEp2ZtTa7JSFIH\nYCRwEmlZ87MkdatTpj+wf0QcSFo+/NYm1B0GTIqIrsBk4PJWOB3LyZtvwne/C337wgknwBNPwG67\n5R2VmW2OvFtGfYAlEbEsItYCY4EBdcoMAMYARMRUoKOkzo3UHQCMzt6PBjz1ZRVavz5NbNq1K7z+\nOrzwQho5t6U7n83anLz/2XYBXirafpmUZBor06WRup0jYiVARKyQ1KmUQVv+CoW0+N1OO8H48dC7\nd94RmVlL5J2MmkPNqBMNfVBbW/vh+5qaGmp8o6Gi/fnPcNllMGsWXHcdnHEGqDk/EWbWZIVCgUKh\nUNZj5J2MlgN7FW3vke2rW2bPespsvYm6KyR1joiVknYD/tZQAMXJyCpTRJpVe9So9PDqJZekWRQ8\nualZ66j7i/oPfvCDkh8j73tG04EDJO0taWtgIDCuTplxwDkAkvoCq7IuuE3VHQcMyt6fCzxc1rOw\nsnjlFRg+HLp1gwsugIMOgnnz4IornIjMqk2uLaOIWC9pCDCRlBjviIgFkganj+O2iBgv6WRJS4F3\ngPM2VTf76uHA/ZLOB5YBZ7byqVkzrVkDjz4Kd94Jzz6buuHuuiuNlHN3nFn1UkSDt1OqnqRoz+df\nSVatSq2gO+6AQw6B88+HL34Rdtgh78jMrC5JRERJfz3Mu5vO2rl169Lidt26wWuvpXtDhQKcc44T\nkVl7kvcABmvHnnwSvvOdtOz3Y49Br155R2RmeXEysla3dGkanj1nDvzkJ3D66b4fZNbeuZvOWk3x\ntD19+8L8+em+kBORmbllZGW3ZEl6RujOO+GUU2DuXNh997yjMrNK4paRlcXq1SkBHX00HHUUfPAB\nTJ6cRss5EZlZXR7a3Y7Pv9Q2zpRw553w61/DMcfAeeel1tBWW+UdnZmVSjmGdrubzkri7rvTonYd\nOqRnhBYs8DIOZtZ0TkbWIu+/DxdfDE89lbrgjjzSAxLMbPP5npE125/+BEccAW+9Bc8/n+4NORGZ\nWXM4GVmz/OY3KRF97Wtw772w4455R2RmbZm76WyzrF0Lw4bBQw+lCU371F0K0cysGZyMrMmWL4cv\nfxk6doQZM+DjH887IjOrFu6msyaZNAk++Uk4+WR45BEnIjMrLbeMbJPWr4drrkkza99zD3zmM3lH\nZGbVyMnIGvTaa/DVr6bh2zNmeOYEMyuf3LrpJO0saaKkRZImSOrYQLl+khZKWixpaGP1s2XI35U0\nM3vd0lrnVE1+/3s47LC0rMOTTzoRmVl55XnPaBgwKSK6ApOBy+sWkNQBGAmcBHQHzpLUrQn1l0ZE\n7+x1YTlPotpEwM9+BqedBv/5n3DttbCl289mVmZ5/jczADg2ez8aKJASTLE+wJKIWAYgaWxWb2Ej\n9f3oZTO8+WaaymfZMpg6FfbdN++IzKy9yLNl1CkiVgJExAqgUz1lugAvFW2/nO0D6LyJ+vtkXXRT\nJB1V+tCrz+zZabRc587wzDNORGbWusraMpL0BNC5eBcQwJX1FG/p9Nkb678K7BURb0jqDfxW0iER\nsbq+SrW1tR++r6mpoaampoVhtC3vvQcjRsANN8CNN8LZZ+cdkZlVmkKhQKFQKOsxcltCQtICoCYi\nVkraDZgSEQfXKdMXqI2Iftn2MCAiYnhT6md1pgCXRsTMej5rt0tIRMD998PQoalFdN11sN9+eUdl\nZm1BOZaQyLObbhwwKHt/LvBwPWWmAwdkI+S2BgZm9RqsL2nXbOADkvYDDgD+XIb426wZM9JaQz/+\nMdx1Fzz4oBORmeUrz2Q0HDhB0iLgs8C1AJJ2l/QoQESsB4YAE4F5wNiIWLCp+sAxwBxJM4H7gcER\nsaqVzqmivfpqWuzu1FPh3HNTUmpnvZJmVqG80ms7OP/330/3ha6/Ps2y/f3vw0475R2VmbVVXunV\nNsvcuWkJ8LvvToveTZsG+++fd1RmZv/MyajKvPFGWl9o1KjULTdoUJpN4YAD8o7MzKxh7qargvPf\nsCFN2TNqFIwfDyedlB5ePf542GKLvKMzs2pTjm46J6M2fv6LFqUBCTvumBLQ2WfDLrvkHZWZVTPf\nM7J/MGcO9OsHP/pRGiVnZtZWORm1UdOmwec+BzffDGeemXc0ZmYt42TUBj39NJxxRhopd+qpeUdj\nZtZyTkZtzMSJ8JWvpBFzxx+fdzRmZqWR5wwMtpkefjitvPqb3zgRmVl1cTJqI+69FwYPTkO3j/Ki\nGGZWZZyM2oA77oDLLoNJk9IM22Zm1cb3jCrMmjUwfz7MmvXR68UXYcoUOOigvKMzMysPP/Sa8/kv\nXAhPPJFWWp01K23vsw/06vXR6/DDPbGpmVUOz8BQYnkmowi45RaorYXTT09Jp2dP6NEDtt8+l5DM\nzJrEMzBUibffhm98I3XH/f73cOCBeUdkZpYvD2BoZfPmpW63HXaA555zIjIzgxyTkaSdJU2UtEjS\nBEkdGyjXT9JCSYslDS3af4akFyStl9S7Tp3LJS2RtEDSieU+l6b65S/TyqrDhsHtt8N22+UdkZlZ\nZcizZTQMmBQRXYHJwOV1C0jqAIwETgK6A2dJ6pZ9PBf4AvBUnToHA2cCBwP9gVsklbRvc3O9/356\nRuiaa2Dy5LTGkJmZfSTPZDQAGJ29Hw2cVk+ZPsCSiFgWEWuBsVk9ImJRRCwB6iaaAcDYiFgXEX8F\nlmTfk4s//Qk+/em06N306XDooXlFYmZWufJMRp0iYiVARKwAOtVTpgvwUtH2y9m+TalbZ3kT6pTc\n2rVw441wxBGpJXTffR6ebWbWkLKOppP0BNC5eBcQwJX1FM9ljHVtbe2H72tqaqipqWnxd44fD5dc\nAnvtlR5W7d69xV9pZpabQqFAoVAo6zFye85I0gKgJiJWStoNmBIRB9cp0xeojYh+2fYwICJieFGZ\nKcClETGzvjKSHgeuioip9cRQ0ueMFixISejPf4YRI+DkkyHfu1VmZqVXjueM8uymGwcMyt6fCzxc\nT5npwAEUbYRxAAAJH0lEQVSS9pa0NTAwq1dX8UUZBwyUtLWkfYEDgGkli7oer78OF18MxxwDJ54I\nc+fCKac4EZmZNVWeyWg4cIKkRcBngWsBJO0u6VGAiFgPDAEmAvNIAxMWZOVOk/QS0Bd4VNJjWZ35\nwP3AfGA8cGG5pllYtw5GjoRu3dL7BQvgO9+Brbcux9HMzKqXpwNq5vlPmJASz7/8C/zsZx4lZ2bt\nh6cDqgCLFqX7QosXw/XXw+c+5+44M7OW8nRATfTGG6kldNRRcNxxaVqfz3/eicjMrBScjBqxbl2a\nXbtbN3jvvZSELr3U94XMzErJ3XSbMHlyGiXXqVNac6hHj7wjMjOrTk5G9Vi3Dv7939PEpjffDAMG\nuDvOzKycnIzqWLECBg6ErbaCGTNSq8jMzMrL94yKPPUUHHYYHHssPP64E5GZWWtxywjYsAGuuw5u\nuAHGjEmzKJiZWetp98no9dfh3HPhf/4nLfGw5555R2Rm1v60+266ww5LS38/9ZQTkZlZXtr9dEAP\nPhh88Yt5R2Jm1naUYzqgdp+M2vP5m5k1R7UtIWFmZgY4GZmZWQVwMjIzs9zllowk7SxpoqRFkiZI\n6thAuX6SFkpaLGlo0f4zJL0gab2k3kX795b0rqSZ2euW1jgfMzNrvjxbRsOASRHRFZgMXF63gKQO\nwEjgJKA7cJakbtnHc4EvAE/V891LI6J39rqwLNG3okKhkHcITeI4S8txlk5biBHaTpzlkGcyGgCM\nzt6PBk6rp0wfYElELIuItcDYrB4RsSgilgD1jeioqmlN28oPqOMsLcdZOm0hRmg7cZZDnsmoU0Ss\nBIiIFUB9M8F1AV4q2n4529eYfbIuuimSjmp5qGZmVk5lnQ5I0hNA5+JdQABX1lO8VA/8vALsFRFv\nZPeSfivpkIhYXaLvNzOzEsvtoVdJC4CaiFgpaTdgSkQcXKdMX6A2Ivpl28OAiIjhRWWmAJdGxMwG\njtPg55L8xKuZWTOU+qHXPCdKHQcMAoYD5wIP11NmOnCApL2BV4GBwFn1lPvwokjaFXg9IjZI2g84\nAPhzfQGU+mKamVnz5HnPaDhwgqRFwGeBawEk7S7pUYCIWA8MASYC84CxEbEgK3eapJeAvsCjkh7L\nvvcYYI6kmcD9wOCIWNWK52VmZpupXc9NZ2ZmlaEqZ2Ao1wO12WeXS1oiaYGkFi3DV4I4661figd/\nGzpmnTI3ZdditqSezY23JcoU51WSXi66fv1yiLNX0f47JK2UNKdO+Uq4nk2JsxKuZ89s3x6SJkua\nJ2mupIuLylfC9WxKnCW9ni2IcRtJUyXNymK8qqj85l/LiKi6F6kL8HvZ+6HAtfWU6QAsBfYGtgJm\nA92yz7oCB5Iexu1dVOdgYBbpXts+WX3lGGe99bOyc1oQV4PHLCrTH/jv7P2ngOeaG28FxnkVcEkJ\nfx6bHWe2fRTQs+7faSVdz0birJjrCewG9MzefwxYVKE/n5uKs2TXswR/59tnf24BPAf0ae61rMqW\nEeV7oHYA6b7Vuoj4K7Ak+55c4mykfksGZ2zqmMWxjwGIiKlAR0mdWxBvJcUJpX1wuiVxEhHPAG/U\n872VdD03FSdUyPWMiBURMTvbvxpYwEfPLlbM9WwkTijd9Wzp3/m7WZltSL+kR1GdzbqW1ZqMyvVA\nbd06y5tQp5xxdt5E/ZY8+NuUa9NQmebG2xzlihNgSNYlcXsJumuaE2dTfraa8vNTCXFCBV5PSfuQ\nWnLPZbsq8noWxTm1aHeprmeLYpTUQdIsYAXwRERMz8ps9rVss8lI0hOS5hS95mZ/fr6e4rmN0mjl\nODfWf5X04G9v4FLgHkkfa+F3N6Y5v6nl8ffSlDhvAfaLiJ6kf2QjyhtSyVTqaKSKu57Zv4cHgW9F\nxDsNFMv9etaJc+OD+xVzPSNiQ0T0AvYAPiXpkIaKNvZdeT5n1CIRcUJDn2U3UTvHRw/U/q2eYsuB\nvYq298j2bcpyYM/NqVPmOFfUVz8i1gBrsvczJf0JOAio98HgzTxmcZn6rsXWmxtvC5Qlzoh4rWj/\nL4BHcoxzU5ry85N7nJV2PSVtSfoP/pcRUfx8Y0Vdz4biLPH1LMnfeUS8pTTBQD9gPs24lm22ZdSI\njQ/UQhMeqJW0NemB2nH1lCv+TXocMFDS1pL2JT1QOy3HOOutL2lXpRnPUSMP/jagKddmHHBOdoy+\nwKqsWb7Z8bZAWeLM/vFsdDrwQo5xbiT+uVVXSdezwTgr8HreCcyPiBvrqTMoe18J17PeOEt8PZsd\nY/b/zMYRvNsBJwALi+oMyt437VqWYkRGpb2AXYBJpBEoE4H/le3fHXi0qFy/rMwSYFjR/tNIfaTv\nkbq8Hiv67HLS6JMFwIk5x9lQ/Y0/oDOB54GTmxHbPx0TGAx8o6jMyOxa/JF/HHW4WfG28BqWI84x\nwBzSyKLfku515RnnPaQ5Fz8AXgTOq9Dr2VCclXA9e2X7jgTWZ7HMyv6N9Kug69mUOEt6PZv7dw4c\nmsU1O4vn+0XlN/ta+qFXMzPLXbV205mZWRviZGRmZrlzMjIzs9w5GZmZWe6cjMzMLHdORmZmljsn\nI7NWkC0HcEKdfd+S9J8NlN9b0tzs/f+R1L814jTLi5ORWeu4Bzirzr6B2f6GbHwIsBdwcjmCMqsU\nTkZmreMh4ORsvjEk7Q3sHhHPSvpJNoHuHyWdWVwpK/8D4MxsFvYvSTomW9BspqQZknZo/dMxK602\nO1GqWVsSEW9ImkZaqOwRUqvofkmnAz0i4lBJnYDpkp4qqrdO0r8Dh0XExQCSxgEXRsQfJG0PvN/q\nJ2RWYm4ZmbWesaQkRPbnvaTVUe8FiIi/AQXg8Ea+51ngZ5L+Ddg5IjaUJVqzVuRkZNZ6HgY+K6kX\nsF1EzKqnTKPrLUXEcOACYDvgWUkHlTZMs9bnZGTWSiIt4lYgLQ1wb7b7d8CXsxUz/zdwNP+8LMnb\nwE4bNyTtFxHzIuI60hIA3codu1m5ORmZta57gR581DX3G9L0+38kTbn/3ay7rtgU4JCNAxiAb2cD\nHmaTFlF8rNWiNysTLyFhZma5c8vIzMxy52RkZma5czIyM7PcORmZmVnunIzMzCx3TkZmZpY7JyMz\nM8udk5GZmeXu/wMyCN7SNU3tggAAAABJRU5ErkJggg==\n", 48 | "text/plain": [ 49 | "" 50 | ] 51 | }, 52 | "metadata": {}, 53 | "output_type": "display_data" 54 | } 55 | ], 56 | "source": [ 57 | "dac = bnc2110('Dev1')\n", 58 | "\n", 59 | "# define some parameters\n", 60 | "v_min = -10E-3\n", 61 | "v_max = 30E-3\n", 62 | "v_step = 1E-3\n", 63 | "\n", 64 | "voltages = np.arange(v_min, v_max, v_step)\n", 65 | "readings = []\n", 66 | "\n", 67 | "for volt in voltages:\n", 68 | "\n", 69 | " # set the DAC output\n", 70 | " dac.setVoltageOutput('ao1', volt)\n", 71 | " \n", 72 | " # take a measurement\n", 73 | " data_pt = dac.readCurrentInput('ai1')\n", 74 | " \n", 75 | " readings.append(data_pt)\n", 76 | " \n", 77 | "# plot the results\n", 78 | "plt.plot(voltages, readings)\n", 79 | "plt.xlabel('Volts')\n", 80 | "plt.ylabel('Ohms')" 81 | ] 82 | } 83 | ], 84 | "metadata": { 85 | "kernelspec": { 86 | "display_name": "Python 3", 87 | "language": "python", 88 | "name": "python3" 89 | }, 90 | "language_info": { 91 | "codemirror_mode": { 92 | "name": "ipython", 93 | "version": 3 94 | }, 95 | "file_extension": ".py", 96 | "mimetype": "text/x-python", 97 | "name": "python", 98 | "nbconvert_exporter": "python", 99 | "pygments_lexer": "ipython3", 100 | "version": "3.4.4" 101 | } 102 | }, 103 | "nbformat": 4, 104 | "nbformat_minor": 0 105 | } 106 | -------------------------------------------------------------------------------- /example_nbs/Isd vs Vbias vs Vgate - bnc2110 sr830 keithley2400.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": { 7 | "collapsed": true 8 | }, 9 | "outputs": [], 10 | "source": [ 11 | "# required to make plots show up\n", 12 | "%matplotlib inline" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": null, 18 | "metadata": { 19 | "collapsed": true 20 | }, 21 | "outputs": [], 22 | "source": [ 23 | "import numpy as np\n", 24 | "import pandas as pd\n", 25 | "import matplotlib.pyplot as plt\n", 26 | "\n", 27 | "from labdrivers.ni import bnc2110\n", 28 | "from labdrivers.srs import sr830\n", 29 | "from labdrivers.keithley import keithley2400" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": null, 35 | "metadata": { 36 | "collapsed": true 37 | }, 38 | "outputs": [], 39 | "source": [ 40 | "# define some parameters\n", 41 | "vgate_min = 20\n", 42 | "vgate_max = 50\n", 43 | "vgate_step = 0.5\n", 44 | "sleep_time = 0.05\n", 45 | "\n", 46 | "vbias_min = -10E-3\n", 47 | "vbias_max = 10E-3\n", 48 | "vbias_step = 0.5E-3\n", 49 | "\n", 50 | "gate_lines = np.arange(vgate_min, vgate_max, vgate_step)\n", 51 | "bias_lines = np.arange(vbias_min, vbias_max, vbias_step)\n", 52 | "\n", 53 | "# make data frames w vbias as columns, vg as index\n", 54 | "lockin_x_2d = pd.DataFrame(columns=bias_lines, index=gate_lines)\n", 55 | "lockin_y_2d = pd.DataFrame(columns=bias_lines, index=gate_lines)\n", 56 | "\n", 57 | "# for temporarily storing each 1d gate sweep\n", 58 | "lockin_x = []\n", 59 | "lockin_y = []\n", 60 | "\n", 61 | "for vbias in bias_lines:\n", 62 | " \n", 63 | " # connect to the instruments\n", 64 | " dac = bnc2110('Dev1')\n", 65 | " lockin = sr830(8)\n", 66 | " gate_keithley = keithley2400(20)\n", 67 | "\n", 68 | " # configure the gate keithley\n", 69 | " gate_keithley.setMeasure('current')\n", 70 | " gate_keithley.setSourceDC('voltage')\n", 71 | " gate_keithley.setCompliance('current', 0.5E-6)\n", 72 | "\n", 73 | " # turn the outputs on\n", 74 | " gate_keithley.rampOutputOn(vgate_min, 0.5)\n", 75 | " time.sleep(3)\n", 76 | "\n", 77 | " # set s-d bias to vbias\n", 78 | " dac.setVoltageOutput('ao1', vbias)\n", 79 | " \n", 80 | " lockin_tuples = []\n", 81 | "\n", 82 | " # do the gate sweep\n", 83 | " for volts in gate_lines:\n", 84 | "\n", 85 | " # set the keithley output, then take a measurement\n", 86 | " gate_keithley.setSourceDC('voltage', volts)\n", 87 | " lockin_tuples.append(lockin.getSnapshot(1,2))\n", 88 | " \n", 89 | " # sleep so that the gate doesn't sweep too fast\n", 90 | " time.sleep(sleep_time)\n", 91 | "\n", 92 | " # unpack [(x1, y1), (x2, y2), (x3, y3), ...] \n", 93 | " # to [x1, x2, x3, ...] and [y1, y2, y3, ...]\n", 94 | " lockin_x, lockin_y = zip(*lockin_tuples)\n", 95 | " \n", 96 | " lockin_x_2d[vbias] = lockin_x\n", 97 | " lockin_y_2d[vbias] = lockin_y\n", 98 | "\n", 99 | " # to keep track of where we are in the scan\n", 100 | " print('V_bias: {:.3e}'.format(vbias))\n", 101 | " \n", 102 | " # turn the keithley output off\n", 103 | " gate_keithley.rampOutputOff(vgate_max, 0.5)\n", 104 | "\n", 105 | "# save the data to file\n", 106 | "lockin_x_2d.to_csv('path/to/save x', index=True)\n", 107 | "lockin_y_2d.to_csv('path/to/save y', index=True)" 108 | ] 109 | } 110 | ], 111 | "metadata": { 112 | "kernelspec": { 113 | "display_name": "Python 3", 114 | "language": "python", 115 | "name": "python3" 116 | }, 117 | "language_info": { 118 | "codemirror_mode": { 119 | "name": "ipython", 120 | "version": 3 121 | }, 122 | "file_extension": ".py", 123 | "mimetype": "text/x-python", 124 | "name": "python", 125 | "nbconvert_exporter": "python", 126 | "pygments_lexer": "ipython3", 127 | "version": "3.4.4" 128 | } 129 | }, 130 | "nbformat": 4, 131 | "nbformat_minor": 0 132 | } 133 | -------------------------------------------------------------------------------- /example_nbs/example_2d_gate_bias_conductance.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Importing drivers" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 1, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "from labdrivers.ni import bnc2110\n", 17 | "from labdrivers.keithley import keithley2400\n", 18 | "from labdrivers.srs import sr830" 19 | ] 20 | }, 21 | { 22 | "cell_type": "markdown", 23 | "metadata": {}, 24 | "source": [ 25 | "# Object instantiation" 26 | ] 27 | }, 28 | { 29 | "cell_type": "code", 30 | "execution_count": 9, 31 | "metadata": {}, 32 | "outputs": [], 33 | "source": [ 34 | "daq = bnc2110(device='Dev1')\n", 35 | "keithley = keithley2400(GPIBaddr=22)\n", 36 | "lockin = sr830(GPIBaddr=8)" 37 | ] 38 | }, 39 | { 40 | "cell_type": "markdown", 41 | "metadata": {}, 42 | "source": [ 43 | "# Importing other useful libraries" 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "execution_count": 4, 49 | "metadata": {}, 50 | "outputs": [], 51 | "source": [ 52 | "import pandas as pd\n", 53 | "import numpy as np\n", 54 | "import matplotlib.pyplot as plt\n", 55 | "import seaborn as sns\n", 56 | "%matplotlib inline" 57 | ] 58 | }, 59 | { 60 | "cell_type": "markdown", 61 | "metadata": {}, 62 | "source": [ 63 | "# Setting up important constants" 64 | ] 65 | }, 66 | { 67 | "cell_type": "code", 68 | "execution_count": 7, 69 | "metadata": {}, 70 | "outputs": [], 71 | "source": [ 72 | "quantum_conductance = 0.000077480917310 # in Siemens\n", 73 | "current_preamp_sensitivity = 10 ** -5\n", 74 | "ac_amplitude = 0.100" 75 | ] 76 | }, 77 | { 78 | "cell_type": "markdown", 79 | "metadata": {}, 80 | "source": [ 81 | "# Setting up the SR830 lock-in amplifier" 82 | ] 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": 8, 87 | "metadata": {}, 88 | "outputs": [], 89 | "source": [ 90 | "lockin.setFrequency(freq=137.5)\n", 91 | "lockin.setInput(i=0)\n", 92 | "lockin.setAmplitude(level=ac_amplitude)\n", 93 | "lockin.setTimeConst(const_index=9)\n", 94 | "lockin.setSensitivity(sens_index=25)" 95 | ] 96 | }, 97 | { 98 | "cell_type": "markdown", 99 | "metadata": {}, 100 | "source": [ 101 | "# Making a 2D sweep\n", 102 | "\n", 103 | "As a very simple example, this is just a sweep on a 20 kOhm resistor at room temperature." 104 | ] 105 | }, 106 | { 107 | "cell_type": "code", 108 | "execution_count": null, 109 | "metadata": {}, 110 | "outputs": [], 111 | "source": [ 112 | "columns = ['gate_voltage','bias_voltage','dConductance']\n", 113 | "data = pd.DataFrame(columns=columns)\n", 114 | "\n", 115 | "for gate_voltage in range(5,1025,25):\n", 116 | " \n", 117 | " gate = gate_voltage / 100\n", 118 | " keithley.setSourceDC(source='voltage', value=gate)\n", 119 | " \n", 120 | " for bias_voltage in range(-10,11,1):\n", 121 | " \n", 122 | " bias = bias_voltage / 10\n", 123 | " daq.setVoltageOutput(channel='ao1', output=bias)\n", 124 | " \n", 125 | " dConductance = lockin.getSinglePoint(parameter=1)[0] * current_preamp_sensitivity / \\\n", 126 | " ac_amplitude\n", 127 | " \n", 128 | " new_record = pd.DataFrame(np.array([[gate,bias,dConductance]]),\n", 129 | " columns=columns)\n", 130 | " data = data.append(new_record)\n", 131 | " \n", 132 | " " 133 | ] 134 | }, 135 | { 136 | "cell_type": "code", 137 | "execution_count": 22, 138 | "metadata": { 139 | "collapsed": true 140 | }, 141 | "outputs": [], 142 | "source": [ 143 | "data_pivoted = data.pivot(index='gate_voltage',\n", 144 | " columns='bias_voltage',\n", 145 | " values='dConductance')" 146 | ] 147 | }, 148 | { 149 | "cell_type": "code", 150 | "execution_count": 23, 151 | "metadata": {}, 152 | "outputs": [ 153 | { 154 | "data": { 155 | "text/html": [ 156 | "
\n", 157 | "\n", 170 | "\n", 171 | " \n", 172 | " \n", 173 | " \n", 174 | " \n", 175 | " \n", 176 | " \n", 177 | " \n", 178 | " \n", 179 | " \n", 180 | " \n", 181 | " \n", 182 | " \n", 183 | " \n", 184 | " \n", 185 | " \n", 186 | " \n", 187 | " \n", 188 | " \n", 189 | " \n", 190 | " \n", 191 | " \n", 192 | " \n", 193 | " \n", 194 | " \n", 195 | " \n", 196 | " \n", 197 | " \n", 198 | " \n", 199 | " \n", 200 | " \n", 201 | " \n", 202 | " \n", 203 | " \n", 204 | " \n", 205 | " \n", 206 | " \n", 207 | " \n", 208 | " \n", 209 | " \n", 210 | " \n", 211 | " \n", 212 | " \n", 213 | " \n", 214 | " \n", 215 | " \n", 216 | " \n", 217 | " \n", 218 | " \n", 219 | " \n", 220 | " \n", 221 | " \n", 222 | " \n", 223 | " \n", 224 | " \n", 225 | " \n", 226 | " \n", 227 | " \n", 228 | " \n", 229 | " \n", 230 | " \n", 231 | " \n", 232 | " \n", 233 | " \n", 234 | " \n", 235 | " \n", 236 | " \n", 237 | " \n", 238 | " \n", 239 | " \n", 240 | " \n", 241 | " \n", 242 | " \n", 243 | " \n", 244 | " \n", 245 | " \n", 246 | " \n", 247 | " \n", 248 | " \n", 249 | " \n", 250 | " \n", 251 | " \n", 252 | " \n", 253 | " \n", 254 | " \n", 255 | " \n", 256 | " \n", 257 | " \n", 258 | " \n", 259 | " \n", 260 | " \n", 261 | " \n", 262 | " \n", 263 | " \n", 264 | " \n", 265 | " \n", 266 | " \n", 267 | " \n", 268 | " \n", 269 | " \n", 270 | " \n", 271 | " \n", 272 | " \n", 273 | " \n", 274 | " \n", 275 | " \n", 276 | " \n", 277 | " \n", 278 | " \n", 279 | " \n", 280 | " \n", 281 | " \n", 282 | " \n", 283 | " \n", 284 | " \n", 285 | " \n", 286 | " \n", 287 | " \n", 288 | " \n", 289 | " \n", 290 | " \n", 291 | " \n", 292 | " \n", 293 | " \n", 294 | " \n", 295 | " \n", 296 | " \n", 297 | " \n", 298 | " \n", 299 | " \n", 300 | " \n", 301 | " \n", 302 | " \n", 303 | " \n", 304 | " \n", 305 | " \n", 306 | " \n", 307 | " \n", 308 | " \n", 309 | " \n", 310 | " \n", 311 | " \n", 312 | " \n", 313 | " \n", 314 | " \n", 315 | " \n", 316 | " \n", 317 | " \n", 318 | " \n", 319 | " \n", 320 | " \n", 321 | " \n", 322 | " \n", 323 | " \n", 324 | " \n", 325 | " \n", 326 | " \n", 327 | " \n", 328 | " \n", 329 | " \n", 330 | " \n", 331 | " \n", 332 | " \n", 333 | " \n", 334 | " \n", 335 | " \n", 336 | " \n", 337 | " \n", 338 | " \n", 339 | " \n", 340 | " \n", 341 | " \n", 342 | " \n", 343 | "
bias_voltage-1.0-0.9-0.8-0.7-0.6-0.5-0.4-0.3-0.2-0.1...0.10.20.30.40.50.60.70.80.91.0
gate_voltage
0.000.0000360.0000360.0000360.0000360.0000360.0000360.0000360.0000360.0000360.000036...0.0000360.0000360.0000360.0000360.0000360.0000360.0000360.0000360.0000360.000036
0.250.0000360.0000360.0000360.0000360.0000360.0000360.0000360.0000360.0000360.000036...0.0000360.0000360.0000360.0000360.0000360.0000360.0000360.0000360.0000360.000036
0.500.0000360.0000360.0000360.0000360.0000360.0000360.0000360.0000360.0000360.000036...0.0000360.0000360.0000360.0000360.0000360.0000360.0000360.0000360.0000360.000036
0.750.0000360.0000360.0000360.0000360.0000360.0000360.0000360.0000360.0000360.000036...0.0000360.0000360.0000360.0000360.0000360.0000360.0000360.0000360.0000360.000036
1.000.0000360.0000360.0000360.0000360.0000360.0000360.0000360.0000360.0000360.000036...0.0000360.0000360.0000360.0000360.0000360.0000360.0000360.0000360.0000360.000036
\n", 344 | "

5 rows × 21 columns

\n", 345 | "
" 346 | ], 347 | "text/plain": [ 348 | "bias_voltage -1.0 -0.9 -0.8 -0.7 -0.6 -0.5 \\\n", 349 | "gate_voltage \n", 350 | "0.00 0.000036 0.000036 0.000036 0.000036 0.000036 0.000036 \n", 351 | "0.25 0.000036 0.000036 0.000036 0.000036 0.000036 0.000036 \n", 352 | "0.50 0.000036 0.000036 0.000036 0.000036 0.000036 0.000036 \n", 353 | "0.75 0.000036 0.000036 0.000036 0.000036 0.000036 0.000036 \n", 354 | "1.00 0.000036 0.000036 0.000036 0.000036 0.000036 0.000036 \n", 355 | "\n", 356 | "bias_voltage -0.4 -0.3 -0.2 -0.1 ... 0.1 \\\n", 357 | "gate_voltage ... \n", 358 | "0.00 0.000036 0.000036 0.000036 0.000036 ... 0.000036 \n", 359 | "0.25 0.000036 0.000036 0.000036 0.000036 ... 0.000036 \n", 360 | "0.50 0.000036 0.000036 0.000036 0.000036 ... 0.000036 \n", 361 | "0.75 0.000036 0.000036 0.000036 0.000036 ... 0.000036 \n", 362 | "1.00 0.000036 0.000036 0.000036 0.000036 ... 0.000036 \n", 363 | "\n", 364 | "bias_voltage 0.2 0.3 0.4 0.5 0.6 0.7 \\\n", 365 | "gate_voltage \n", 366 | "0.00 0.000036 0.000036 0.000036 0.000036 0.000036 0.000036 \n", 367 | "0.25 0.000036 0.000036 0.000036 0.000036 0.000036 0.000036 \n", 368 | "0.50 0.000036 0.000036 0.000036 0.000036 0.000036 0.000036 \n", 369 | "0.75 0.000036 0.000036 0.000036 0.000036 0.000036 0.000036 \n", 370 | "1.00 0.000036 0.000036 0.000036 0.000036 0.000036 0.000036 \n", 371 | "\n", 372 | "bias_voltage 0.8 0.9 1.0 \n", 373 | "gate_voltage \n", 374 | "0.00 0.000036 0.000036 0.000036 \n", 375 | "0.25 0.000036 0.000036 0.000036 \n", 376 | "0.50 0.000036 0.000036 0.000036 \n", 377 | "0.75 0.000036 0.000036 0.000036 \n", 378 | "1.00 0.000036 0.000036 0.000036 \n", 379 | "\n", 380 | "[5 rows x 21 columns]" 381 | ] 382 | }, 383 | "execution_count": 23, 384 | "metadata": {}, 385 | "output_type": "execute_result" 386 | } 387 | ], 388 | "source": [ 389 | "data_pivoted.head()" 390 | ] 391 | }, 392 | { 393 | "cell_type": "markdown", 394 | "metadata": {}, 395 | "source": [ 396 | "The number here is a bit off from what you are supposed to expect for a 20 kOhm resistor, but otherwise this does exactly what we expect in terms of outputting data." 397 | ] 398 | } 399 | ], 400 | "metadata": { 401 | "kernelspec": { 402 | "display_name": "Python 3", 403 | "language": "python", 404 | "name": "python3" 405 | }, 406 | "language_info": { 407 | "codemirror_mode": { 408 | "name": "ipython", 409 | "version": 3 410 | }, 411 | "file_extension": ".py", 412 | "mimetype": "text/x-python", 413 | "name": "python", 414 | "nbconvert_exporter": "python", 415 | "pygments_lexer": "ipython3", 416 | "version": "3.6.4" 417 | } 418 | }, 419 | "nbformat": 4, 420 | "nbformat_minor": 2 421 | } 422 | -------------------------------------------------------------------------------- /example_nbs/example_realtimeplot.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | 4 | fig = plt.figure() 5 | ax1 = fig.add_subplot(111) 6 | 7 | # set up the independent variable 8 | x = np.array([]) 9 | 10 | for i in np.linspace(0, 5*np.pi, 100): 11 | # update data 12 | x = np.append(x, i) 13 | y = np.sin(x) 14 | 15 | # clear the plot of lines already plotted 16 | ax1.clear() 17 | 18 | # update the plot 19 | plt.plot(x,y) 20 | plt.draw() 21 | 22 | # no need to pause, but helps so you can see real-time plots 23 | plt.pause(0.1) 24 | -------------------------------------------------------------------------------- /labdrivers/__init__.py: -------------------------------------------------------------------------------- 1 | from .labdrivers import * # noqa 2 | from .version import __version__ # noqa 3 | 4 | name = "labdrivers" 5 | -------------------------------------------------------------------------------- /labdrivers/keithley/__init__.py: -------------------------------------------------------------------------------- 1 | from .keithley2400 import Keithley2400 2 | -------------------------------------------------------------------------------- /labdrivers/keithley/keithley2400.py: -------------------------------------------------------------------------------- 1 | from statistics import mean, stdev 2 | 3 | import visa 4 | 5 | 6 | class Keithley2400: 7 | 8 | def __init__(self, gpib_addr=23): 9 | """ 10 | Constructor for Keithley 2400 Sourcemeter 11 | 12 | :param gpib_addr: GPIB address (configured on Keithley 2400) 13 | """ 14 | self._gpib_addr = str(gpib_addr) 15 | self._resource_manager = visa.ResourceManager() 16 | self._instrument = self._resource_manager.open_resource("GPIB::{}".format(self.gpib_addr)) 17 | 18 | @property 19 | def gpib_addr(self): 20 | """Returns the GPIB address of the Keithley 2400 Sourcemeter.""" 21 | return self._gpib_addr 22 | 23 | # source functions 24 | 25 | @property 26 | def source_type(self): 27 | """Gets or sets the source type of the Keithley 2400 SourceMeter. 28 | 29 | Expected strings for setting: 'voltage', 'current'""" 30 | response = self._instrument.query("source:function:mode?").strip() 31 | source_type = {'VOLT': 'voltage', 'CURR': 'current'} 32 | return source_type[response] 33 | 34 | @source_type.setter 35 | def source_type(self, value): 36 | if value.lower() == 'voltage' or value.lower() == 'v': 37 | source = 'voltage' 38 | self._instrument.write("source:function:mode {}".format(source.lower())) 39 | elif value.lower() == 'current' or value.lower() == 'i': 40 | source = 'current' 41 | self._instrument.write('source:function:mode {}'.format(source.lower())) 42 | else: 43 | raise RuntimeError('Not a valid source type.') 44 | 45 | @property 46 | def source_mode(self): 47 | """Gets or sets the mode of the source. 48 | 49 | Expected strings for setting: 'fixed', 'sweep', 'list'""" 50 | # TODO: test 51 | return self._instrument.query('source:' + self.source_type.lower() + ':mode?') 52 | 53 | @source_mode.setter 54 | def source_mode(self, mode): 55 | if mode.lower() in ('fixed', 'sweep', 'list'): 56 | self._instrument.write('source:' + self.source_type.lower() + ':mode {}'.format(mode)) 57 | else: 58 | raise RuntimeError('Mode is not one of [fixed | sweep | list]') 59 | 60 | @property 61 | def source_value(self): 62 | """Get or set the numeric value of the source chosen from Keithley2400.source_type.""" 63 | # TODO: test 64 | return self._instrument.query('source:' + self.source_type.lower() + ':level?') 65 | 66 | @source_value.setter 67 | def source_value(self, value): 68 | self._instrument.write("source:function:mode " + self.source_type.lower()) 69 | self._instrument.write("source:" + self.source_type.lower() + ":range " + str(value)) 70 | self._instrument.write("source:" + self.source_type.lower() + ":level " + str(value)) 71 | 72 | @property 73 | def measure_type(self): 74 | """The type of measurement the Keithley 2400 SourceMeter will make. 75 | 76 | Expected strings for setting: 'voltage', 'current', 'resistance' 77 | """ 78 | measure_type = {'VOLT:DC': 'voltage', 'CURR:DC': 'current', 'RES': 'resistance'} 79 | measure_type_response = self._instrument.query("sense:function?").strip().replace('\"', '').split(',')[-1] 80 | return measure_type[measure_type_response] 81 | 82 | @measure_type.setter 83 | def measure_type(self, value): 84 | measure_type = {'voltage': '\'VOLTAGE:DC\'', 'current': '\'CURRENT:DC\'', 'resistance': 'RESISTANCE'} 85 | if value.lower() in measure_type: 86 | self._instrument.write("sense:function:ON {}".format(measure_type[value.lower()])) 87 | else: 88 | raise RuntimeError('Expected a value from [\'voltage\'|\'current\'|\'resistance\'') 89 | 90 | # Resistance sensing 91 | 92 | @property 93 | def resistance_ohms_mode(self): 94 | """Gets or sets the resistance mode. 95 | 96 | Expected strings for setting: 'manual', 'auto'""" 97 | modes = {'MAN': 'manual', 'AUTO': 'auto'} 98 | response = self._instrument.query('sense:resistance:mode?').strip() 99 | return modes[response] 100 | 101 | @resistance_ohms_mode.setter 102 | def resistance_ohms_mode(self, value): 103 | modes = {'manual': 'MAN', 'auto': 'AUTO'} 104 | if value.lower() in modes.keys(): 105 | self._instrument.write('sense:resistance:mode {}'.format(modes[value.lower()])) 106 | else: 107 | raise RuntimeError('Expected a value from [\'manual\'|\'auto\']') 108 | 109 | @property 110 | def expected_ohms_reading(self): 111 | """Gets or sets the expected range of a resistance reading from the device under test.""" 112 | response = self._instrument.query('sense:resistance:range?').strip() 113 | return float(response) 114 | 115 | @expected_ohms_reading.setter 116 | def expected_ohms_reading(self, value): 117 | if isinstance(value, int) or isinstance(value, float): 118 | self._instrument.write('sense:resistance:range {}'.format(value)) 119 | else: 120 | raise RuntimeError('Expected an int or float.') 121 | 122 | @property 123 | def four_wire_sensing(self): 124 | """Gets the status of or sets four-wire sensing. 125 | 126 | Expected booleans for setting: True, False.""" 127 | response = self._instrument.query('system:rsense?').strip() 128 | return bool(int(response)) 129 | 130 | @four_wire_sensing.setter 131 | def four_wire_sensing(self, value): 132 | if isinstance(value, bool): 133 | self._instrument.write('system:rsense {}'.format(int(value))) 134 | else: 135 | raise RuntimeError('Expected boolean value.') 136 | 137 | # Voltage sensing and compliance 138 | 139 | @property 140 | def expected_voltage_reading(self): 141 | """Gets or sets the expected voltage reading from the device under test.""" 142 | response = self._instrument.query('sense:voltage:RANGE?').strip() 143 | return float(response) 144 | 145 | @expected_voltage_reading.setter 146 | def expected_voltage_reading(self, value): 147 | if isinstance(value, int) or isinstance(value, float): 148 | self._instrument.write('sense:voltage:range {}'.format(value)) 149 | else: 150 | raise RuntimeError('Expected an int or float.') 151 | 152 | @property 153 | def voltage_compliance(self): 154 | """Gets or sets the voltage compliance. 155 | 156 | Expected range of floats: 200e-6 <= x <= 210""" 157 | response = self._instrument.query("SENS:VOLT:PROT:LEV?").strip() 158 | return float(response) 159 | 160 | @voltage_compliance.setter 161 | def voltage_compliance(self, value): 162 | if 200e-6 <= value <= 210: 163 | self._instrument.write("SENS:VOLT:PROT {}".format(str(value))) 164 | else: 165 | raise RuntimeError('Voltage compliance cannot be set. Value must be between 200 \u03BC' + 'V and 210 V.') 166 | 167 | def within_voltage_compliance(self): 168 | """Queries if the measured voltage is within the set compliance. 169 | 170 | :returns: boolean""" 171 | response = self._instrument.query('SENS:VOLT:PROT:TRIP?').strip() 172 | return not bool(int(response)) 173 | 174 | # Current sensing and compilance 175 | 176 | @property 177 | def expected_current_reading(self): 178 | """Gets or sets the expected current reading from the device under test.""" 179 | response = self._instrument.query('sense:current:range?').strip() 180 | return float(response) 181 | 182 | @expected_current_reading.setter 183 | def expected_current_reading(self, value): 184 | if isinstance(value, int) or isinstance(value, float): 185 | self._instrument.write('sense:current:range {}'.format(value)) 186 | else: 187 | RuntimeError('Expected an int or float.') 188 | 189 | @property 190 | def current_compliance(self): 191 | """Sets or gets the current compliance level in Amperes.""" 192 | response = self._instrument.query("SENS:CURR:PROT:LEV?").strip() 193 | return float(response) 194 | 195 | @current_compliance.setter 196 | def current_compliance(self, value): 197 | if 1e-9 <= value <= 1.05: 198 | self._instrument.write("SENS:CURR:PROT {}".format(str(value))) 199 | else: 200 | raise RuntimeError('Current compliance cannot be set. Value must be between 1 nA and 1.05 A.') 201 | 202 | def within_current_compliance(self): 203 | """Queries if the measured current is within the set compliance. 204 | 205 | :returns: boolean""" 206 | response = self._instrument.query('SENS:CURR:PROT:TRIP?').strip() 207 | return not bool(int(response)) 208 | 209 | # Output configuration 210 | 211 | @property 212 | def output(self): 213 | """Gets or sets the source output of the Keithley 2400. 214 | 215 | Expected input: boolean 216 | 217 | :returns: boolean""" 218 | output = {'0': False, '1': True} 219 | response = self._instrument.query("OUTP?").strip() 220 | return output[response] 221 | 222 | @output.setter 223 | def output(self, value): 224 | if value: 225 | self._instrument.write("OUTP ON") 226 | else: 227 | self._instrument.write("OUTP OFF") 228 | 229 | @property 230 | def output_off_mode(self): 231 | """Gets or sets the output mode when the output is off. 232 | 233 | Expected input strings: 'himp', 'normal', 'zero', 'guard' 234 | 235 | :returns: description of the output's off mode""" 236 | modes = {'HIMP': 'high impedance', 'NORM': 'normal', 'ZERO': 'zero', 'GUAR': 'guard'} 237 | response = self._instrument.query('OUTP:SMOD?').strip() 238 | return modes[response] 239 | 240 | @output_off_mode.setter 241 | def output_off_mode(self, value): 242 | modes = {'high impedance': 'HIMP', 'himp': 'HIMP', 'normal': 'NORM', 'norm': 'NORM', 243 | 'zero': 'ZERO', '0': 'ZERO', 'guard': 'GUARD'} 244 | self._instrument.write('OUTP:SMOD {}'.format(modes[value.lower()])) 245 | 246 | # Data acquisition 247 | 248 | def read(self, *measurements): 249 | """ 250 | 251 | Reads data from the Keithley 2400. Equivalent to the command :INIT; :FETCH? 252 | 253 | Multiple string arguments may be used. For example:: 254 | 255 | keithley.read('voltage', 'current') 256 | keithley.read('time') 257 | 258 | The first line returns a list in the form [voltage, current] and the second line 259 | returns a list in the form [time]. 260 | 261 | Note: The returned lists contains the values in the order that you requested. 262 | 263 | :param str *measurements: Any number of arguments that are from: 'voltage', 'current', 'resistance', 'time' 264 | :return list measure_list: A list of the arithmetic means in the order of the given arguments 265 | :return list measure_stdev_list: A list of the standard deviations (if more than 1 measurement) in the order 266 | of the given arguments 267 | """ 268 | response = self._instrument.query('read?').strip().split(',') 269 | response = [float(x) for x in response] 270 | read_types = {'voltage': 0, 'current': 1, 'resistance': 2, 'time': 3} 271 | 272 | measure_list = [] 273 | measure_stdev_list = [] 274 | 275 | for measurement in measurements: 276 | samples = response[read_types[measurement]::5] 277 | measure_list.append(mean(samples)) 278 | if len(samples) > 1: 279 | measure_stdev_list.append(stdev(samples)) 280 | 281 | return measure_list, measure_stdev_list 282 | 283 | # Trigger functions 284 | 285 | @property 286 | def trace_delay(self): 287 | """The amount of time the SourceMeter waits after the trigger to perform Device Action.""" 288 | return float(self._instrument.query('trigger:delay?').strip()) 289 | 290 | @trace_delay.setter 291 | def trace_delay(self, delay): 292 | if isinstance(delay, float) or isinstance(delay, int): 293 | if 0.0 <= delay <= 999.9999: 294 | self._instrument.write('trigger:delay {}'.format(delay)) 295 | else: 296 | raise RuntimeError('Expected delay to be between 0.0 and 999.9999 seconds.') 297 | else: 298 | raise RuntimeError('Expected delay to be an int or float.') 299 | 300 | @property 301 | def trigger(self): 302 | """Gets or sets the type of trigger to be used. 303 | 304 | Expected strings for setting: 'immediate', 'tlink', 'timer', 'manual', 'bus', 305 | 'nst', 'pst', 'bst' (see source code for other possibilities)""" 306 | triggers = {'IMM': 'immediate', 'TLIN': 'trigger link', 'TIM': 'timer', 307 | 'MAN': 'manual', 'BUS': 'bus trigger', 'NST': 'low SOT pulse', 308 | 'PST': 'high SOT pulse', 'BST': 'high or low SOT pulse'} 309 | return triggers[self._instrument.query('trigger:source?')] 310 | 311 | @trigger.setter 312 | def trigger(self, trigger): 313 | triggers = { 314 | 'imm': 'IMM', 'immediate': 'IMM', 315 | 'tlin': 'TLIN', 'tlink': 'TLIN', 'trigger link': 'TLIN', 316 | 'tim': 'TIM', 'timer': 'TIM', 317 | 'man': 'MAN', 'manual': 'MAN', 318 | 'bus': 'BUS', 'bus trigger': 'BUS', 319 | 'nst': 'NST', 'low SOT pulse': 'NST', 320 | 'pst': 'PST', 'high SOT pulse': 'PST', 321 | 'bst': 'BST', 'high or low SOT pulse': 'BST' 322 | } 323 | if trigger.lower() in triggers.keys(): 324 | self._instrument.query('trigger:source {}'.format(trigger)) 325 | else: 326 | raise RuntimeError('Unexpected trigger input. See documentation for details.') 327 | 328 | @property 329 | def trigger_count(self): 330 | """Gets or sets the number of triggers 331 | 332 | Expected integer value range: 1 <= n <= 2500""" 333 | return float(self._instrument.query('trigger:count?').strip()) 334 | 335 | @trigger_count.setter 336 | def trigger_count(self, num_triggers): 337 | if isinstance(num_triggers, int): 338 | if 1 <= num_triggers <= 2500: 339 | self._instrument.write('trigger:count {}'.format(num_triggers)) 340 | else: 341 | raise RuntimeError('Trigger count expected to be between 1 and 2500.') 342 | else: 343 | raise RuntimeError('Trigger count expected to be type int.') 344 | 345 | def initiate_cycle(self): 346 | """Initiates source or measure cycle, taking the SourceMeter out of an idle state.""" 347 | self._instrument.write('initiate') 348 | 349 | def abort_cycle(self): 350 | """Aborts the source or measure cycle, bringing the SourceMeter back into an idle state.""" 351 | self._instrument.write('abort') 352 | 353 | # Data storage / Buffer functions 354 | 355 | # Note: :trace:data? and :read? are two separate buffers of 356 | # maximum size 2500 readings. 357 | 358 | @property 359 | def num_readings_in_buffer(self): 360 | """Gets the number of readings that are stored in the buffer.""" 361 | return int(self._instrument.query('trace:points:actual?').strip()) 362 | 363 | @property 364 | def trace_points(self): 365 | """Gets or sets the size of the buffer 366 | 367 | Expected integer value range: 1 <= n <= 2500""" 368 | return int(self._instrument.query('trace:points?').strip()) 369 | 370 | @trace_points.setter 371 | def trace_points(self, num_points): 372 | if isinstance(num_points, int): 373 | if 1 <= num_points <= 2500: 374 | self._instrument.write('trace:points {}'.format(num_points)) 375 | else: 376 | raise RuntimeError('Keithley 2400 SourceMeter may only have 1 to 2500 buffer points.') 377 | else: 378 | raise RuntimeError('Expected type of num_points: int.') 379 | 380 | def trace_feed_source(self, value): 381 | """Sets the source of the trace feed. 382 | 383 | Expected strings: 'sense', 'calculate1', 'calculate2'""" 384 | if value in ('sense', 'calculate1', 'calculate2'): 385 | self._instrument.write('trace:feed {}'.format(value)) 386 | else: 387 | raise RuntimeError('Unexpected trace source type. See documentation for details.') 388 | 389 | def read_trace(self): 390 | """Read contents of buffer.""" 391 | trace = self._instrument.query('trace:data?').strip().split(',') 392 | trace_list = [float(x) for x in trace] 393 | return trace_list 394 | 395 | def clear_trace(self): 396 | """Clear the buffer.""" 397 | self._instrument.query('trace:clear') 398 | 399 | def buffer_memory_status(self): 400 | """Check buffer memory status.""" 401 | response = self._instrument.query('trace:free?') 402 | return response 403 | 404 | def fill_buffer(self): 405 | """Fill buffer and stop.""" 406 | self._instrument.write('trace:feed:control next') 407 | 408 | def disable_buffer(self): 409 | """Disables the buffer.""" 410 | self._instrument.write('trace:feed:control never') 411 | 412 | # Sweeping 413 | 414 | # TODO: implement these!!! 415 | 416 | @property 417 | def sweep_start(self): 418 | """To be implemented.""" 419 | pass 420 | 421 | @sweep_start.setter 422 | def sweep_start(self, start): 423 | pass 424 | 425 | @property 426 | def sweep_end(self): 427 | """To be implemented.""" 428 | pass 429 | 430 | @sweep_end.setter 431 | def sweep_end(self, end): 432 | pass 433 | 434 | @property 435 | def sweep_center(self): 436 | """To be implemented.""" 437 | pass 438 | 439 | @sweep_center.setter 440 | def sweep_center(self, center): 441 | pass 442 | 443 | @property 444 | def sweep_span(self): 445 | """To be implemented.""" 446 | pass 447 | 448 | @sweep_span.setter 449 | def sweep_span(self, span): 450 | pass 451 | 452 | @property 453 | def sweep_ranging(self): 454 | """To be implemented.""" 455 | pass 456 | 457 | @sweep_ranging.setter 458 | def sweep_ranging(self, _range): 459 | pass 460 | 461 | @property 462 | def sweep_scale(self): 463 | """To be implemented.""" 464 | pass 465 | 466 | @sweep_scale.setter 467 | def sweep_scale(self, scale): 468 | pass 469 | 470 | @property 471 | def sweep_points(self): 472 | """To be implemented.""" 473 | pass 474 | 475 | @sweep_points.setter 476 | def sweep_points(self, num_points): 477 | pass 478 | 479 | @property 480 | def sweep_direction(self): 481 | """To be implemented.""" 482 | pass 483 | 484 | @sweep_direction.setter 485 | def sweep_direction(self, direction): 486 | pass 487 | 488 | # Ramping commands 489 | 490 | def ramp_to_zero(self): 491 | pass 492 | 493 | def ramp_to_setpoint(self, setpoint: float, step: float, wait: float): 494 | pass 495 | 496 | # Common commands 497 | 498 | def clear_status(self): 499 | """Clears all event registers and Error Queue.""" 500 | self._instrument.write('*cls') 501 | 502 | def reset_to_defaults(self): 503 | """Resets to defaults of Sourcemeter.""" 504 | self._instrument.write('*rst') 505 | 506 | def identify(self): 507 | """Returns manufacturer, model number, serial number, and firmware revision levels.""" 508 | response = self._instrument.write('*idn?') 509 | return {'manufacturer': response[0], 510 | 'model': response[1], 511 | 'serial number': response[2], 512 | 'firmware revision level': response[3] 513 | } 514 | 515 | def send_bus_trigger(self): 516 | """Sends a bus trigger to SourceMeter.""" 517 | self._instrument.write('*trg') 518 | -------------------------------------------------------------------------------- /labdrivers/labdrivers.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masonlab/labdrivers/9e79b0b5c4026eb124d62db7781f5f031b6284ed/labdrivers/labdrivers.py -------------------------------------------------------------------------------- /labdrivers/lakeshore/__init__.py: -------------------------------------------------------------------------------- 1 | from .ls332 import Ls332 2 | -------------------------------------------------------------------------------- /labdrivers/lakeshore/ls332.py: -------------------------------------------------------------------------------- 1 | """Module for controlling the Lake Shore Cryotronics 332. 2 | 3 | This module requires a National Instruments VISA driver, which can be found at 4 | https://www.ni.com/visa/. 5 | 6 | Attributes: 7 | resource_manager: the pyvisa resource manager which provides the visa 8 | objects used for communicating over the GPIB interface 9 | 10 | logger: a python logger object 11 | 12 | Classes: 13 | LS332: A class for interfacing to the LS332 temperature controller. 14 | """ 15 | import visa 16 | 17 | 18 | class Ls332: 19 | 20 | CHANNELS = ('A', 'B') 21 | 22 | def __init__(self, gpib_addr = 12): 23 | self.resource_manager = visa.ResourceManager() 24 | self._visa_resource = self.resource_manager.open_resource("GPIB::%d" % gpib_addr) 25 | self.channel = None 26 | 27 | @property 28 | def channel(self): 29 | """Gets or sets the current channel of the LS332. 30 | 31 | Setting expects the strings: 'A', 'B' 32 | 33 | :returns: The channel 'A' or 'B'""" 34 | return self.channel 35 | 36 | @channel.setter 37 | def channel(self, channel): 38 | input_value = channel.upper() 39 | 40 | if input_value in Ls332.CHANNELS: 41 | self.channel = input_value 42 | else: 43 | raise RuntimeError('Invalid channel') 44 | 45 | def reset(self): 46 | """Resets device to power-up settings.""" 47 | self._visa_resource.write("*RST") 48 | 49 | @property 50 | def temperature(self): 51 | """Reads the temperature of the currently-set channel.""" 52 | return self._visa_resource.query_ascii_values("KRDG? {}".format(self.channel)) 53 | 54 | @property 55 | def temperature_setpoint(self): 56 | """Reads the temperature set point of the currently-set channel.""" 57 | return self._visa_resource.query_ascii_values("SETP? {}".format(self.channel)) 58 | -------------------------------------------------------------------------------- /labdrivers/ni/__init__.py: -------------------------------------------------------------------------------- 1 | from .nidaq import Nidaq 2 | -------------------------------------------------------------------------------- /labdrivers/ni/nidaq.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | 3 | import PyDAQmx 4 | from PyDAQmx import Task 5 | 6 | 7 | class Nidaq: 8 | """Class to interface with a National Instruments DAQ 9 | 10 | Built on top of PyDAQmx. 11 | 12 | Reference: NI-DAQmx C Reference Help 13 | """ 14 | 15 | def __init__(self, device='Dev1'): 16 | self.device = device 17 | self._channel = None 18 | 19 | def reset(self): 20 | """Reset the device. Equivalent to -> Reset in NI MAX""" 21 | PyDAQmx.DAQmxResetDevice(self.device) 22 | 23 | def output_voltage(self, channel, output=0): 24 | """Outputs a voltage from the NI DAQ. 25 | 26 | :param channel: The name of the channel for output. 27 | :param output: The value of the output in Volts 28 | :return: None 29 | """ 30 | 31 | task = Task() 32 | 33 | # from NI-DAQmx C Reference: 34 | # int32 DAQmxCreateAOVoltageChan (TaskHandle taskHandle, 35 | # const char physicalChannel[], 36 | # const char nameToAssignToChannel[], 37 | # float64 minVal, 38 | # float64 maxVal, 39 | # int32 units, 40 | # const char customScaleName[]); 41 | task.CreateAOVoltageChan('/{}/{}'.format(self.device, channel), 42 | '', 43 | -10.0, 44 | 10.0, 45 | PyDAQmx.DAQmx_Val_Volts, 46 | None) 47 | 48 | task.StartTask() 49 | # from NI-DAQmx C Reference: 50 | # int32 DAQmxWriteAnalogScalarF64 (TaskHandle taskHandle, 51 | # bool32 autoStart, 52 | # float64 timeout, 53 | # float64 value, 54 | # bool32 *reserved); 55 | task.WriteAnalogScalarF64(1, 56 | 10.0, 57 | output, 58 | None) 59 | task.StopTask() 60 | 61 | def output_current(self, channel, output=0): 62 | """Outputs a current from the NI DAQ. 63 | 64 | :param channel: The name of the channel for output. 65 | :param output: The value of the output in Amps 66 | :return: None 67 | """ 68 | 69 | task = Task() 70 | 71 | # from NI-DAQmx C Reference: 72 | # int32 DAQmxCreateAOCurrentChan (TaskHandle taskHandle, 73 | # const char physicalChannel[], 74 | # const char nameToAssignToChannel[], 75 | # float64 minVal, 76 | # float64 maxVal, 77 | # int32 units, 78 | # const char customScaleName[]); 79 | task.CreateAOCurrentChan('/{}/{}'.format(self.device, channel), 80 | '', 81 | -10.0, 82 | 10.0, 83 | PyDAQmx.DAQmx_Val_Amps, 84 | None) 85 | 86 | task.StartTask() 87 | # from NI-DAQmx C Reference: 88 | # int32 DAQmxWriteAnalogScalarF64 (TaskHandle taskHandle, 89 | # bool32 autoStart, 90 | # float64 timeout, 91 | # float64 value, 92 | # bool32 *reserved); 93 | task.WriteAnalogScalarF64(1, 94 | 10.0, 95 | output, 96 | None) 97 | task.StopTask() 98 | 99 | def read_voltage(self, channel, minVal=-10.0, maxVal=10.0): 100 | """Read the voltage input level of `channel` 101 | 102 | Using narrow bounds (with minVal and maxVal) will improve accuracy 103 | since the same digital resolution will be applied to a smaller 104 | analog range. 105 | 106 | Args: 107 | channel(str): the name of the channel to use 108 | minVal(float): the minimum value you expect to measure 109 | maxVal(float): the maximum value you expect to measure""" 110 | 111 | task = Task() 112 | measured_point = ctypes.c_double(0) 113 | 114 | # from NI-DAQmx C Reference: 115 | # int32 DAQmxCreateAIVoltageChan (TaskHandle taskHandle, 116 | # const char physicalChannel[], 117 | # const char nameToAssignToChannel[], 118 | # int32 terminalConfig, 119 | # float64 minVal, 120 | # float64 maxVal, 121 | # int32 units, 122 | # const char customScaleName[]); 123 | task.CreateAIVoltageChan('/{}/{}'.format(self.device, channel), 124 | '', 125 | PyDAQmx.DAQmx_Val_Cfg_Default, 126 | minVal, 127 | maxVal, 128 | PyDAQmx.DAQmx_Val_Volts, 129 | None) 130 | 131 | task.StartTask() 132 | # from NI-DAQmx C Reference: 133 | # int32 DAQmxReadAnalogScalarF64 (TaskHandle taskHandle, 134 | # float64 timeout, 135 | # float64 *value, 136 | # bool32 *reserved); 137 | task.ReadAnalogScalarF64(1.0, 138 | ctypes.POINTER(ctypes.c_double)(measured_point), 139 | None) 140 | 141 | # see http://stackoverflow.com/questions/1413851/expected-lp-c-double-instance-instead-of-c-double-array-python-ctypes-error 142 | # for an explanation of the POINTER black magic 143 | 144 | task.StopTask() 145 | 146 | return measured_point.value 147 | 148 | def read_current(self, channel, minVal=-10.0, maxVal=10.0): 149 | """Read the current input level of `channel` 150 | 151 | Using narrow bounds (with minVal and maxVal) will improve accuracy 152 | since the same digital resolution will be applied to a smaller 153 | analog range. 154 | 155 | Args: 156 | channel(str): the name of the channel to use 157 | minVal(float): the minimum value you expect to measure 158 | maxVal(float): the maximum value you expect to measure""" 159 | 160 | task = Task() 161 | measured_point = ctypes.c_double(0) 162 | 163 | # from NI-DAQmx C Reference: 164 | # int32 DAQmxCreateAICurrentChan (TaskHandle taskHandle, 165 | # const char physicalChannel[], 166 | # const char nameToAssignToChannel[], 167 | # int32 terminalConfig, 168 | # float64 minVal, 169 | # float64 maxVal, 170 | # int32 units, 171 | # int32 shuntResistorLoc, 172 | # float64 extShuntResistorVal, 173 | # const char customScaleName[]); 174 | task.CreateAICurrentChan('/{}/{}'.format(self.device, channel), 175 | '', 176 | PyDAQmx.DAQmx_Val_Cfg_Default, 177 | minVal, 178 | maxVal, 179 | PyDAQmx.DAQmx_Val_Amps, 180 | PyDAQmx.DAQmx_Val_Default, 181 | 1.0, 182 | None) 183 | 184 | task.StartTask() 185 | # from NI-DAQmx C Reference: 186 | # int32 DAQmxReadAnalogScalarF64 (TaskHandle taskHandle, 187 | # float64 timeout, 188 | # float64 *value, 189 | # bool32 *reserved); 190 | task.ReadAnalogScalarF64(1.0, 191 | ctypes.POINTER(ctypes.c_double)(measured_point), 192 | None) 193 | 194 | # see http://stackoverflow.com/questions/1413851/expected-lp-c-double-instance-instead-of-c-double-array-python-ctypes-error 195 | # for an explanation of the POINTER black magic 196 | 197 | task.StopTask() 198 | 199 | return measured_point.value 200 | -------------------------------------------------------------------------------- /labdrivers/oxford/__init__.py: -------------------------------------------------------------------------------- 1 | from .ips120 import Ips120 2 | from .itc503 import Itc503 3 | from .mercuryips import MercuryIps 4 | from .triton200 import Triton200 5 | -------------------------------------------------------------------------------- /labdrivers/oxford/ips120.py: -------------------------------------------------------------------------------- 1 | """Module containing a class to interface with an Oxford Instruments IPS 120-10. 2 | 3 | This module requires a National Instruments VISA driver, which can be found at 4 | https://www.ni.com/visa/ 5 | 6 | Attributes: 7 | resource_manager: the pyvisa resource manager which provides the visa 8 | objects used for communicating over the GPIB interface 9 | 10 | logger: a python logger object 11 | 12 | 13 | Classes: 14 | ips120: a class for interfacing with a IPS 120-10 magnet power supply 15 | 16 | """ 17 | from datetime import datetime 18 | import time 19 | import logging 20 | 21 | import visa 22 | 23 | # create a logger object for this module 24 | logger = logging.getLogger(__name__) 25 | # added so that log messages show up in Jupyter notebooks 26 | logger.addHandler(logging.StreamHandler()) 27 | 28 | try: 29 | # the pyvisa manager we'll use to connect to the GPIB resources 30 | resource_manager = visa.ResourceManager() 31 | except OSError: 32 | logger.exception("\n\tCould not find the VISA library. Is the National Instruments VISA driver installed?\n\n") 33 | 34 | 35 | class Ips120: 36 | 37 | def __init__(self, GPIBaddr): 38 | """Connect to an IPS 120-10 at the specified GPIB address 39 | 40 | Args: 41 | GPIBaddr(int): GPIB address of the IPS 120-10 42 | """ 43 | self._visa_resource = resource_manager.open_resource("GPIB::%d" % GPIBaddr) 44 | self._visa_resource.read_termination = '\r' 45 | self.setDisplay('tesla') 46 | 47 | def setControl(self, state=3): 48 | """Set the LOCAL / REMOTE control state of the IPS 120-10 49 | 50 | 0 - Local & Locked (default state) 51 | 1 - Remote & Locked 52 | 2 - Local & Unlocked 53 | 3 - Remote & Locked 54 | 55 | Args: 56 | state(int): the state in which to place the IPS 120-10 57 | """ 58 | assert type(state) == int, 'argument must be integer' 59 | assert state in [0,1,2,3], 'argument must be one of [0,1,2,3]' 60 | 61 | self._visa_resource.write("$C{}".format(state)) 62 | 63 | def readField(self): 64 | """Read the current magnetic field in Tesla 65 | 66 | Returns: 67 | field(float): current magnetic field in Tesla 68 | """ 69 | self._visa_resource.write('R 7') 70 | self._visa_resource.wait_for_srq() 71 | value_str = self._visa_resource.read() 72 | 73 | return float(value_str.strip('R+')) 74 | 75 | def readFieldSetpoint(self): 76 | """Read the current set point for the magnetic field in Tesla 77 | 78 | Returns: 79 | setpoint(float): current set point for the magnetic field in Tesla 80 | """ 81 | self._visa_resource.write('R 8') 82 | self._visa_resource.wait_for_srq() 83 | value_str = self._visa_resource.read() 84 | 85 | return float(value_str.strip('R+')) 86 | 87 | def readFieldSweepRate(self): 88 | """Read the current magnetic field sweep rate in Tesla/min 89 | 90 | Returns: 91 | sweep_rate(float): current magnetic field sweep rate in Tesla/min 92 | """ 93 | self._visa_resource.write('R 9') 94 | self._visa_resource.wait_for_srq() 95 | value_str = self._visa_resource.read() 96 | 97 | return float(value_str.strip('R+')) 98 | 99 | def setActivity(self, state=1): 100 | """Set the field activation method 101 | 102 | 0 - Hold 103 | 1 - To Set Point 104 | 2 - To Zero 105 | 3 - Clamp (clamp the power supply output) 106 | 107 | Args: 108 | state(int): the field activation method 109 | """ 110 | assert type(state) == int, 'argument must be integer' 111 | assert state in [0,1,2,3], 'argument must be one of [0,1,2,3]' 112 | self._visa_resource.write("$A{}".format(state)) 113 | 114 | 115 | def setHeater(self, state=1): 116 | """Set the switch heater activation state 117 | 118 | 0 - Heater Off (close switch) 119 | 1 - Heater On if PSU=Magnet (open switch) 120 | 2 - Heater On, no checks (open switch) 121 | 122 | Args: 123 | state(int): the switch heater activation state 124 | """ 125 | assert type(state) == int, 'argument must be integer' 126 | assert state in [0,1,2], 'argument must be one of [0,1,2]' 127 | self._visa_resource.write("$H{}".format(state)) 128 | 129 | # TODO: add timer to account for time it takes for switch to activate 130 | 131 | def setFieldSetpoint(self, field): 132 | """Set the magnetic field set point, in Tesla 133 | 134 | Args: 135 | field(float): the magnetic field set point, in Tesla 136 | """ 137 | MAX_FIELD = 8 138 | assert abs(field) < MAX_FIELD, 'field must be less than {}'.format(MAX_FIELD) 139 | 140 | self._visa_resource.write("$J{}".format(field)) 141 | 142 | def setFieldSweepRate(self, rate): 143 | """Set the magnetic field sweep rate, in Tesla/min 144 | 145 | Args: 146 | rate(float): the magnetic field sweep rate, in Tesla/min 147 | """ 148 | self._visa_resource.write("$T{}".format(rate)) 149 | 150 | def setDisplay(self, display): 151 | """Set the display to show amps or tesla 152 | 153 | Args: 154 | display(str): One of ['amps','tesla'] 155 | """ 156 | assert display in ['amps', 'tesla'], "argument must be one of ['amps','tesla']" 157 | 158 | mode_dict = {'amps':8, 159 | 'tesla':9 160 | } 161 | 162 | self._visa_resource.write("$M{}".format(mode_dict[display])) 163 | 164 | def waitForField(self, timeout=600, error_margin=0.01): 165 | """Wait for the field to reach the set point 166 | 167 | Args: 168 | timeout(int): maximum time to wait, in seconds 169 | error_margin(float): how close the field needs to be to the set point, in tesla 170 | 171 | Returns: 172 | (bool): whether the field set point was reached 173 | """ 174 | start_time = datetime.now() 175 | stop_time = start_time + datetime.timedelta(seconds=timeout) 176 | 177 | while datetime.now() < stop_time: 178 | field = self.readField() 179 | set_point = self.readFieldSetpoint() 180 | 181 | if abs(field - set_point) < error_margin: 182 | return True 183 | 184 | time.sleep(5) 185 | 186 | return False 187 | -------------------------------------------------------------------------------- /labdrivers/oxford/itc503.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import visa 4 | 5 | # create a logger object for this module 6 | logger = logging.getLogger(__name__) 7 | # added so that log messages show up in Jupyter notebooks 8 | logger.addHandler(logging.StreamHandler()) 9 | 10 | try: 11 | # the pyvisa manager we'll use to connect to the GPIB resources 12 | resource_manager = visa.ResourceManager() 13 | except OSError: 14 | logger.exception("\n\tCould not find the VISA library. Is the National Instruments VISA driver installed?\n\n") 15 | 16 | 17 | class Itc503: 18 | """ 19 | Module to connect to an ITC 503. 20 | 21 | Modes supported: GPIB 22 | 23 | :param gpib_addr: GPIB address of the ITC 503 24 | """ 25 | 26 | def __init__(self, gpib_addr=24): 27 | self._visa_resource = resource_manager.open_resource("GPIB::%d" % gpib_addr) 28 | self._visa_resource.read_termination = '\r' 29 | 30 | def setControl(self, unlocked=1, remote=1): 31 | """Set the LOCAL / REMOTE control state of the ITC 503 32 | 33 | :param unlocked (int): 0 to lock, 1 to unlock 34 | :param remote (int): 0 for local, 1 for remote 35 | :return: None 36 | """ 37 | state_bit = str(remote) + str(unlocked) 38 | state = int(state_bit, 2) 39 | 40 | self._visa_resource.write("$C{}".format(state)) 41 | 42 | def setTemperature(self, temperature=0.010): 43 | """Change the temperature set point. 44 | 45 | :param temperature (float): Temperature set point in Kelvin (default: 0.010) 46 | """ 47 | assert type(temperature) in [int, float], 'argument must be a number' 48 | 49 | command = '$T' + str(int(1000*temperature)) 50 | self._visa_resource.write(command) 51 | 52 | def getValue(self, variable=0): 53 | """Read the variable defined by the index. 54 | 55 | The possible inputs are:: 56 | 57 | 0: SET TEMPERATURE 58 | 1: SENSOR 1 TEMPERATURE 59 | 2: SENSOR 2 TEMPERATURE 60 | 3: SENSOR 3 TEMPERATURE 61 | 4: TEMPERATURE ERROR 62 | 5: HEATER O/P (as %) 63 | 6: HEATER O/P (as V) 64 | 7: GAS FLOW O/P (a.u.) 65 | 8: PROPORTIONAL BAND 66 | 9: INTEGRAL ACTION TIME 67 | 10: DERIVATIVE ACTION TIME 68 | 69 | :param variable (int): Index of variable to read. 70 | """ 71 | assert type(variable) == int, 'Argument must be an integer.' 72 | assert variable in range(0,11), 'Argument is not a valid number.' 73 | 74 | self._visa_resource.write('$R{}'.format(variable)) 75 | self._visa_resource.wait_for_srq() 76 | value = self._visa_resource.read() 77 | 78 | return float(value.strip('R+')) 79 | 80 | def setProportional(self, prop=0): 81 | """Sets the proportional band. 82 | 83 | :param prop (float): Proportional band, in steps of 0.0001K. 84 | """ 85 | self._visa_resource.write('$P{}'.format(prop)) 86 | return None 87 | 88 | def setIntegral(self, integral=0): 89 | """Sets the integral action time. 90 | 91 | Args: 92 | integral: Integral action time, in steps of 0.1 minute. 93 | Ranges from 0 to 140 minutes. 94 | """ 95 | self._visa_resource.write('$I{}'.format(integral)) 96 | return None 97 | 98 | def setDerivative(self, derivative=0): 99 | """Sets the derivative action time. 100 | 101 | Args: 102 | derivative: Derivative action time. 103 | Ranges from 0 to 273 minutes. 104 | """ 105 | self._visa_resource.write('$D{}'.format(derivative)) 106 | return None 107 | 108 | def setHeaterSensor(self, sensor=1): 109 | """Selects the heater sensor. 110 | 111 | Args: 112 | sensor: Should be 1, 2, or 3, corresponding to 113 | the heater on the front panel. 114 | """ 115 | 116 | assert sensor in [1,2,3], 'Heater not on list.' 117 | 118 | self._visa_resource.write('$H{}'.format(sensor)) 119 | return None 120 | 121 | def setHeaterOutput(self, heater_output=0): 122 | """Sets the heater output level. 123 | 124 | Args: 125 | heater_output: Sets the percent of the maximum 126 | heater output in units of 0.1%. 127 | Min: 0. Max: 999. 128 | """ 129 | 130 | self._visa_resource.write('$O{}'.format(heater_output)) 131 | return None 132 | 133 | def setGasOutput(self, gas_output=0): 134 | """Sets the gas (needle valve) output level. 135 | 136 | Args: 137 | gas_output: Sets the percent of the maximum gas 138 | output in units of 0.1%. 139 | Min: 0. Max: 999. 140 | """ 141 | self._visa_resource.write('$G{}'.format(gas_output)) 142 | return None 143 | 144 | def setAutoControl(self, auto_manual=0): 145 | """Sets automatic control for heater/gas(needle valve). 146 | 147 | Value:Status map 148 | 0: heater manual, gas manual 149 | 1: heater auto , gas manual 150 | 2: heater manual, gas auto 151 | 3: heater auto , gas auto 152 | 153 | Args: 154 | auto_manual: Index for gas/manual. 155 | """ 156 | self._visa_resource.write('$A{}'.format(auto_manual)) 157 | 158 | def setSweeps(self, sweep_parameters): 159 | """Sets the parameters for all sweeps. 160 | 161 | This fills up a dictionary with all the possible steps in 162 | a sweep. If a step number is not found in the sweep_parameters 163 | dictionary, then it will create the sweep step with all 164 | parameters set to 0. 165 | 166 | Args: 167 | sweep_parameters: A dictionary whose keys are the step 168 | numbers (keys: 1-16). The value of each key is a 169 | dictionary whose keys are the parameters in the 170 | sweep table (see _setSweepStep). 171 | """ 172 | steps = range(1,17) 173 | parameters_keys = sweep_parameters.keys() 174 | null_parameter = { 'set_point' : 0, 175 | 'sweep_time': 0, 176 | 'hold_time' : 0 } 177 | 178 | for step in steps: 179 | if step in parameters_keys: 180 | self._setSweepStep(step, sweep_parameters[step]) 181 | else: 182 | self._setSweepStep(step, null_parameter) 183 | 184 | def _setSweepStep(self, sweep_step, sweep_table): 185 | """Sets the parameters for a sweep step. 186 | 187 | This sets the step pointer (x) to the proper step. 188 | Then this sets the step parameters (y1, y2, y3) to 189 | the values dictated by the sweep_table. Finally, this 190 | resets the x and y pointers to 0. 191 | 192 | Args: 193 | sweep_step: The sweep step to be modified (values: 1-16) 194 | sweep_table: A dictionary of parameters describing the 195 | sweep. Keys: set_point, sweep_time, hold_time. 196 | """ 197 | step_setting = '$x{}'.format(sweep_step) 198 | self._visa_resource.write(step_setting) 199 | 200 | setpoint_setting = '$s{}'.format( 201 | sweep_table['set_point']) 202 | sweeptime_setting = '$s{}'.format( 203 | sweep_table['sweep_time']) 204 | holdtime_setting = '$s{}'.format( 205 | sweep_table['hold_time']) 206 | 207 | self._visa_resource.write('$y1') 208 | self._visa_resource.write(setpoint_setting) 209 | 210 | self._visa_resource.write('$y2') 211 | self._visa_resource.write(sweeptime_setting) 212 | 213 | self._visa_resource.write('$y3') 214 | self._visa_resource.write(holdtime_setting) 215 | 216 | self._resetSweepTablePointers() 217 | 218 | def _resetSweepTablePointers(self): 219 | """Resets the table pointers to x=0 and y=0 to prevent 220 | accidental sweep table changes. 221 | """ 222 | self._visa_resource.write('$x0') 223 | self._visa_resource.write('$y0') -------------------------------------------------------------------------------- /labdrivers/oxford/mercuryips.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | import visa 4 | 5 | 6 | class MercuryIps: 7 | 8 | # Magnet class 9 | 10 | class Magnet: 11 | """ 12 | Constructor for a magnet along a certain axis. 13 | 14 | :param axis: The axis for the magnet, given by ['GRPX'|'GRPY'|'GRPZ'] 15 | :type axis: string 16 | :param mode: Connection, given by ['ip'|'visa'] 17 | :type mode: string 18 | :param resource_name: VISA resource name of the MercuryIPS 19 | :type resource_name: string 20 | :param ip_address: IP address of the MercuryIPS 21 | :type ip_address: string 22 | :param port: Port number of the Mercury iPS 23 | :type port: integer 24 | :param timeout: Time to wait for a response from the MercuryIPS before throwing an error. 25 | :type timeout: float 26 | :param bytes_to_read: Amount of information to read from a response 27 | :type bytes_to_read: integer 28 | """ 29 | 30 | def __init__(self, axis, mode='ip', resource_name=None, ip_address=None, port=7020, timeout=10.0, 31 | bytes_to_read=2048): 32 | self.axis = axis 33 | self.mode = mode 34 | self.resource_name = resource_name 35 | self.resource_manager = visa.ResourceManager() 36 | self.ip_address = ip_address 37 | self.port = port 38 | self.timeout = timeout 39 | self.bytes_to_read = bytes_to_read 40 | 41 | ################### 42 | # Query functions # 43 | #################### 44 | 45 | def query_ip(self, command): 46 | """Sends a query to the MercuryIPS via ethernet. 47 | 48 | :param command: The command, which should be in the NOUN + VERB format 49 | :type command: string 50 | :returns str: The MercuryIPS response 51 | """ 52 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: 53 | s.connect((self.ip_address, self.port)) 54 | s.settimeout(self.timeout) 55 | s.sendall(command.encode()) 56 | response = s.recv(self.bytes_to_read).decode() 57 | 58 | return response.decode() 59 | 60 | def query_visa(self, command): 61 | """Sends a query to the MercuryIPS via VISA. 62 | 63 | :param command: The command, which should be in the NOUN + VERB format 64 | :type command: string 65 | :returns str: The MercuryIPS response 66 | """ 67 | instr = self.resource_manager.open_resource(self.resource_name) 68 | response = instr.query(command) 69 | instr.close() 70 | 71 | return response 72 | 73 | @staticmethod 74 | def extract_value(response, noun, unit): 75 | """Finds the value that is contained within the response to a previously sent query. 76 | 77 | :param response: The response from a query. 78 | :type response: string 79 | :param noun: The part of the query that refers to the NOUN (refer to MercuryIPS documentation). 80 | :param unit: The measurement unit (e.g. K for Kelvin, T for Tesla). 81 | :returns float: The value of the response, but without units. 82 | """ 83 | expected_response = 'STAT:' + noun + ':' 84 | value = float(response.replace(expected_response, '').strip('\n').replace(unit, '')) 85 | return value 86 | 87 | # Employing hash tables instead of if-else trees 88 | QUERY_AND_RECEIVE = {'ip': query_ip, 'visa': query_visa} 89 | 90 | @property 91 | def field_setpoint(self): 92 | """The magnetic field set point in Tesla""" 93 | noun = 'DEV:' + self.axis + ':PSU:SIG:FSET' 94 | command = 'READ:' + noun + '\n' 95 | response = MercuryIps.Magnet.QUERY_AND_RECEIVE[self.mode](self, command) 96 | return self.extract_value(response, noun, 'T') 97 | 98 | @field_setpoint.setter 99 | def field_setpoint(self, value): 100 | if ((self.axis == 'GRPZ' and (-6 <= value <= 6)) or 101 | ((self.axis == 'GRPX' or self.axis == 'GRPY') and (-1 <= value <= 1))): 102 | setpoint = str(value) 103 | command = 'SET:DEV:' + self.axis + ':PSU:SIG:FSET:' + setpoint + '\n' 104 | response = MercuryIps.Magnet.QUERY_AND_RECEIVE[self.mode](self, command) 105 | 106 | if not response: 107 | raise RuntimeWarning("No response from the MercuryIps after querying the field setpoint.") 108 | else: 109 | raise RuntimeError("The setpoint must be within the proper limits.") 110 | 111 | @property 112 | def field_ramp_rate(self): 113 | """The magnetic field ramp rate in Tesla per minute along the magnet axis.""" 114 | noun = 'DEV:' + self.axis + ':PSU:SIG:RFST' 115 | command = 'READ:' + noun + '\n' 116 | response = MercuryIps.Magnet.QUERY_AND_RECEIVE[self.mode](self, command) 117 | return self.extract_value(response, noun, 'T/m') 118 | 119 | @field_ramp_rate.setter 120 | def field_ramp_rate(self, value): 121 | ramp_rate = str(value) 122 | command = 'SET:DEV:' + self.axis + ':PSU:SIG:RFST:' + ramp_rate + '\n' 123 | response = MercuryIps.Magnet.QUERY_AND_RECEIVE[self.mode](self, command) 124 | 125 | if not response: 126 | raise RuntimeWarning("No response after setting a field ramp rate.") 127 | 128 | @property 129 | def current_setpoint(self): 130 | """The set point of the current for a magnet in Amperes.""" 131 | noun = 'DEV:' + self.axis + ':PSU:SIG:CSET' 132 | command = 'READ:' + noun + '\n' 133 | response = MercuryIps.Magnet.QUERY_AND_RECEIVE[self.mode](self, command) 134 | return self.extract_value(response, noun, 'A') 135 | 136 | @current_setpoint.setter 137 | def current_setpoint(self, value): 138 | setpoint = str(value) 139 | command = 'SET:DEV:' + self.axis + ':PSU:SIG:CSET' + setpoint + '\n' 140 | response = MercuryIps.Magnet.QUERY_AND_RECEIVE[self.mode](self, command) 141 | 142 | if not response: 143 | raise RuntimeWarning("No response after setting current set point.") 144 | 145 | @property 146 | def current_ramp_rate(self): 147 | """The ramp rate of the current for a magnet in Amperes per minute.""" 148 | noun = 'DEV:' + self.axis + ':PSU:SIG:RCST' 149 | command = 'READ:' + noun + '\n' 150 | response = MercuryIps.Magnet.QUERY_AND_RECEIVE[self.mode](self, command) 151 | return self.extract_value(response, noun, 'A/m') 152 | 153 | @current_ramp_rate.setter 154 | def current_ramp_rate(self, value): 155 | ramp_rate = str(value) 156 | command = 'SET:DEV:' + self.axis + ':PSU:SIG:RCST' + ramp_rate + '\n' 157 | response = MercuryIps.Magnet.QUERY_AND_RECEIVE[self.mode](self, command) 158 | 159 | if not response: 160 | raise RuntimeWarning("No response after setting current ramp rate.") 161 | 162 | @property 163 | def magnetic_field(self): 164 | """Gets the magnetic field.""" 165 | noun = 'DEV:' + self.axis + ':PSU:SIG:FLD' 166 | command = 'READ:' + noun + '\n' 167 | response = MercuryIps.Magnet.QUERY_AND_RECEIVE[self.mode](self, command) 168 | return self.extract_value(response, noun, 'T') 169 | 170 | def ramp_to_setpoint(self): 171 | """Ramps a magnet to the setpoint.""" 172 | command = 'SET:DEV:' + self.axis + ':PSU:ACTN:RTOS\n' 173 | response = MercuryIps.Magnet.QUERY_AND_RECEIVE[self.mode](self, command) 174 | 175 | if not response: 176 | raise RuntimeWarning("No response after attempting to ramp to set point.") 177 | 178 | def ramp_to_zero(self): 179 | """Ramps a magnet from its current magnetic field to zero field.""" 180 | command = 'SET:DEV:' + self.axis + ':PSU:ACTN:RTOZ\n' 181 | response = MercuryIps.Magnet.QUERY_AND_RECEIVE[self.mode](self, command) 182 | 183 | if not response: 184 | raise RuntimeWarning("No response after attempting to ramp to zero.") 185 | 186 | def ramping(self): 187 | """Queries if magnet is ramping.""" 188 | # ask if ramping to zero 189 | # command = 'READ:DEV:' + self.axis + ':PSU:ACTN:RTOZ\n' 190 | # ask if ramping to set 191 | # command = 'READ:DEV:' + self.axis + ':PSU:ACTN:RTOS\n' 192 | # TODO: find out what kind of response you expect 193 | pass 194 | 195 | def hold(self): 196 | """Puts a magnet in a HOLD state. 197 | 198 | This action does one of the following: 199 | 1) Stops a ramp 200 | 2) Allows the field and current to ramp 201 | """ 202 | command = 'SET:DEV:' + self.axis + ':PSU:ACTN:HOLD\n' 203 | response = MercuryIps.Magnet.QUERY_AND_RECEIVE[self.mode](self, command) 204 | 205 | if not response: 206 | raise RuntimeWarning("No response after telling Mercury iPS to hold.") 207 | 208 | def holding(self): 209 | """Queries if magnet is in a HOLD state.""" 210 | # command = 'READ:DEV:' + self.axis + ':PSU:ACTN:HOLD\n' 211 | # response = MercuryIps.Magnet.QUERY_AND_RECEIVE[self.mode](self, command) 212 | # TODO: find out what kind of response you expect 213 | pass 214 | 215 | def clamp(self): 216 | """Puts a magnet in a CLAMP state.""" 217 | command = 'SET:DEV:' + self.axis + ':PSU:ACTN:CLMP\n' 218 | response = MercuryIps.Magnet.QUERY_AND_RECEIVE[self.mode](self, command) 219 | 220 | if not response: 221 | raise RuntimeWarning("No response after telling Mercury iPS to clamp.") 222 | 223 | def clamped(self): 224 | """Queries if magnet is in a CLAMP state.""" 225 | # command = 'READ:DEV:' + self.axis + ':PSU:ACTN:CLMP\n' 226 | # response = MercuryIps.Magnet.QUERY_AND_RECEIVE[self.mode](self, command) 227 | # TODO: find out what kind of response you expect 228 | pass 229 | 230 | def __init__(self, mode='ip', 231 | resource_name=None, 232 | ip_address=None, port=7020, timeout=10.0, bytes_to_read=2048): 233 | """ 234 | Parameters: 235 | :param str mode: The connection to the iPS, either 'ip' or 'visa' 236 | :param str resource_name: VISA resource name of the Mercury iPS 237 | :param str ip_address: IP address of the Mercury iPS 238 | :param port: Port number of the Mercury iPS 239 | :type port: integer 240 | :param timeout: Time in seconds to wait for command acknowledgment 241 | :type timeout: float 242 | :param bytes_to_read: Number of bytes to read from query 243 | :type bytes_to_read: integer 244 | """ 245 | supported_modes = ('ip', 'visa') 246 | 247 | if mode.lower().strip() in supported_modes: 248 | self.mode = mode 249 | else: 250 | raise RuntimeError('Mode is not currently supported.') 251 | 252 | self.x_magnet = MercuryIps.Magnet('GRPX', mode=mode, resource_name=resource_name, ip_address=ip_address, 253 | port=7020, timeout=timeout, bytes_to_read=bytes_to_read) 254 | self.y_magnet = MercuryIps.Magnet('GRPY', mode=mode, resource_name=resource_name, ip_address=ip_address, 255 | port=7020, timeout=timeout, bytes_to_read=bytes_to_read) 256 | self.z_magnet = MercuryIps.Magnet('GRPZ', mode=mode, resource_name=resource_name, ip_address=ip_address, 257 | port=7020, timeout=timeout, bytes_to_read=bytes_to_read) 258 | 259 | def circle_sweep(self, field_radius, number_points): 260 | pass 261 | -------------------------------------------------------------------------------- /labdrivers/oxford/triton200.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | 4 | class Triton200: 5 | """ 6 | Create an instance of the Triton200 class. 7 | 8 | Supported modes: IP 9 | 10 | :param str ip_address: The IP address of the Triton 200. 11 | :param int port_number: The associated port number of the Triton 200 (default: 33576) 12 | :param int timeout: How long to wait for a response (default: 10000) 13 | :param int bytes_to_read: How many bytes to accept from the response (default: 2048) 14 | """ 15 | 16 | def __init__(self, ip_address, port_number=33576, timeout=10000, bytes_to_read=2048): 17 | 18 | self._address = (str(ip_address), int(port_number)) 19 | self._timeout = timeout 20 | self._bytes_to_read = bytes_to_read 21 | self._temperature_channel = Triton200.RUO2_CHANNEL 22 | self._temperature_setpoint = 0.0 23 | self._heater_range = 0.0 24 | 25 | self._heater_channel = '1' 26 | self._turbo_channel = '1' 27 | 28 | @property 29 | def temperature_channel(self): 30 | """ 31 | :returns str: The temperature channel, either the cernox (5) or the RuO2 (6) 32 | """ 33 | return self._temperature_channel 34 | 35 | @temperature_channel.setter 36 | def temperature_channel(self, value): 37 | self._temperature_channel = str(value) 38 | 39 | @property 40 | def temperature_setpoint(self): 41 | return self._temperature_setpoint 42 | 43 | @temperature_setpoint.setter 44 | def temperature_setpoint(self, value): 45 | if not isinstance(value, float): 46 | raise RuntimeError("Make sure the temperature set point is a number.") 47 | elif 0 <= value < 10: 48 | self._temperature_setpoint = value 49 | else: 50 | print("Keep an eye on the turbo pump if you ramp!!!") 51 | self._temperature_setpoint = value 52 | 53 | @property 54 | def temperature(self): 55 | """The temperature reading from the current temperature channel.""" 56 | noun = 'DEV:T' + str(self.temperature_channel) + ':TEMP:SIG:TEMP' 57 | command = 'READ:' + noun + '\r\n' 58 | response = self.query_and_receive(command) 59 | 60 | return self.extract_value(response, noun, 'K') 61 | 62 | def update_heater(self): 63 | """ 64 | Associates the heater with the current temperature channel and changes the heater current to 65 | preset values given the temperature set point. 66 | """ 67 | heater_range = ['0.316', '1', '3.16', '10', '31.6', '100'] 68 | command = 'SET:DEV:T' + str(self.temperature_channel) + ':TEMP:LOOP:HTR:H' + str(self._heater_channel) + '\r\n' 69 | response = self.query_and_receive(command) 70 | 71 | if not response: 72 | raise RuntimeError("Changing of heater focus unsuccessful.") 73 | 74 | heater_index = ((self.temperature_setpoint > 0.030) 75 | + (self.temperature_setpoint > 0.050) 76 | + (self.temperature_setpoint > 0.300) 77 | + (self.temperature_setpoint > 1.000) 78 | + (self.temperature_setpoint > 1.500)) 79 | heater_current = heater_range[heater_index] 80 | 81 | command = 'SET:DEV:T' + str(self.temperature_channel) + ':TEMP:LOOP:RANGE:' + heater_current + '\r\n' 82 | response = self.query_and_receive(command) 83 | 84 | if not response: 85 | raise RuntimeError("Changing of heater range unsuccessful.") 86 | 87 | def controlled_ramp_on(self): 88 | """Starts a temperature sweep for the current temperature channel.""" 89 | command = 'SET:DEV:T' + str(self.temperature_channel) + 'TEMP:LOOP:RAMP:ENAB:ON\r\n' 90 | response = self.query_and_receive(command) 91 | 92 | if not response: 93 | raise RuntimeError("Enabling of temperature ramp unsuccessful.") 94 | 95 | def controlled_ramp_off(self): 96 | """Stops a temperature sweep for the current temperature channel.""" 97 | command = 'SET:DEV:T' + str(self.temperature_channel) + 'TEMP:LOOP:RAMP:ENAB:OFF\r\n' 98 | response = self.query_and_receive(command) 99 | 100 | if not response: 101 | raise RuntimeError("Disabling of temperature ramp unsuccessful.") 102 | 103 | def turbo_on(self): 104 | """Turns on a turbo pump. 105 | 106 | WARNING: Do not use this unless you know what you are doing.""" 107 | command = 'SET:DEV:TURB' + self._turbo_channel + ':PUMP:SIG:STATE:ON\r\n' 108 | response = self.query_and_receive(command) 109 | 110 | if not response: 111 | raise RuntimeError("Enabling of turbo pump unsuccessful.") 112 | 113 | def turbo_off(self): 114 | """Turns off a turbo pump. 115 | 116 | WARNING: Do not use this unless you know what you are doing.""" 117 | command = 'SET:DEV:TURB' + self._turbo_channel + ':PUMP:SIG:STATE:OFF\r\n' 118 | response = self.query_and_receive(command) 119 | 120 | if not response: 121 | raise RuntimeError("Disabling of turbo pump unsuccessful.") 122 | 123 | def query_and_receive(self, command): 124 | """ 125 | Queries the Oxford Triton 200 with the given command. 126 | 127 | :param command: Specifies a read/write of a property. 128 | """ 129 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: 130 | s.connect(self._address) 131 | s.settimeout(self._timeout) 132 | s.sendall(command.encode()) 133 | response = s.recv(self._bytes_to_read).decode() 134 | 135 | return response 136 | 137 | @staticmethod 138 | def extract_value(response, noun, unit): 139 | expected_response = 'STAT:' + noun + ':' 140 | value = float(response.replace(expected_response, '').strip('\n').replace(unit, '')) 141 | return value 142 | -------------------------------------------------------------------------------- /labdrivers/quantumdesign/QDInstrument.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masonlab/labdrivers/9e79b0b5c4026eb124d62db7781f5f031b6284ed/labdrivers/quantumdesign/QDInstrument.dll -------------------------------------------------------------------------------- /labdrivers/quantumdesign/__init__.py: -------------------------------------------------------------------------------- 1 | from .qdinstrument import Dynacool 2 | from .qdinstrument import Ppms 3 | from .qdinstrument import Svsm 4 | from .qdinstrument import VersaLab 5 | from .qdinstrument import Mpms 6 | -------------------------------------------------------------------------------- /labdrivers/quantumdesign/qdinstrument.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import clr 4 | 5 | logger = logging.getLogger(__name__) 6 | logger.addHandler(logging.StreamHandler()) 7 | 8 | # load the C# .dll supplied by Quantum Design 9 | clr.AddReference('QDInstrument') 10 | 11 | if clr.FindAssembly('QDInstrument') is None: 12 | logger.exception('\n\tCould not find QDInstrument.dll') 13 | else: 14 | logger.exception('\n\tFound QDInstrument.dll at {}'.format(clr.FindAssembly('QDInstrument'))) 15 | logger.exception('\n\tTry right-clicking the .dll, selecting "Properties", and then clicking "Unblock"') 16 | 17 | # import the C# classes for interfacing with the PPMS 18 | from QuantumDesign.QDInstrument import QDInstrumentBase, QDInstrumentFactory 19 | 20 | QDINSTRUMENT_TYPE = {'DynaCool': QDInstrumentBase.QDInstrumentType.DynaCool, 21 | 'PPMS': QDInstrumentBase.QDInstrumentType.PPMS, 22 | 'SVSM': QDInstrumentBase.QDInstrumentType.SVSM, 23 | 'VersaLab': QDInstrumentBase.QDInstrumentType.VersaLab, 24 | 'MPMS': 4121982} 25 | DEFAULT_PORT = 11000 26 | 27 | 28 | class QdInstrument: 29 | """A class to interface with Quantum Design instruments. 30 | 31 | This class is a thin wrapper around the C# QuantumDesign.QDInstrument.QDInstrumentBase class 32 | provided in the QDInstrument.dll file. 33 | 34 | There is now support for using a Quantum Design DynaCool, PPMS, SVSM, and VersaLab. The MPMS 35 | class requires testing, but should work. For some reason, the MPMS enum was hard-coded in 36 | as the number 4121982 casted as a QDInstrumentBase.QDInstrumentType enum. 37 | """ 38 | 39 | def __init__(self, instrument_type, ip_address, remote=True): 40 | self.qdi_instrument = QDInstrumentFactory.GetQDInstrument( 41 | QDINSTRUMENT_TYPE[instrument_type], remote, ip_address, DEFAULT_PORT) 42 | 43 | def getTemperature(self): 44 | """Returns the instrument temperature in Kelvin. 45 | 46 | Parameters are from: 47 | SetTemperature(double Temperature, double Rate, QDInstrumentBase.TemperatureApproach Approach) 48 | """ 49 | return self.qdi_instrument.GetTemperature(0, 0) 50 | 51 | def setTemperature(self, temp, rate=10): 52 | """Ramps the instrument temperature to the set point. 53 | 54 | Parameters are from: 55 | GetTemperature(ref double Temperature, ref QDInstrumentBase.TemperatureStatus Status) 56 | 57 | :param temp: Desired temperature in Kelvin 58 | :param rate: Temperature ramp rate in Kelvin/min. 59 | :return: None 60 | """ 61 | if 0 <= temp <= 400: 62 | return self.qdi_instrument.SetTemperature(temp, rate, 0) 63 | else: 64 | raise RuntimeError("Temperature is out of bounds. Should be between 0 and 400 K") 65 | 66 | def waitForTemperature(self, delay=5, timeout=600): 67 | """ 68 | Prevents other processes from executing while the QD instrument temperature 69 | is settling down. 70 | 71 | :param delay: Length of time to wait after wait condition achieved in seconds. 72 | :param timeout: Length of time to wait to achieve wait condition in seconds. 73 | :return: 0 when complete. 74 | """ 75 | return self.qdi_instrument.WaitFor(True, False, False, False, delay, timeout) 76 | 77 | def getField(self): 78 | """Returns the Magnetic field in Gauss. 79 | 80 | Parameters are from: 81 | GetField(ref double Field, ref QDInstrumentBase.FieldStatus Status) 82 | 83 | :return: Field in Gauss. 84 | """ 85 | return self.qdi_instrument.GetField(0, 0) 86 | 87 | def setField(self, field, rate=200): 88 | """Ramps the instrument magnetic field to the set point. 89 | 90 | Parameters are from: 91 | SetField(double Field, double Rate, QDInstrumentBase.FieldApproach Approach, QDInstrumentBase.FieldMode Mode) 92 | 93 | :param field: Set point of the applied magnetic field in Gauss. 94 | :param rate: Ramp rate of the applied magnetic field in Gauss/sec. 95 | :return: None 96 | """ 97 | return self.qdi_instrument.SetField(field, rate, 0, 0) 98 | 99 | def waitForField(self, delay=5, timeout=600): 100 | """ 101 | Prevents other processes from executing while the QD instrument magnetic field 102 | is settling down. 103 | 104 | :param delay: Length of time to wait after wait condition achieved in seconds. 105 | :param timeout: Length of time to wait to achieve wait condition in seconds. 106 | :return: 0 when complete. 107 | """ 108 | return self.qdi_instrument.WaitFor(False, True, False, False, delay, timeout) 109 | 110 | def getPosition(self): 111 | """Retrieves the position of the rotator. 112 | 113 | GetPosition(string Axis, ref double Position, ref QDInstrumentBase.PositionStatus Status) 114 | 115 | "Horizontal Rotator" seems to be the name that one should pass to GetPosition, as 116 | observed in the WaitConditionReached function. 117 | """ 118 | return self.qdi_instrument.GetPosition("Horizontal Rotator", 0, 0) 119 | 120 | def setPosition(self, position, speed): 121 | """Ramps the instrument position to the set point. 122 | 123 | Parameters are from: 124 | SetPosition(string Axis, double Position, double Speed, QDInstrumentBase.PositionMode Mode) 125 | 126 | :param position: Position on the rotator to move to. 127 | :param speed: Rate of change of position on the rotator. 128 | """ 129 | return self.qdi_instrument.SetPosition("Horizontal Rotator", position, speed, 0) 130 | 131 | def waitForPosition(self, delay, timeout): 132 | """ 133 | Prevents other processes from executing while the QD instrument rotator position 134 | is settling down. 135 | 136 | :param delay: Length of time to wait after wait condition achieved in seconds. 137 | :param timeout: Length of time to wait to achieve wait condition in seconds. 138 | :return: 0 when complete. 139 | """ 140 | return self.qdi_instrument.WaitFor(False, False, True, False, delay, timeout) 141 | 142 | 143 | class Dynacool(QdInstrument): 144 | """QdInstrument subclass that connects to the Quantum Design PPMS DynaCool.""" 145 | def __init__(self, ip_address): 146 | super().__init__(instrument_type='DynaCool', ip_address=ip_address) 147 | 148 | 149 | class Ppms(QdInstrument): 150 | """QdInstrument subclass that connects to the Quantum Design PPMS.""" 151 | def __init__(self, ip_address): 152 | super().__init__(instrument_type='PPMS', ip_address=ip_address) 153 | 154 | 155 | class Svsm(QdInstrument): 156 | """QdInstrument subclass that connects to the Quantum Design SVSM.""" 157 | def __init__(self, ip_address): 158 | super().__init__(instrument_type='SVSM', ip_address=ip_address) 159 | 160 | 161 | class VersaLab(QdInstrument): 162 | """QdInstrument subclass that connects to the Quantum Design VersaLab.""" 163 | def __init__(self, ip_address): 164 | super().__init__(instrument_type='VersaLab', ip_address=ip_address) 165 | 166 | 167 | class Mpms(QdInstrument): 168 | """QdInstrument subclass that connects to the Quantum Design MPMS.""" 169 | def __init__(self, ip_address): 170 | super().__init__(instrument_type='MPMS', ip_address=ip_address) 171 | -------------------------------------------------------------------------------- /labdrivers/srs/__init__.py: -------------------------------------------------------------------------------- 1 | from .sr830 import Sr830 2 | -------------------------------------------------------------------------------- /labdrivers/srs/sr830.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import visa 4 | 5 | # create a logger object for this module 6 | logger = logging.getLogger(__name__) 7 | # added so that log messages show up in Jupyter notebooks 8 | logger.addHandler(logging.StreamHandler()) 9 | 10 | 11 | class Sr830: 12 | """Interface to a Stanford Research Systems 830 lock in amplifier.""" 13 | 14 | def __init__(self, gpib_addr): 15 | """Create an instance of the Sr830 object. 16 | 17 | :param gpib_addr: GPIB address of the SR830 18 | """ 19 | try: 20 | # the pyvisa manager we'll use to connect to the GPIB resources 21 | self.resource_manager = visa.ResourceManager() 22 | except OSError: 23 | logger.exception("\n\tCould not find the VISA library. Is the VISA driver installed?\n\n") 24 | 25 | self._gpib_addr = gpib_addr 26 | self._instrument = None 27 | self._instrument = self.resource_manager.open_resource("GPIB::%d" % self._gpib_addr) 28 | 29 | @property 30 | def sync_filter(self): 31 | """ 32 | The state of the sync filter (< 200 Hz). 33 | """ 34 | return self._instrument.query_ascii_values('SYNC?')[0] 35 | 36 | @sync_filter.setter 37 | def sync_filter(self, value): 38 | if isinstance(value, bool): 39 | self._instrument.query_ascii_values('SYNC {}'.format(int(value))) 40 | else: 41 | raise RuntimeError('Sync filter input expects [True|False].') 42 | 43 | @property 44 | def low_pass_filter_slope(self): 45 | """ 46 | The low pass filter slope in units of dB/octave. The choices are: 47 | 48 | i slope(dB/oct) 49 | --- ------------- 50 | 0 6 51 | 1 12 52 | 2 18 53 | 3 24 54 | """ 55 | response = self._instrument.query_ascii_values('OSFL?')[0] 56 | slope = {'0': '6 dB/oct', '1': '12 dB/oct', '2': '18 dB/oct', '3': '24 dB/oct'} 57 | return slope[response] 58 | 59 | @low_pass_filter_slope.setter 60 | def low_pass_filter_slope(self, value): 61 | """ 62 | Sets the low pass filter slope. 63 | 64 | :param value: The slope in units of dB/oct. 65 | """ 66 | if value in (6, 12, 18, 24): 67 | slope = {6: '0', 12: '1', 18: '2', 24: '3'} 68 | self._instrument.query_ascii_values('OSFL {}'.format(slope[value])) 69 | else: 70 | raise RuntimeError('Low pass filter slope only accepts [6|12|18|24].') 71 | 72 | @property 73 | def reserve(self): 74 | """ 75 | The reserve mode of the SR830. 76 | """ 77 | reserve = {'0': 'high', '1': 'normal', '2': 'low noise'} 78 | response = self._instrument.query_ascii_values('RMOD?')[0] 79 | return reserve[response] 80 | 81 | @reserve.setter 82 | def reserve(self, value): 83 | if isinstance(value, str): 84 | mode = value.lower() 85 | elif isinstance(value, int): 86 | mode = value 87 | else: 88 | raise RuntimeError('Reserve expects a string or integer argument.') 89 | 90 | modes_dict = {'hi': 0, 'high': 0, 'high reserve': 0, 0: 0, 91 | 'normal': 1, 1: 1, 92 | 'lo': 2, 'low': 2, 'low noise': 2, 2: 2} 93 | if mode in modes_dict.keys(): 94 | self._instrument.query_ascii_values('RMOD {}'.format(mode)) 95 | else: 96 | raise RuntimeError('Incorrect key for reserve.') 97 | 98 | @property 99 | def frequency(self): 100 | """ 101 | The frequency of the output signal. 102 | """ 103 | return self._instrument.query_ascii_values('FREQ?')[0] 104 | 105 | @frequency.setter 106 | def frequency(self, value): 107 | if 0.001 <= value <= 102000: 108 | self._instrument.write("FREQ {}".format(value)) 109 | else: 110 | raise RuntimeError('Valid frequencies are between 0.001 Hz and 102 kHz.') 111 | 112 | # INPUT and FILTER 113 | 114 | @property 115 | def input(self): 116 | """ 117 | The input on the SR830 machine. Possible values: 118 | 0: A 119 | 1: A-B 120 | 2: I (1 MOhm) 121 | 3: I (100 MOhm) 122 | """ 123 | return self._instrument.query_ascii_values('ISRC?')[0] 124 | 125 | @input.setter 126 | def input(self, input_value): 127 | input_ = {'0': 0, 0: 0, 'A': 0, 128 | '1': 1, 1: 1, 'A-B': 1, 'DIFFERENTIAL': 1, 129 | '2': 2, 2: 2, 'I1': 2, 'I1M': 2, 'I1MOHM': 2, 130 | '3': 3, 3: 3, 'I100': 3, 'I100M': 3, 'I100MOHM': 3} 131 | if isinstance(input_value, str): 132 | query = input_value.upper().replace('(', '').replace(')', '').replace(' ', '') 133 | else: 134 | query = input_value 135 | 136 | if query in input_.keys(): 137 | command = input_[query] 138 | self._instrument.write("ISRC {}".format(command)) 139 | else: 140 | raise RuntimeError('Unexpected input for SR830 input command.') 141 | 142 | @property 143 | def input_shield_grounding(self): 144 | """Tells whether the shield is floating or grounded.""" 145 | response = self._instrument.query_ascii_values("IGND?")[0] 146 | return {'0': 'Float', '1': 'Ground'}[response] 147 | 148 | @input_shield_grounding.setter 149 | def input_shield_grounding(self, ground_type): 150 | ground_types = {'float': '0', 'floating': '0', '0': '0', 151 | 'ground': '1', 'grounded': '1', '1': '1'} 152 | if ground_type.lower() in ground_types.keys(): 153 | self._instrument.write("IGND {}".format(ground_type.lower())) 154 | else: 155 | raise RuntimeError('Improper input grounding shield type.') 156 | 157 | @property 158 | def phase(self): 159 | """ 160 | The phase of the output relative to the input. 161 | """ 162 | return self._instrument.query_ascii_values('PHAS?')[0] 163 | 164 | @phase.setter 165 | def phase(self, value): 166 | if (isinstance(value, float) or isinstance(value, int)) and -360.0 <= value <= 729.99: 167 | self._instrument.write("PHAS {}".format(value)) 168 | else: 169 | raise RuntimeError('Given phase is out of range for the SR830. Should be between -360.0 and 729.99.') 170 | 171 | @property 172 | def amplitude(self): 173 | """ 174 | The amplitude of the voltage output. 175 | """ 176 | return self._instrument.query_ascii_values('SLVL?')[0] 177 | 178 | @amplitude.setter 179 | def amplitude(self, value): 180 | if 0.004 <= value <= 5.0: 181 | self._instrument.write("SLVL {}".format(value)) 182 | else: 183 | raise RuntimeError('Given amplitude is out of range. Expected 0.004 to 5.0 V.') 184 | 185 | @property 186 | def time_constant(self): 187 | """ 188 | The time constant of the SR830. 189 | """ 190 | time_constant = {0: '10 us', 10: '1 s', 191 | 1: '30 us', 11: '3 s', 192 | 2: '100 us', 12: '10 s', 193 | 3: '300 us', 13: '30 s', 194 | 4: '1 ms', 14: '100 s', 195 | 5: '3 ms', 15: '300 s', 196 | 6: '10 ms', 16: '1 ks', 197 | 7: '30 ms', 17: '3 ks', 198 | 8: '100 ms', 18: '10 ks', 199 | 9: '300 ms', 19: '30 ks'} 200 | 201 | const_index = self._instrument.query_ascii_values('OFLT?')[0] 202 | return time_constant[const_index] 203 | 204 | @time_constant.setter 205 | def time_constant(self, value): 206 | if value.lower() == 'increment': 207 | if self.time_constant + 1 <= 19: 208 | self.time_constant += 1 209 | elif value.lower() == 'decrement': 210 | if self.time_constant - 1 >= 0: 211 | self.time_constant -= 1 212 | elif 0 <= value <= 19: 213 | self._instrument.write("SENS {}".format(value)) 214 | else: 215 | raise RuntimeError('Time constant index must be between 0 and 19 (inclusive).') 216 | 217 | @property 218 | def sensitivity(self): 219 | """Voltage/current sensitivity for inputs.""" 220 | sensitivity = {0: "2 nV/fA", 13: "50 uV/pA", 221 | 1: "5 nV/fA", 14: "100 uV/pA", 222 | 2: "10 nV/fA", 15: "200 uV/pA", 223 | 3: "20 nV/fA", 16: "500 uV/pA", 224 | 4: "50 nV/fA", 17: "1 mV/nA", 225 | 5: "100 nV/fA", 18: "2 mV/nA", 226 | 6: "200 nV/fA", 19: "5 mV/nA", 227 | 7: "500 nV/fA", 20: "10 mV/nA", 228 | 8: "1 uV/pA", 21: "20 mV/nA", 229 | 9: "2 uV/pA", 22: "50 mV/nA", 230 | 10: "5 uV/pA", 23: "100 mV/nA", 231 | 11: "10 uV/pA", 24: "200 mV/nA", 232 | 12: "20 uV/pA", 25: "500 mV/nA", 233 | 26: "1 V/uA"} 234 | 235 | sens_index = self._instrument.query_ascii_values('SENS?')[0] 236 | return sensitivity[sens_index] 237 | 238 | @sensitivity.setter 239 | def sensitivity(self, value): 240 | if isinstance(value, int) and 0 <= value <= 26: 241 | self._instrument.write("SENS {}".format(value)) 242 | else: 243 | raise RuntimeError("Invalid input for sensitivity.") 244 | 245 | def set_display(self, channel, display, ratio=0): 246 | """Set the display of the amplifier. 247 | 248 | Display options are: 249 | (for channel 1) (for channel 2) 250 | 0: X 0: Y 251 | 1: R 1: Theta 252 | 2: X Noise 2: Y Noise 253 | 3: Aux in 1 3: Aux in 3 254 | 4: Aux in 2 4: Aux in 4 255 | 256 | Ratio options are (i.e. divide output by): 257 | 0: none 0: none 258 | 1: Aux in 1 1: Aux in 3 259 | 2: Aux in 2 2: Aux in 4 260 | 261 | Args: 262 | channel (int): which channel to modify (1 or 2) 263 | display (int): what to display 264 | ratio (int, optional): display the output as a ratio 265 | """ 266 | self._instrument.write("DDEF {}, {}, {}".format(channel, display, ratio)) 267 | 268 | def get_display(self, channel): 269 | """Get the display configuration of the amplifier. 270 | 271 | Display options are: 272 | (for channel 1) (for channel 2) 273 | 0: X 0: Y 274 | 1: R 1: Theta 275 | 2: X Noise 2: Y Noise 276 | 3: Aux in 1 3: Aux in 3 277 | 4: Aux in 2 4: Aux in 4 278 | 279 | Args: 280 | channel (int): which channel to return the configuration for 281 | 282 | Returns: 283 | int: the parameter being displayed by the amplifier 284 | """ 285 | return self._instrument.query_ascii_values("DDEF? {}".format(channel)) 286 | 287 | def single_output(self, value): 288 | """Get the current value of a single parameter. 289 | Possible parameter values are: 290 | 1: X 291 | 2: Y 292 | 3: R 293 | 4: Theta 294 | 295 | Returns: 296 | float: the value of the specified parameter 297 | """ 298 | return self._instrument.query_ascii_values("OUTP? {}".format(value))[0] 299 | 300 | def multiple_output(self, *values): 301 | """Queries the SR830 for multiple output. See below for possibilities. 302 | 303 | Possible parameters are: 304 | 1: X 305 | 2: Y 306 | 3: R 307 | 4: Theta 308 | 5: Aux in 1 309 | 6: Aux in 2 310 | 7: Aux in 3 311 | 8: Aux in 4 312 | 9: Reference frequency 313 | 10: CH1 display 314 | 11: CH2 display 315 | 316 | :param values: A variable number of arguments to obtain output 317 | :return: 318 | """ 319 | 320 | command_string = "SNAP?" + " {}," * len(values) 321 | return self._instrument.query_ascii_values(command_string.format(*values)) 322 | 323 | def auto_gain(self): 324 | """ 325 | Mimics pressing the Auto Gain button. Does nothing if the time 326 | constant is more than 1 second. 327 | """ 328 | self._instrument.query_ascii_values("AGAN") 329 | 330 | def auto_reserve(self): 331 | """ 332 | Mimics pressing the Auto Reserve button. 333 | """ 334 | self._instrument.query_ascii_values("ARSV") 335 | 336 | def auto_phase(self): 337 | """ 338 | Mimics pressing the Auto Phase button. 339 | """ 340 | self._instrument.query_ascii_values("APHS") 341 | 342 | def auto_offset(self, parameter): 343 | """ 344 | Automatically offsets the given voltage parameter. 345 | 346 | :param parameter: A string from ['x'|'y'|'r'], case insensitive. 347 | """ 348 | self._instrument.query_ascii_values("AOFF {}".format(parameter.upper())) 349 | 350 | # Data storage commands 351 | 352 | @property 353 | def data_sample_rate(self): 354 | """Data sample rate, which can be 62.5 mHz, 512 Hz, or Trigger. 355 | 356 | Expected strings: 62.5, 62.5 mhz, 62.5mhz, mhz, 0, 512, 512hz, 512 hz, 357 | hz, 13, trig, trigger, 14.""" 358 | rate_dict = {'0': '62.5 mHz', '13': '512 Hz', '14': 'Trigger'} 359 | 360 | response = self._instrument.query_ascii_values("SRAT?")[0] 361 | return rate_dict[response] 362 | 363 | @data_sample_rate.setter 364 | def data_sample_rate(self, rate): 365 | rate_dict = {'62.5': '0', '0': '0', '62.5mhz': '0', 'mhz': '0', 366 | '512': '13', '13': '13', '512hz': '13', 'hz': '13', 367 | 'trig': '14', '14': '14', 'trigger': '14'} 368 | rate_value = str(rate).lower().replace(' ', '') 369 | if rate_value in rate_dict.keys(): 370 | self._instrument.write("SRAT {}".format(rate_value)) 371 | else: 372 | raise RuntimeError('Sample rate input not recognized.') 373 | 374 | @property 375 | def data_scan_mode(self): 376 | """Data scan mode, which is either a 1-shot or a loop. 377 | 378 | Expected strings: 1-shot, 1 shot, 1shot, loop.""" 379 | scan_modes = {'0': '1-shot', '1': 'loop'} 380 | response = self._instrument.query_ascii_values("SEND?")[0] 381 | return scan_modes[response] 382 | 383 | @data_scan_mode.setter 384 | def data_scan_mode(self, scan_mode): 385 | scan_modes = {'1shot': '0', 'loop': '1'} 386 | mode = scan_mode.replace('-', '').replace(' ', '') 387 | self._instrument.write("SEND {}".format(scan_modes[mode])) 388 | 389 | @property 390 | def trigger_starts_scan(self): 391 | """Determines if a Trigger starts scan mode.""" 392 | response = self._instrument.query_ascii_values("TSTR?")[0] 393 | return {'0': False, '1': True}[response] 394 | 395 | @trigger_starts_scan.setter 396 | def trigger_starts_scan(self, starts): 397 | starts_value = int(bool(starts)) 398 | self._instrument.write("TSTR {}".format(starts_value)) 399 | 400 | def trigger(self): 401 | """Sends a software trigger.""" 402 | self._instrument.write("TRIG") 403 | 404 | def start_scan(self): 405 | """Starts or continues a scan.""" 406 | self._instrument.write("STRT") 407 | 408 | def pause_scan(self): 409 | """Pauses a scan.""" 410 | self._instrument.write("PAUS") 411 | 412 | def reset_scan(self): 413 | """Resets a scan and releases all stored data.""" 414 | self._instrument.write("REST") 415 | -------------------------------------------------------------------------------- /labdrivers/version.py: -------------------------------------------------------------------------------- 1 | from os.path import join as pjoin 2 | 3 | # Format expected by setup.py and doc/source/conf.py: string of form "X.Y.Z" 4 | _version_major = 0 5 | _version_minor = 9 6 | _version_micro = 6 # use '' for first of series, number for 1 and above 7 | _version_extra = 'dev' 8 | # _version_extra = '' # Uncomment this for full releases 9 | 10 | # Construct full version string from these. 11 | _ver = [_version_major, _version_minor] 12 | if _version_micro: 13 | _ver.append(_version_micro) 14 | if _version_extra: 15 | _ver.append(_version_extra) 16 | 17 | __version__ = '.'.join(map(str, _ver)) 18 | 19 | CLASSIFIERS = ["Development Status :: 3 - Alpha", 20 | "Environment :: Console", 21 | "Intended Audience :: Science/Research", 22 | "License :: OSI Approved :: MIT License", 23 | "Operating System :: OS Independent", 24 | "Programming Language :: Python", 25 | "Topic :: Scientific/Engineering"] 26 | 27 | # Description should be a one-liner: 28 | description = "labdrivers: python drivers for lab instruments" 29 | # Long description will go up on the pypi page 30 | long_description = """ 31 | 32 | labdrivers 33 | ======== 34 | labdrivers is a collection of drivers for common research lab instruments. 35 | 36 | It contains a suite of instrument-specific drivers which can be used to 37 | interface measurement hardware with Python code, along with a set of 38 | Jupyter notebooks demonstrating example use cases. 39 | 40 | To get started using these components in your own software, please go to the 41 | repository README_. 42 | 43 | .. _README: https://github.com/masonlab/labdrivers/blob/master/README.md 44 | 45 | License 46 | ======= 47 | ``labdrivers`` is licensed under the terms of the MIT license. See the file 48 | "LICENSE" for information on the history of this software, terms & conditions 49 | for usage, and a DISCLAIMER OF ALL WARRANTIES. 50 | 51 | All trademarks referenced herein are property of their respective holders. 52 | 53 | Copyright (c) 2016--, Henry Hinnefeld. 54 | """ 55 | 56 | NAME = "labdrivers" 57 | MAINTAINER = "Jeff Damasco" 58 | MAINTAINER_EMAIL = "jeffdamasco@gmail.com" 59 | DESCRIPTION = description 60 | LONG_DESCRIPTION = long_description 61 | URL = "http://github.com/masonlab/labdrivers" 62 | DOWNLOAD_URL = "" 63 | LICENSE = "MIT" 64 | AUTHOR = "Henry Hinnefeld" 65 | AUTHOR_EMAIL = "henry.hinnefeld@gmail.com" 66 | PLATFORMS = "OS Independent" 67 | MAJOR = _version_major 68 | MINOR = _version_minor 69 | MICRO = _version_micro 70 | VERSION = __version__ 71 | PACKAGES = ['labdrivers', 72 | 'labdrivers.keithley', 73 | 'labdrivers.lakeshore', 74 | 'labdrivers.srs', 75 | 'labdrivers.quantumdesign', 76 | 'labdrivers.oxford', 77 | 'labdrivers.ni'] 78 | PACKAGE_DATA = {'labdrivers': [pjoin('data', '*')]} 79 | REQUIRES = ["pyvisa", "PyDAQmx"] 80 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | try: 3 | from setuptools import setup 4 | except ImportError: 5 | from distutils.core import setup 6 | 7 | # Get version and release info, which is all stored in labdrivers/version.py 8 | ver_file = os.path.join('labdrivers', 'version.py') 9 | with open(ver_file) as f: 10 | exec(f.read()) 11 | 12 | opts = dict(name=NAME, 13 | maintainer=MAINTAINER, 14 | maintainer_email=MAINTAINER_EMAIL, 15 | description=DESCRIPTION, 16 | long_description=LONG_DESCRIPTION, 17 | url=URL, 18 | download_url=DOWNLOAD_URL, 19 | license=LICENSE, 20 | classifiers=CLASSIFIERS, 21 | author=AUTHOR, 22 | author_email=AUTHOR_EMAIL, 23 | platforms=PLATFORMS, 24 | version=VERSION, 25 | packages=PACKAGES, 26 | package_data=PACKAGE_DATA, 27 | install_requires=REQUIRES) 28 | 29 | 30 | if __name__ == '__main__': 31 | setup(**opts) 32 | -------------------------------------------------------------------------------- /tests/test_keithley2400.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from labdrivers.keithley.keithley2400 import keithley2400 4 | 5 | KEITHLEY_GPIB_ADDR = 23 6 | 7 | 8 | def test_source_type(): 9 | sourcemeter = keithley2400(KEITHLEY_GPIB_ADDR) 10 | sourcemeter.enable_remote() 11 | 12 | sourcemeter.source_type = 'voltage' 13 | assert sourcemeter.source_type == 'voltage' 14 | sourcemeter.source_type = 'current' 15 | assert sourcemeter.source_type == 'current' 16 | 17 | with pytest.raises(RuntimeError): 18 | sourcemeter.source_type = 'ham' 19 | 20 | 21 | -------------------------------------------------------------------------------- /tests/test_sr830.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from labdrivers.srs.sr830 import sr830 4 | 5 | LOCKIN_GPIB_ADDR = 8 6 | 7 | 8 | def test_sync_filter(): 9 | lock_in = sr830(LOCKIN_GPIB_ADDR) 10 | 11 | lock_in.sync_filter = True 12 | assert int(lock_in.sync_filter) == 1 13 | lock_in.sync_filter = False 14 | assert int(lock_in.sync_filter) == 0 15 | 16 | with pytest.raises(RuntimeError): 17 | lock_in.sync_filter = 'ham' 18 | 19 | 20 | def test_low_pass_filter_slope(): 21 | lock_in = sr830(LOCKIN_GPIB_ADDR) 22 | 23 | lock_in.low_pass_filter_slope = 6 24 | assert int(lock_in.low_pass_filter_slope) == 0 25 | lock_in.low_pass_filter_slope = 12 26 | assert int(lock_in.low_pass_filter_slope) == 1 27 | lock_in.low_pass_filter_slope = 18 28 | assert int(lock_in.low_pass_filter_slope) == 2 29 | lock_in.low_pass_filter_slope = 24 30 | assert int(lock_in.low_pass_filter_slope) == 3 31 | 32 | with pytest.raises(RuntimeError): 33 | lock_in.low_pass_filter_slope = 100 34 | 35 | 36 | def test_reserve(): 37 | lock_in = sr830(LOCKIN_GPIB_ADDR) 38 | 39 | lock_in.reserve = 'hi' 40 | assert int(lock_in.reserve) == 0 41 | lock_in.reserve = 'high' 42 | assert int(lock_in.reserve) == 0 43 | lock_in.reserve = 'high reserve' 44 | assert int(lock_in.reserve) == 0 45 | lock_in.reserve = 0 46 | assert int(lock_in.reserve) == 0 47 | 48 | lock_in.reserve = 'normal' 49 | assert int(lock_in.reserve) == 1 50 | lock_in.reserve = 1 51 | assert int(lock_in.reserve) == 1 52 | 53 | lock_in.reserve = 'lo' 54 | assert int(lock_in.reserve) == 2 55 | lock_in.reserve = 'low' 56 | assert int(lock_in.reserve) == 2 57 | lock_in.reserve = 'low noise' 58 | assert int(lock_in.reserve) == 2 59 | lock_in.reserve = 2 60 | assert int(lock_in.reserve) == 2 61 | 62 | with pytest.raises(RuntimeError): 63 | lock_in.reserve = 'eggs' 64 | 65 | 66 | def test_frequency(): 67 | lock_in = sr830(LOCKIN_GPIB_ADDR) 68 | 69 | lock_in.frequency = 23.33 70 | assert lock_in.frequency == 23.33 71 | 72 | with pytest.raises(RuntimeError): 73 | lock_in.frequency = -23.33 74 | 75 | 76 | def test_input(): 77 | lock_in = sr830(LOCKIN_GPIB_ADDR) 78 | 79 | lock_in.input = '0' 80 | assert lock_in.input == '0' 81 | lock_in.input = 0 82 | assert lock_in.input == '0' 83 | lock_in.input = 'a' 84 | assert lock_in.input == '0' 85 | lock_in.input = 'A' 86 | assert lock_in.input == '0' 87 | 88 | lock_in.input = '1' 89 | assert lock_in.input == '1' 90 | lock_in.input = 1 91 | assert lock_in.input == '1' 92 | lock_in.input = 'a-b' 93 | assert lock_in.input == '1' 94 | lock_in.input = 'A-b' 95 | assert lock_in.input == '1' 96 | lock_in.input = 'differential' 97 | assert lock_in.input == '1' 98 | 99 | lock_in.input = '2' 100 | assert lock_in.input == '2' 101 | lock_in.input = 2 102 | assert lock_in.input == '2' 103 | lock_in.input = 'i1' 104 | assert lock_in.input == '2' 105 | lock_in.input = 'i1m' 106 | assert lock_in.input == '2' 107 | lock_in.input = 'I1MOHM' 108 | assert lock_in.input == '2' 109 | 110 | lock_in.input = '3' 111 | assert lock_in.input == '3' 112 | lock_in.input = 3 113 | assert lock_in.input == '3' 114 | lock_in.input = 'i100' 115 | assert lock_in.input == '3' 116 | lock_in.input = 'i100m' 117 | assert lock_in.input == '3' 118 | lock_in.input = 'i100mohm' 119 | assert lock_in.input == '3' 120 | 121 | 122 | def test_phase(): 123 | lock_in = sr830(LOCKIN_GPIB_ADDR) 124 | 125 | with pytest.raises(RuntimeError): 126 | lock_in.phase = -720.0 127 | 128 | lock_in.phase = 128.0 129 | assert lock_in.phase == 128.0 130 | 131 | 132 | def test_amplitude(): 133 | lock_in = sr830(LOCKIN_GPIB_ADDR) 134 | 135 | with pytest.raises(RuntimeError): 136 | lock_in.amplitude = 5.002 137 | 138 | with pytest.raises(RuntimeError): 139 | lock_in.amplitude = 0.0 140 | 141 | lock_in.amplitude = 0.100 142 | assert lock_in.amplitude == 0.100 143 | -------------------------------------------------------------------------------- /tests/test_triton200.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from labdrivers.oxford import triton200 4 | 5 | def test_temperature_channel(): 6 | fridge = triton200("""ip_address""") 7 | assert fridge.temperature_channel != 5 8 | assert fridge.temperature_channel == '5' 9 | fridge.temperature_channel = 6 10 | assert fridge.temperature_channel != 6 11 | assert fridge.temperature_channel == '6' 12 | 13 | 14 | def test_temperature_setpoint(): 15 | fridge --------------------------------------------------------------------------------