diff --git a/.coveragerc b/.coveragerc index 426ca9de8..650e7e628 100644 --- a/.coveragerc +++ b/.coveragerc @@ -9,3 +9,6 @@ exclude_lines = if not self._testing: raise NotImplementedError raise AssertionError + +[run] +relative_files = True diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 000000000..c55dde561 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,28 @@ +name: Upload Python Package + +on: + release: + types: [published] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip virtualenv + pip install build + - name: Build package + run: python -m build + - name: Publish package + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..84a23c03b --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,67 @@ +name: Testing + +on: + push: + branches: [ master ] + pull_request: + +jobs: + static-checks: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: pip install --upgrade pre-commit + - name: Run static checks via pre-commit + run: SKIP=no-commit-to-branch pre-commit run --all --show-diff-on-failure + test: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - python-version: 3.6 + TOXENV: "py36" + - python-version: 3.7 + TOXENV: "py37" + - python-version: 3.8 + TOXENV: "py38" + - python-version: 3.9 + TOXENV: "py39" + - python-version: 3.9 + TOXENV: "py39-numpy" + - python-version: "3.10" + TOXENV: "py310" + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: pip install --upgrade pip setuptools wheel virtualenv tox + - name: Test with tox + env: + TOXENV: ${{ matrix.TOXENV }} + run: tox + - name: Submit to coveralls + uses: AndreMiras/coveralls-python-action@develop + with: + parallel: true + github-token: ${{ secrets.github_token }} + + coverage: + needs: test + runs-on: ubuntu-latest + steps: + - name: Coveralls Finished + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.github_token }} + parallel-finished: true diff --git a/.gitignore b/.gitignore index 3f7a79c94..9c71cd94b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,10 @@ Thumbs.db ## Build directories ## doc/_build +## Venv +.venv/ +.python-version + ## setup.py generated files ## MANIFEST @@ -20,6 +24,7 @@ MANIFEST *.so # Packages +.egg/ *.egg *.egg-info dist @@ -41,6 +46,7 @@ pip-log.txt .coverage .tox nosetests.xml +.pytest_cache #Translations *.mo @@ -50,3 +56,15 @@ nosetests.xml #pycharm generated .idea + +# VS Code IDE internals +.vscode/ + +# nosetests metadata +.noseids + +# Hypothesis files +.hypothesis/ + +# version file generated by setuptools_scm +instruments/_version.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..153c095d8 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,19 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.1.0 + hooks: + - id: no-commit-to-branch + args: [--branch, master] + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: debug-statements + - repo: https://github.com/psf/black + rev: 21.12b0 + hooks: + - id: black + - repo: https://github.com/asottile/pyupgrade + rev: v2.31.0 + hooks: + - id: pyupgrade + args: [ --py36-plus ] diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index 0cb0a5c85..000000000 --- a/.pylintrc +++ /dev/null @@ -1,377 +0,0 @@ -[MASTER] - -# Specify a configuration file. -#rcfile= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore=CVS - -# Pickle collected data for later comparisons. -persistent=yes - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins= - -# Use multiple processes to speed up Pylint. -jobs=1 - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code -extension-pkg-whitelist= - -# Allow optimization of some AST trees. This will activate a peephole AST -# optimizer, which will apply various small optimizations. For instance, it can -# be used to obtain the result of joining multiple strings with the addition -# operator. Joining a lot of strings can lead to a maximum recursion error in -# Pylint and this flag can prevent that. It has one side effect, the resulting -# AST will be different than the one from reality. -optimize-ast=no - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED -confidence= - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time. See also the "--disable" option for examples. -#enable= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once).You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use"--disable=all --enable=classes -# --disable=W" -disable=import-star-module-level,old-octal-literal,oct-method,print-statement,unpacking-in-except,parameter-unpacking,backtick,old-raise-syntax,old-ne-operator,long-suffix,dict-view-method,dict-iter-method,metaclass-assignment,next-method-called,raising-string,indexing-exception,raw_input-builtin,long-builtin,file-builtin,execfile-builtin,coerce-builtin,cmp-builtin,buffer-builtin,basestring-builtin,apply-builtin,filter-builtin-not-iterating,using-cmp-argument,useless-suppression,range-builtin-not-iterating,suppressed-message,no-absolute-import,old-division,cmp-method,reload-builtin,zip-builtin-not-iterating,intern-builtin,unichr-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,input-builtin,round-builtin,hex-method,nonzero-method,map-builtin-not-iterating,redefined-builtin,invalid-name,unsubscriptable-object,too-few-public-methods,too-many-public-methods,fixme,duplicate-code - -[REPORTS] - -# Set the output format. Available formats are text, parseable, colorized, msvs -# (visual studio) and html. You can also give a reporter class, eg -# mypackage.mymodule.MyReporterClass. -output-format=text - -# Put messages in a separate file for each module / package specified on the -# command line instead of printing them on stdout. Reports (if any) will be -# written in a file name "pylint_global.[txt|html]". -files-output=no - -# Tells whether to display a full report or only the messages -reports=yes - -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details -#msg-template= - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME,XXX,TODO - - -[LOGGING] - -# Logging modules to check that the string format arguments are in logging -# function parameter format -logging-modules=logging - - -[TYPECHECK] - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis. It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= - -# List of classes names for which member attributes should not be checked -# (useful for classes with attributes dynamically set). This supports can work -# with qualified names. -ignored-classes= - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - - -[SPELLING] - -# Spelling dictionary name. Available dictionaries: none. To make it working -# install python-enchant package. -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to indicated private dictionary in -# --spelling-private-dict-file option instead of raising a message. -spelling-store-unknown-words=no - - -[FORMAT] - -# Maximum number of characters on a single line. -max-line-length=100 - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma,dict-separator - -# Maximum number of lines in a module -max-module-lines=1000 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - - -[SIMILARITIES] - -# Minimum lines number of a similarity. -min-similarity-lines=10 - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=yes - - -[VARIABLES] - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# A regular expression matching the name of dummy variables (i.e. expectedly -# not used). -dummy-variables-rgx=_$|dummy - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid to define new builtins when possible. -additional-builtins= - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_,_cb - - -[BASIC] - -# List of builtins function names that should not be used, separated by a comma -bad-functions=map,filter,input - -# Good variable names which should always be accepted, separated by a comma -good-names=i,j,k,ex,Run,_ - -# Bad variable names which should always be refused, separated by a comma -bad-names=foo,bar,baz,toto,tutu,tata - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Include a hint for the correct naming format with invalid-name -include-naming-hint=no - -# Regular expression matching correct function names -function-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for function names -function-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct variable names -variable-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for variable names -variable-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct constant names -const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ - -# Naming hint for constant names -const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ - -# Regular expression matching correct attribute names -attr-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for attribute names -attr-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct argument names -argument-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for argument names -argument-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct class attribute names -class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ - -# Naming hint for class attribute names -class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ - -# Regular expression matching correct inline iteration names -inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ - -# Naming hint for inline iteration names -inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ - -# Regular expression matching correct class names -class-rgx=[A-Z_][a-zA-Z0-9]+$ - -# Naming hint for class names -class-name-hint=[A-Z_][a-zA-Z0-9]+$ - -# Regular expression matching correct module names -module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - -# Naming hint for module names -module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - -# Regular expression matching correct method names -method-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for method names -method-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_|^test|^Mock|^mock|^Test - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=2 - - -[ELIF] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - - -[DESIGN] - -# Maximum number of arguments for function / method -max-args=5 - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore -ignored-argument-names=_.* - -# Maximum number of locals for function / method body -max-locals=15 - -# Maximum number of return / yield for function / method body -max-returns=6 - -# Maximum number of branch for function / method body -max-branches=12 - -# Maximum number of statements in function / method body -max-statements=50 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of attributes for a class (see R0902). -max-attributes=10 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of boolean expressions in a if statement -max-bool-expr=5 - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__,__new__,setUp - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=mcs - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict,_fields,_replace,_source,_make - - -[IMPORTS] - -# Deprecated modules which should not be used, separated by a comma -deprecated-modules=regsub,TERMIOS,Bastion,rexec - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled) -import-graph= - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled) -ext-import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled) -int-import-graph= - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "Exception" -overgeneral-exceptions=Exception diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 000000000..a08b5496d --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,15 @@ +version: 2 +build: + os: ubuntu-20.04 + tools: + python: "3.9" + +sphinx: + builder: html + configuration: doc/source/conf.py + fail_on_warning: false + +python: + install: + - method: pip + path: . diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 7cbb7d02b..000000000 --- a/.travis.yml +++ /dev/null @@ -1,25 +0,0 @@ -sudo: false -language: python -python: - - "2.7" - - "3.3" - - "3.4" - - "3.5" -install: - - "pip install -r requirements.txt" - - "pip install -r dev-requirements.txt" - - pip install python-coveralls - - pip install coverage -script: - - nosetests --with-coverage -w instruments - - pylint --py3k instruments/ - - pylint instruments/ -after_success: - - coveralls -deploy: - provider: pypi - user: ${PYPI_USERNAME} - password: ${PYPI_PASSWORD} - distributions: "sdist bdist_wheel" - on: - tags: true diff --git a/README.rst b/README.rst index 67bbd6c3c..42c6ceb29 100644 --- a/README.rst +++ b/README.rst @@ -1,12 +1,12 @@ InstrumentKit ============= -.. image:: https://img.shields.io/travis/Galvant/InstrumentKit.svg?maxAge=2592000 - :target: https://travis-ci.org/Galvant/InstrumentKit - :alt: Travis-CI build status +.. image:: https://github.com/Galvant/InstrumentKit/workflows/Testing/badge.svg?branch=master + :target: https://github.com/Galvant/InstrumentKit + :alt: Github Actions build status -.. image:: https://img.shields.io/coveralls/Galvant/InstrumentKit/dev.svg?maxAge=2592000 - :target: https://coveralls.io/r/Galvant/InstrumentKit?branch=dev +.. image:: https://img.shields.io/coveralls/Galvant/InstrumentKit/master.svg?maxAge=2592000 + :target: https://coveralls.io/github/Galvant/InstrumentKit?branch=master :alt: Coveralls code coverage .. image:: https://readthedocs.org/projects/instrumentkit/badge/?version=latest @@ -38,6 +38,7 @@ Supported means of communication are: - Read/write from unix files (``open_file``) - USBTMC (``open_usbtmc``) - VXI11 over Ethernet (``open_vxi11``) +- Raw USB (``open_usb``) There is planned support for HiSLIP someday, but a good Python HiSLIP library will be needed first. @@ -97,7 +98,7 @@ measurement reading: .. code-block:: python >>> reading = inst.measure(inst.Mode.voltage_dc) - >>> print("Value: {}, units: {}".format(reading.magnitude, reading.units)) + >>> print(f"Value: {reading.magnitude}, units: {reading.units}") Due to the sheer number of commands most instruments support, not every single one is included in InstrumentKit. If there is a specific command you wish to @@ -111,7 +112,7 @@ send, one can use the following functions to do so: Python Version Compatibility ---------------------------- -At this time, Python 2.7, 3.3, 3.4, and 3.5 are supported. Should you encounter +At this time, Python 3.6, 3.7, 3.8, 3.9, and 3.10 are supported. Should you encounter any problems with this library that occur in one version or another, please do not hesitate to let us know. @@ -131,10 +132,8 @@ existing classes which are similar to your work to learn more about the structure of this project. To run the tests against all supported version of Python, you will need to -have the binary for each installed, as well as any requirements needed to -install ``numpy`` under each Python version. On Debian/Ubuntu systems this means -you will need to install the ``python-dev`` package for each version of Python -supported (``python2.7-dev``, ``python3.3-dev``, etc). +have the binary for each installed. The easiest way to accomplish this is +to use the tool `pyenv_`. With the required system packages installed, all tests can be run with ``tox``: @@ -143,8 +142,42 @@ With the required system packages installed, all tests can be run with ``tox``: $ pip install tox $ tox +Pre-commit +---------- + +A variety of static code checks are managed and executed via the tool +`pre-commit_`. This only needs to be setup once +and then it'll manage everything for you. + +.. code-block:: console + + $ pip install pre-commit + $ pre-commit install + +Afterwards, when you go to make a git commit, all the plugins (as specified +by the configuration file ``.pre-commit-config.yaml``) will be executed against +the files that have changed. If any plugins make changes to the files, the +commit will abort, allowing you to add those changes to your changeset and +try to commit again. This tool will gate CI, so be sure to let them run +and pass! + +You can also run all the hooks against all the files by directly calling +pre-commit, or though the ``tox`` environment: + +.. code-block:: console + + $ pre-commit run --all + +or + +.. code-block:: console + + $ tox -e precommit + +See the ``pre-commit`` documentation for more information. + License ------- All code in this repository is released under the AGPL-v3 license. Please see -the ``license`` folder for more information. \ No newline at end of file +the ``license`` folder for more information. diff --git a/dev-requirements.txt b/dev-requirements.txt deleted file mode 100644 index 7418bc754..000000000 --- a/dev-requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -mock -nose -pylint diff --git a/doc/examples/.ipynb_checkpoints/ex_keithley6514-checkpoint.ipynb b/doc/examples/.ipynb_checkpoints/ex_keithley6514-checkpoint.ipynb index 57d28106c..504959281 100644 --- a/doc/examples/.ipynb_checkpoints/ex_keithley6514-checkpoint.ipynb +++ b/doc/examples/.ipynb_checkpoints/ex_keithley6514-checkpoint.ipynb @@ -168,4 +168,4 @@ "metadata": {} } ] -} \ No newline at end of file +} diff --git a/doc/examples/ex_generic_scpi.ipynb b/doc/examples/ex_generic_scpi.ipynb index fbb694469..7e5e1f3e5 100644 --- a/doc/examples/ex_generic_scpi.ipynb +++ b/doc/examples/ex_generic_scpi.ipynb @@ -97,4 +97,4 @@ "metadata": {} } ] -} \ No newline at end of file +} diff --git a/doc/examples/ex_generic_scpi.py b/doc/examples/ex_generic_scpi.py index f973fe46a..8190df806 100644 --- a/doc/examples/ex_generic_scpi.py +++ b/doc/examples/ex_generic_scpi.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # 3.0 # @@ -11,7 +10,7 @@ # -# In this example, we will demonstrate how to connect to a generic SCPI +# In this example, we will demonstrate how to connect to a generic SCPI # instrument and query its identification information. # @@ -24,7 +23,7 @@ # -# Next, we open our connection to the instrument. Here we use the generic +# Next, we open our connection to the instrument. Here we use the generic # SCPIInstrument class and open the connection using the Galvant Industries' # GPIBUSB adapter. Our connection is made to the virtual serial port located at # /dev/ttyUSB0 and GPIB address 1 @@ -35,7 +34,7 @@ # -inst = ik.generic_scpi.SCPIInstrument.open_gpibusb('/dev/ttyUSB0', 1) +inst = ik.generic_scpi.SCPIInstrument.open_gpibusb("/dev/ttyUSB0", 1) # @@ -44,5 +43,4 @@ # -print inst.name - +print(inst.name) diff --git a/doc/examples/ex_hp3456.py b/doc/examples/ex_hp3456.py index 1ddbe5884..441b3289d 100644 --- a/doc/examples/ex_hp3456.py +++ b/doc/examples/ex_hp3456.py @@ -1,14 +1,11 @@ #!/usr/bin/python -# -*- coding: utf-8 -*- - -from __future__ import absolute_import, print_function import logging import time import instruments as ik -import quantities as pq +import instruments.units as u -dmm = ik.hp.HP3456a.open_gpibusb('/dev/ttyUSB0', 22) +dmm = ik.hp.HP3456a.open_gpibusb("/dev/ttyUSB0", 22) logging.basicConfig(level=logging.DEBUG) dmm._file.debug = True dmm.trigger_mode = dmm.TriggerMode.hold @@ -30,10 +27,10 @@ # Read registers dmm.nplc = 10 -print("n = {}".format(dmm.number_of_readings)) -print("g = {}".format(dmm.number_of_digits)) -print("p = {}".format(dmm.nplc)) -print("d = {}".format(dmm.delay)) +print(f"n = {dmm.number_of_readings}") +print(f"g = {dmm.number_of_digits}") +print(f"p = {dmm.nplc}") +print(f"d = {dmm.delay}") print(dmm.mean) print(dmm.variance) print(dmm.count) @@ -50,7 +47,7 @@ print(dmm.measure(dmm.Mode.resistance_2wire)) dmm.nplc = 1 for i in range(-1, 4): - value = (10 ** i) * pq.volt + value = (10 ** i) * u.volt dmm.input_range = value print(dmm.measure(dmm.Mode.dcv)) @@ -74,4 +71,3 @@ print(dmm.measure(dmm.Mode.dcv)) dmm.autozero = 0 print(dmm.measure(dmm.Mode.dcv)) - diff --git a/doc/examples/ex_keithley195.ipynb b/doc/examples/ex_keithley195.ipynb index 804fff576..87ee0dcaa 100644 --- a/doc/examples/ex_keithley195.ipynb +++ b/doc/examples/ex_keithley195.ipynb @@ -89,4 +89,4 @@ "metadata": {} } ] -} \ No newline at end of file +} diff --git a/doc/examples/ex_keithley195.py b/doc/examples/ex_keithley195.py index 82546ce02..a9d4027a6 100644 --- a/doc/examples/ex_keithley195.py +++ b/doc/examples/ex_keithley195.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # 3.0 # @@ -24,14 +23,14 @@ # -# Next, we open our connection to the instrument. Here we use the +# Next, we open our connection to the instrument. Here we use the # Keithley195 class and open the connection using Galvant Industries' # GPIBUSB adapter. Our connection is made to the virtual serial port located at # /dev/ttyUSB0 and GPIB address 16. # -dmm = ik.keithley.Keithley195.open_gpibusb('/dev/ttyUSB0', 1) +dmm = ik.keithley.Keithley195.open_gpibusb("/dev/ttyUSB0", 1) # @@ -39,5 +38,4 @@ # -print dmm.measure() - +print(dmm.measure()) diff --git a/doc/examples/ex_keithley6514.ipynb b/doc/examples/ex_keithley6514.ipynb index 6dc47376f..4d6b6397d 100644 --- a/doc/examples/ex_keithley6514.ipynb +++ b/doc/examples/ex_keithley6514.ipynb @@ -312,4 +312,4 @@ "metadata": {} } ] -} \ No newline at end of file +} diff --git a/doc/examples/ex_maui.ipynb b/doc/examples/ex_maui.ipynb new file mode 100644 index 000000000..dce96fca7 --- /dev/null +++ b/doc/examples/ex_maui.ipynb @@ -0,0 +1,862 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# MAUI Oscilloscope controller\n", + "\n", + "The middle to high-end Teledyne-LeCroy oscilloscope come with the MAUI (Most Advanced User Interface) control interface. Each of these MAUI-enabled scopes can be controlled in the same way, assuming they have the same functionality, etc. The `MAUI` class presents a control interface to remotely access and setup an oscilloscope. Not every functionality is incorporated at this point, but the most imporant and basic ones are, i.e.:\n", + " * General Oscilloscope controls, i.e., triggering\n", + " * Channels\n", + " * Math functions\n", + " * Measurement setup and data retrieval\n", + " * Waveform retrieval\n", + "Here, some detailed examples for various applications are shown. \n", + "\n", + "## Communications\n", + "These Oscilloscopes have many different ways of communicating with the host computer. This class only supports the `LXI (VXI11)` protocol, which should come by default on theses oscilloscopes. The reason for this is that this protocoll supports the NI-VISA protocol, which can be completely replaced with the open PyVISA. Thus the oscilloscope can be controlled from any OS, in fact, most of the development have taken place on Linux. *Note*: The scope that the software was developed with is an older wavesurfer 3054, which was at least supposed to support the `LXI (VXI11)` protocol. However, it could not be activated. After contacting Teledyne-LeCroy, they responded fairly quickly and sent an activation code to enable the protocol, free of charge.\n", + "\n", + "In order to successfully communicate with the oscilloscope, PyVISA requires the [pyvisa-py](https://pyvisa-py.readthedocs.io/en/latest/) backend. This should be the requirements for the package now. If not or not yet installed on your setup, you can install it by typing:\n", + "\n", + " pip install pyvisa-py\n", + "\n", + "## Importing the pre-requisites\n", + "First let's import some packages that we'll need, mostly instrumentkit of course :)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import sys, os\n", + "\n", + "# if you run this script from a cloned InstrumentKit path without a full installation, leave the following line in\n", + "sys.path.insert(0, os.path.abspath('../../'))\n", + "\n", + "# import the instrument kit\n", + "import instruments as ik\n", + "import instruments.units as u\n", + "\n", + "# imports for specific functions in this script\n", + "import matplotlib.pyplot as plt\n", + "from time import sleep" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Enabling the oscilloscope\n", + "\n", + "First let us look at how to establish communications with the oscilloscope. ON the oscilloscope itself, go to `Utilities` -> `Utilities Setup` -> `Remote` and select on the left side the `LXI (VXI11)` communications protocol. Connect the oscilloscope to your local area network and check it's IP address. The example IP address that will be used here is `192.168.8.154`.\n", + "\n", + "Then you can load the oscilloscope and enable communications in the following way:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "inst = ik.teledyne.MAUI.open_visa(\"TCPIP0::192.168.0.10::INSTR\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Specifying your oscilloscope setup\n", + "\n", + "The MAUI interface works for mulitple different Teledyne-LeCroy oscilloscopes. Not all of these oscilloscopes will have the same options, so some commands might not be available on your scope. To make the oscilloscope controller versatile, the number of available channels (default 4), available functions (default 2), and available measurements (default 6) can be adjusted. The number of channels is simply how many inputs are available on the front. The number of functions is how many functions can be set up in the scopes math menu, usually labeled as `F1`, ... `Fn` in th oscilloscope software. The number of available measurements is the number of measurements that can be configured on the scope, usually labeled as `P1`, ... `Pn`." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "4" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# setting and getting the number of channels\n", + "inst.number_channels = 4\n", + "inst.number_channels" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "2" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# setting and getting the number of functions\n", + "inst.number_functions = 2\n", + "inst.number_functions" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "6" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# setting and getting the number of measurements\n", + "inst.number_measurements = 6\n", + "inst.number_measurements" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Triggering the scope\n", + "\n", + "The simplest possible way to stop and start the oscilloscope from triggering is as following:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "# stop the oscilloscope from triggering\n", + "inst.stop()" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "# start the oscilloscope in automatic triggering mode\n", + "inst.run()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "However, the four triggering states can also be controlled manually. These states are: automatic triggering `auto`, normal triggering `normal`, a single trigger `single` and no triggering `stop`. These trigger states are implemented as a `inst.TriggerState` subclass under the instrument class. Reading the trigger state (should be `auto` from just before) and then setting it to `normal` can be done in the following way:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# get the current trigger state\n", + "inst.trigger_state" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "# restart the trigger with a single trigger\n", + "inst.trigger_state = inst.TriggerState.normal" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In addition, e.g., for a measurement, a trigger can also be forced upon request. For this to work, set the oscilloscope into stop mode, then force a trigger." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "# Stop the triggering\n", + "inst.stop()\n", + "\n", + "# A trigger can also be forced by calling:\n", + "inst.force_trigger()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The oscilloscope will be put back into stopped mode. To continue triggering in normal mode, run:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "inst.trigger_state = inst.TriggerState.normal" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In addition to selecting the triggering state, the triggering source, type, and level can also be chosen. For most oscilloscopes, all channels and an external triggering source can be chosen from, optional settings are possible. Possible triggering sources are stored in the `enum` class `TriggerSource`, while triggering types are stored in the `TriggerType` `enum` class.\n", + "\n", + "Let's set the triggering source to the external trigger and trigger on the edge. This can be accomplished with the following commands:" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "inst.trigger_source = inst.TriggerSource.ext\n", + "inst.trigger_type = inst.TriggerType.edge" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Time base\n", + "\n", + "The timebase is the same for all channels and therefore implemented on the instrument level. Setting the timebase of the scope expects a unitful value. If no units are given, seconds are assumed. To set the time per division to 20 ns and read it back out, run the following commands." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array(2.e-08) * s" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "inst.time_div = u.Quantity(20, u.ns)\n", + "inst.time_div" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To shift the timebase with respect to the trigger, a trigger delay can be called. This call is unitful as well. To set a trigger delay of 60 ns and read it back, run the following command." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array(6.e-08) * s" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "inst.trigger_delay = u.Quantity(60, u.ns)\n", + "inst.trigger_delay" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Controlling a channel\n", + "\n", + "To control a channel, several functions are implemented. The first channel is referred to as `0`, as is common in python. To create an instance of the first channel you can run:" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "channel = inst.channel[0]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Turning a trace on and off can be done by setting the `channel.trace` with a bool. For example, to turn the trace on (no matter what state it is in) and then read its state back, run" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "channel.trace = True\n", + "channel.trace" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Control over the coupling of the specific channel is supplied via the `channel.Coupling` class. To set the coupling to $50\\,\\Omega$ and then read it back, run the following commands:" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "channel.coupling = channel.Coupling.dc50\n", + "channel.coupling" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The scale (i.e., the volts per division) of a channel can be set unitful as well. If no units are given it is assumed that the user means Volts per division. To set the scale to 1 V per division and read its state back, run" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array(1.) * V" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "channel.scale = u.Quantity(1, u.V)\n", + "channel.scale" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the same manner, the trace can also be shifted to, let's say -2950 mV in vertical position. This offset can be set / read as following:" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array(-2.95) * V" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "channel.offset = u.Quantity(-2950, u.mV)\n", + "channel.offset" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, after having gone through all configurations, the waveform can be read back to the computer. The waveform is reutrned as a two dimensional numpy array representing the timebase and the signal. We can directly unpack the waveform via:" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [], + "source": [ + "timebase, signal = channel.read_waveform()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Furthermore we know that the signal has been shifted by -2.95 V in the negative direction and by 60 ns in the positive time base direction. Let's see how the signal looks." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0, 0.5, 'Signal (V)')" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "plt.plot(timebase, signal)\n", + "plt.xlabel(\"Time since trigger (s)\")\n", + "plt.ylabel(\"Signal (V)\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The offset in horizontal and vertical access are automatically taken into account such that the signal that is returned can directly be interpreted in time relative to the trigger and in the signal in absolute voltage. Clearly, this peak is almost 4 V heigh and very short. Let us move the peak in horizontal and vertical direction to the center of the oscilloscope display and read back one waveform with 1 V per division on the vertical axis and one waveform with 250 mV per division. The latter will surely clip the peak at the top." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, '1 V / division: Signal visible')" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# set offsets to zero\n", + "inst.trigger_delay = 0\n", + "channel.offset = 0\n", + "\n", + "# set horizontal axis to 5 ns per division\n", + "inst.time_div = u.Quantity(5, u.ns)\n", + "\n", + "# set to 250 mV per division and read waveform back\n", + "channel.scale = u.Quantity(250, u.mV)\n", + "x1, y1 = channel.read_waveform()\n", + "\n", + "# allow for 250 ms to not have it read to fast\n", + "sleep(0.25)\n", + "\n", + "# set to 1 V per division and read the waveform back\n", + "channel.scale = u.Quantity(1, u.V)\n", + "x2, y2 = channel.read_waveform()\n", + "\n", + "# plot the results\n", + "fig, ax = plt.subplots(1, 2, sharey=True, figsize=(12,4))\n", + "\n", + "ax[0].plot(x1, y1)\n", + "ax[0].set_xlabel(\"Time (s)\")\n", + "ax[0].set_ylabel(\"Signal (V)\")\n", + "ax[0].set_title(\"250 mV / division: Signal clipped\")\n", + "ax[1].plot(x2, y2)\n", + "ax[1].set_xlabel(\"Time (s)\")\n", + "ax[1].set_title(\"1 V / division: Signal visible\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Clearly, the left signal is clipped on top. This results from the fact that the signal did not fit on the oscilloscope screen. If the signal is off scale, the data returned is simple the largest possible one, in this case, full signal. Since we artificially scale the two figures to the same y scale, this of course does not show up on top but at 1 V, which is equivalent to 4 divisions up.\n", + "\n", + "**Remember**: Reading a wave form only returns the data that is displayed on the oscilloscope screen." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Math functions\n", + "\n", + "Many math functions are available on these oscilloscopes, depending on the options that the oscilloscope is shipped with, more or less functions are available. For an overview of the implemented functions and how they work, have a look at the InstrumentKit documentation. You will find all functions in the `Math.Operators` subclass.\n", + "\n", + "For now, let's set up averaging of the first channel." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [], + "source": [ + "# Access the first math function `F1`\n", + "function = inst.math[0]\n", + "\n", + "# turn on the trace of this math function\n", + "function.trace = True\n", + "\n", + "# set it to averaging of channel 1 (0 in python)\n", + "function.operator.average(('C', 0))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This will have set up the first function to average the first channel. The parameter that is passed on to to average, the required parameter, is the source it should average. Different operators have of course different parameters that are required / optional. \n", + "\n", + "**Note**: There are two ways that sources can be specified. If an integer is given, it is assumed that the source is a channel. Alternatively another source, e.g., a math function could also be defined. This would be done by submitting a tuple, i.e., `('F', 0)` to select the first math function (called `F1` in the oscilloscope).\n", + "\n", + "To check if this all worked we can read back the channel itself and the average math function and plot the results." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Average of the channel (math)')" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# read channel\n", + "x_ch, y_ch = channel.read_waveform()\n", + "\n", + "# allow for 250 ms to not have it read to fast\n", + "sleep(0.25)\n", + "\n", + "# read math\n", + "x_math, y_math = function.read_waveform()\n", + "\n", + "# plot\n", + "fig, ax = plt.subplots(1, 2, sharey=True, figsize=(12, 4))\n", + "\n", + "ax[0].plot(x_ch, y_ch)\n", + "ax[0].set_xlabel(\"Time (s)\")\n", + "ax[0].set_ylabel(\"Signal (V)\")\n", + "ax[0].set_title(\"Channel readout\")\n", + "ax[1].plot(x_math, y_math)\n", + "ax[1].set_xlabel(\"Time (s)\")\n", + "ax[1].set_title(\"Average of the channel (math)\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "With a poorer signal generator, the average should look a lot smoother than the channel. To finish up the math section, let's turn off the math trace." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [], + "source": [ + "function.trace = False" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Measurements and statistics\n", + "\n", + "In addition to mathematical operations on channels, oscilloscopes can take measurements and display the statistics. Many measurement parameters are already implemented, check out the documentation and look for the `inst.MeasurementParameters` class. Currently, only measurement parameters that act on a single source are available.\n", + "\n", + "As an example, let us set up 2 measurements. Measurement 1 determines the rise time (10% to 90%), measurement 2 the fall time (80% to 20%) of the first channel readout. Setting up the measurement can be done as following:" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [], + "source": [ + "# assign the two measurements\n", + "msr1 = inst.measurement[0]\n", + "msr2 = inst.measurement[1]\n", + "\n", + "# turn on the measurements (only one is really necessary, the other one is turned on automatically!)\n", + "msr1.measurement_state = msr1.State.both\n", + "\n", + "# assign the measurement types and which source the measurement should be on\n", + "msr1.set_parameter(inst.MeasurementParameters.rise_time_10_90, 0)\n", + "msr2.set_parameter(inst.MeasurementParameters.fall_time_80_20, 0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The oscilloscope will now automatically set up these measurements and will start accumulating statistics. The statistics can be returned at any point, which will return a tuple containing 5 floats. These floats are:\n", + "\n", + " 1. Average\n", + " 2. Lowest value measured\n", + " 3. Highest value measured\n", + " 4. Standard deviation\n", + " 5. Number of sweeps\n", + " \n", + "These returns are not unitful, so you will need to know what was set up. For the rise and fall times, the returns will of course be in the form of time. SI units are always returned, in this case, seconds." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(1.52152e-09, 1.49e-09, 1.56e-09, 2.553e-11, 4.0)" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "msr1.statistics" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(9.6247e-10, 9.02e-10, 1.01e-09, 2.415e-11, 24.0)" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "msr2.statistics" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To start a new series of measurements, i.e., reset the number of sweeps, the following command can be executed:" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(1.56e-09, 1.56e-09, 1.56e-09, 0.0, 1.0)" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "inst.clear_sweeps()\n", + "\n", + "# getting statistics for `msr1` again:\n", + "msr1.statistics" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To delete the measurement parameters again and turn off the table, the following commands are used:" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [], + "source": [ + "# delete parameters\n", + "msr1.delete()\n", + "msr2.delete()\n", + "\n", + "# turn off measurement\n", + "msr1.measurement_state = msr1.State.off" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Further information\n", + "\n", + "Please check out the documention on [readthedocs.io](https://instrumentkit.readthedocs.io/en/latest/) and feel free to open an issue on the github repository if you have any problems / feature requests." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.3" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/doc/examples/ex_oscilloscope_waveform.ipynb b/doc/examples/ex_oscilloscope_waveform.ipynb index ca45d0622..c28b4d35c 100644 --- a/doc/examples/ex_oscilloscope_waveform.ipynb +++ b/doc/examples/ex_oscilloscope_waveform.ipynb @@ -118,4 +118,4 @@ "metadata": {} } ] -} \ No newline at end of file +} diff --git a/doc/examples/ex_oscilloscope_waveform.py b/doc/examples/ex_oscilloscope_waveform.py index 3269c7586..5b4588260 100644 --- a/doc/examples/ex_oscilloscope_waveform.py +++ b/doc/examples/ex_oscilloscope_waveform.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # 3.0 # @@ -11,12 +10,12 @@ # -# In this example, we will demonstrate how to connect to a Tektronix DPO 4104 +# In this example, we will demonstrate how to connect to a Tektronix DPO 4104 # oscilloscope and transfer the waveform from channel 1 into memory. # -# We start by importing the InstrumentKit and numpy packages. In this example +# We start by importing the InstrumentKit and numpy packages. In this example # we require numpy because the waveforms will be returned as numpy arrays. # @@ -36,18 +35,18 @@ # -tek = ik.tektronix.TekTDS224.open_tcpip('192.168.0.2', 8080) +tek = ik.tektronix.TekTDS224.open_tcpip("192.168.0.2", 8080) # -# Now that we are connected to the instrument, we can transfer the waveform +# Now that we are connected to the instrument, we can transfer the waveform # from the oscilloscope. Note that Python channel[0] specifies the physical # channel 1. This is due to Python's zero-based numbering vs Tektronix's # one-based numbering. # -[x,y] = tek.channel[0].read_waveform() +[x, y] = tek.channel[0].read_waveform() # @@ -56,5 +55,4 @@ # -print np.mean(y) - +print(np.mean(y)) diff --git a/doc/examples/ex_qubitekk_gui.py b/doc/examples/ex_qubitekk_gui.py index 2ee971b70..d768f4196 100644 --- a/doc/examples/ex_qubitekk_gui.py +++ b/doc/examples/ex_qubitekk_gui.py @@ -1,12 +1,14 @@ #!/usr/bin/python # Qubitekk Coincidence Counter example import matplotlib -matplotlib.use('TkAgg') + +matplotlib.use("TkAgg") from matplotlib.figure import Figure from numpy import arange, sin, pi from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg + # implement the default mpl key bindings from matplotlib.backend_bases import key_press_handler from sys import platform as _platform @@ -35,19 +37,19 @@ def getvalues(i): chan2counts.set("Overflow") if cc.channel[2].count < 0: coinc_counts.set("Overflow") - t.append(i*time_diff) + t.append(i * time_diff) i += 1 # plot values - p1, = a.plot(t, coincvals, color="r", linewidth=2.0) - p2, = a.plot(t, chan1vals, color="b", linewidth=2.0) - p3, = a.plot(t, chan2vals, color="g", linewidth=2.0) + (p1,) = a.plot(t, coincvals, color="r", linewidth=2.0) + (p2,) = a.plot(t, chan1vals, color="b", linewidth=2.0) + (p3,) = a.plot(t, chan2vals, color="g", linewidth=2.0) a.legend([p1, p2, p3], ["Coincidences", "Channel 1", "Channel 2"]) - a.set_xlabel('Time (s)') - a.set_ylabel('Counts (Hz)') + a.set_xlabel("Time (s)") + a.set_ylabel("Counts (Hz)") canvas.show() # get the values again in the specified amount of time - root.after(int(time_diff*1000),getvalues,i) + root.after(int(time_diff * 1000), getvalues, i) def gate_enable(): @@ -83,6 +85,7 @@ def reset(*args): trigger_enabled.set(cc.count_enable) gate_enabled.set(cc.gate_enable) + if __name__ == "__main__": cc = ik.qubitekk.CC1.open_serial(vid=1027, pid=24577, baud=19200, timeout=10) print(cc.firmware) @@ -140,7 +143,7 @@ def reset(*args): # set up the plotting area f = Figure(figsize=(10, 8), dpi=100) - a = f.add_subplot(111, axisbg='black') + a = f.add_subplot(111, axisbg="black") t = [] coincvals = [] @@ -152,48 +155,79 @@ def reset(*args): canvas.get_tk_widget().grid(column=3, row=1, rowspan=11, sticky=tk.W) # label initialization - dwell_time_entry = tk.Entry(mainframe, width=7, textvariable=dwell_time, font="Verdana 20") + dwell_time_entry = tk.Entry( + mainframe, width=7, textvariable=dwell_time, font="Verdana 20" + ) dwell_time_entry.grid(column=2, row=2, sticky=(tk.W, tk.E)) window_entry = tk.Entry(mainframe, width=7, textvariable=window, font="Verdana 20") window_entry.grid(column=2, row=3, sticky=(tk.W, tk.E)) - tk.Label(mainframe, text="Dwell Time:", font="Verdana 20").grid(column=1, row=2, sticky=tk.W) - tk.Label(mainframe, text="Window size:", font="Verdana 20").grid(column=1, row=3, sticky=tk.W) - - tk.Checkbutton(mainframe, font="Verdana 20", variable=gate_enabled, command=gate_enable).grid(column=2, row=4) - tk.Label(mainframe, text="Gate Enable: ", font="Verdana 20").grid(column=1, row=4, sticky=tk.W) - - tk.Checkbutton(mainframe, font="Verdana 20", variable=subtract_enabled, command=subtract_enable).grid(column=2, row=5) - tk.Label(mainframe, text="Subtract Accidentals: ", font="Verdana 20").grid(column=1, row=5, sticky=tk.W) - - tk.Checkbutton(mainframe, font="Verdana 20", variable=trigger_enabled, command=trigger_enable).grid(column=2, row=6) - tk.Label(mainframe, text="Continuous Trigger: ", font="Verdana 20").grid(column=1, row=6, sticky=tk.W) - - tk.Label(mainframe, text="Channel 1: ", font="Verdana 20").grid(column=1, row=7, sticky=tk.W) - tk.Label(mainframe, text="Channel 2: ", font="Verdana 20").grid(column=1, row=8, sticky=tk.W) - tk.Label(mainframe, text="Coincidences: ", font="Verdana 20").grid(column=1, row=9, sticky=tk.W) - - tk.Label(mainframe, textvariable=chan1counts, font="Verdana 34", fg="white", bg="black").grid(column=2, row=7, - sticky=tk.W) - tk.Label(mainframe, textvariable=chan2counts, font="Verdana 34", fg="white", bg="black").grid(column=2, row=8, - sticky=tk.W) - tk.Label(mainframe, textvariable=coinc_counts, font="Verdana 34", fg="white", bg="black").grid(column=2, row=9, - sticky=tk.W) - - tk.Button(mainframe, text="Reset", font="Verdana 24", command=reset).grid(column=1, row=10, sticky=tk.W) - - tk.Button(mainframe, text="Clear Counts", font="Verdana 24", command=clear_counts).grid(column=2, row=10, - sticky=tk.W) - - tk.Label(mainframe, text="Firmware Version: " + str(cc.firmware), - font="Verdana 20").grid(column=1, row=11, columnspan=2, sticky=tk.W) + tk.Label(mainframe, text="Dwell Time:", font="Verdana 20").grid( + column=1, row=2, sticky=tk.W + ) + tk.Label(mainframe, text="Window size:", font="Verdana 20").grid( + column=1, row=3, sticky=tk.W + ) + + tk.Checkbutton( + mainframe, font="Verdana 20", variable=gate_enabled, command=gate_enable + ).grid(column=2, row=4) + tk.Label(mainframe, text="Gate Enable: ", font="Verdana 20").grid( + column=1, row=4, sticky=tk.W + ) + + tk.Checkbutton( + mainframe, font="Verdana 20", variable=subtract_enabled, command=subtract_enable + ).grid(column=2, row=5) + tk.Label(mainframe, text="Subtract Accidentals: ", font="Verdana 20").grid( + column=1, row=5, sticky=tk.W + ) + + tk.Checkbutton( + mainframe, font="Verdana 20", variable=trigger_enabled, command=trigger_enable + ).grid(column=2, row=6) + tk.Label(mainframe, text="Continuous Trigger: ", font="Verdana 20").grid( + column=1, row=6, sticky=tk.W + ) + + tk.Label(mainframe, text="Channel 1: ", font="Verdana 20").grid( + column=1, row=7, sticky=tk.W + ) + tk.Label(mainframe, text="Channel 2: ", font="Verdana 20").grid( + column=1, row=8, sticky=tk.W + ) + tk.Label(mainframe, text="Coincidences: ", font="Verdana 20").grid( + column=1, row=9, sticky=tk.W + ) + + tk.Label( + mainframe, textvariable=chan1counts, font="Verdana 34", fg="white", bg="black" + ).grid(column=2, row=7, sticky=tk.W) + tk.Label( + mainframe, textvariable=chan2counts, font="Verdana 34", fg="white", bg="black" + ).grid(column=2, row=8, sticky=tk.W) + tk.Label( + mainframe, textvariable=coinc_counts, font="Verdana 34", fg="white", bg="black" + ).grid(column=2, row=9, sticky=tk.W) + + tk.Button(mainframe, text="Reset", font="Verdana 24", command=reset).grid( + column=1, row=10, sticky=tk.W + ) + + tk.Button( + mainframe, text="Clear Counts", font="Verdana 24", command=clear_counts + ).grid(column=2, row=10, sticky=tk.W) + + tk.Label( + mainframe, text="Firmware Version: " + str(cc.firmware), font="Verdana 20" + ).grid(column=1, row=11, columnspan=2, sticky=tk.W) for child in mainframe.winfo_children(): child.grid_configure(padx=5, pady=5) # when the enter key is pressed, send the current values in the entries to the dwelltime and window to the # coincidence counter - root.bind('',parse) + root.bind("", parse) # in 100 milliseconds, get the counts values off of the coincidence counter - root.after(int(time_diff*1000), getvalues, i) + root.after(int(time_diff * 1000), getvalues, i) # start the GUI root.mainloop() diff --git a/doc/examples/ex_qubitekkcc.py b/doc/examples/ex_qubitekkcc.py index be4448243..ac6036821 100644 --- a/doc/examples/ex_qubitekkcc.py +++ b/doc/examples/ex_qubitekkcc.py @@ -2,24 +2,25 @@ from sys import platform as _platform import instruments as ik -from quantities import second, nanosecond +import instruments.units as u def main(): - cc = ik.qubitekk.CC1.open_serial(vid=1027, pid=24577, baud=19200, timeout=10) - cc1.dwell_time = 1.0 * second - print cc1.dwell_time - cc1.delay = 0.0 * nanosecond - print cc1.delay - cc1.window = 3.0 * nanosecond - print cc1.window + cc1 = ik.qubitekk.CC1.open_serial(vid=1027, pid=24577, baud=19200, timeout=10) + cc1.dwell_time = 1.0 * u.s + print(cc1.dwell_time) + cc1.delay = 0.0 * u.ns + print(cc1.delay) + cc1.window = 3.0 * u.ns + print(cc1.window) cc1.trigger = ik.qubitekk.TriggerModeInt.start_stop - print cc1.trigger - print "Fetching Counts" - print cc1.channel[0].count - print cc1.channel[1].count - print cc1.channel[2].count - print "Fetched Counts" + print(cc1.trigger) + print("Fetching Counts") + print(cc1.channel[0].count) + print(cc1.channel[1].count) + print(cc1.channel[2].count) + print("Fetched Counts") + if __name__ == "__main__": while True: diff --git a/doc/examples/ex_qubitekkcc_simple.py b/doc/examples/ex_qubitekkcc_simple.py index e792cbe7e..e347aa21a 100644 --- a/doc/examples/ex_qubitekkcc_simple.py +++ b/doc/examples/ex_qubitekkcc_simple.py @@ -4,24 +4,24 @@ from sys import platform as _platform import instruments as ik -import quantities +import instruments.units as u if __name__ == "__main__": # open connection to coincidence counter. If you are using Windows, this will be a com port. On linux, it will show # up in /dev/ttyusb if _platform == "linux" or _platform == "linux2": - cc = ik.qubitekk.CC1.open_serial('/dev/ttyUSB0', 19200, timeout=1) + cc = ik.qubitekk.CC1.open_serial("/dev/ttyUSB0", 19200, timeout=1) else: - cc = ik.qubitekk.CC1.open_serial('COM8', 19200, timeout=1) + cc = ik.qubitekk.CC1.open_serial("COM8", 19200, timeout=1) - print "Initializing Coincidence Counter" - cc.dwell_time = 1.0*quantities.s - cc.delay = 0.0*quantities.ns - cc.window = 3.0*quantities.ns + print("Initializing Coincidence Counter") + cc.dwell_time = 1.0 * u.s + cc.delay = 0.0 * u.ns + cc.window = 3.0 * u.ns cc.trigger = cc.TriggerMode.start_stop - print "ch1 counts: "+str(cc.channel[0].count) - print "ch2 counts: "+str(cc.channel[1].count) - print "counts counts: "+str(cc.channel[2].count) + print(f"ch1 counts: {str(cc.channel[0].count)}") + print(f"ch2 counts: {str(cc.channel[1].count)}") + print(f"counts counts: {str(cc.channel[2].count)}") - print "Finished Initializing Coincidence Counter" \ No newline at end of file + print("Finished Initializing Coincidence Counter") diff --git a/doc/examples/ex_tekdpo70000.ipynb b/doc/examples/ex_tekdpo70000.ipynb index 8954a36d6..d4d18a78a 100644 --- a/doc/examples/ex_tekdpo70000.ipynb +++ b/doc/examples/ex_tekdpo70000.ipynb @@ -152,7 +152,7 @@ "cell_type": "code", "collapsed": false, "input": [ - "_.rescale('GHz')" + "_.to('GHz')" ], "language": "python", "metadata": {}, @@ -322,4 +322,4 @@ "metadata": {} } ] -} \ No newline at end of file +} diff --git a/doc/examples/ex_thorlabslcc.py b/doc/examples/ex_thorlabslcc.py index 6aa2376a8..f3814097f 100644 --- a/doc/examples/ex_thorlabslcc.py +++ b/doc/examples/ex_thorlabslcc.py @@ -1,10 +1,11 @@ -#Thorlabs Liquid Crystal Controller example +# Thorlabs Liquid Crystal Controller example import instruments as ik -lcc = ik.thorlabs.LCC25.open_serial('COM10', 115200,timeout=1) -#put model in voltage1 setting: +lcc = ik.thorlabs.LCC25.open_serial("COM10", 115200, timeout=1) + +# put model in voltage1 setting: lcc.mode = llc.Mode.voltage1 -print("The current frequency is: ",lcc.frequency) -print("The current voltage is: ",lcc.voltage1) +print("The current frequency is: ", lcc.frequency) +print("The current voltage is: ", lcc.voltage1) diff --git a/doc/examples/ex_thorlabssc10.py b/doc/examples/ex_thorlabssc10.py index aaa79a678..ab6ae1ece 100644 --- a/doc/examples/ex_thorlabssc10.py +++ b/doc/examples/ex_thorlabssc10.py @@ -1,32 +1,32 @@ -#Thorlabs Shutter Controller example +# Thorlabs Shutter Controller example import instruments as ik -#if the baud mode is set to 1, then the baud rate is 115200 -#otherwise, the baud rate is 9600 -sc = ik.thorlabs.SC10.open_serial('COM9', 9600,timeout=1) + +# if the baud mode is set to 1, then the baud rate is 115200 +# otherwise, the baud rate is 9600 +sc = ik.thorlabs.SC10.open_serial("COM9", 9600, timeout=1) print("It is a: ", sc.name) print("Setting shutter open time to 10 ms") sc.open_time = 10 -print("The shutter open time is: ",sc.open_time) +print("The shutter open time is: ", sc.open_time) print("Setting shutter open time to 50 ms") sc.open_time = 50 -print("The shutter open time is: ",sc.open_time) +print("The shutter open time is: ", sc.open_time) print("Setting shutter close time to 10 ms") sc.open_time = 10 -print("The shutter close time is: ",sc.open_time) +print("The shutter close time is: ", sc.open_time) print("Setting shutter close time to 50 ms") sc.open_time = 50 -print("The shutter close time is: ",sc.open_time) +print("The shutter close time is: ", sc.open_time) print("Setting repeat count to 4") sc.repeat = 4 -print("The repeat count is: ",sc.repeat) +print("The repeat count is: ", sc.repeat) print("Setting repeat count to 8") sc.repeat = 8 -print("The repeat count is: ",sc.repeat) +print("The repeat count is: ", sc.repeat) print("setting mode to auto") sc.mode = sc.Mode.auto - diff --git a/doc/examples/ex_thorlabstc200.py b/doc/examples/ex_thorlabstc200.py index b38f6ec27..eabaac943 100644 --- a/doc/examples/ex_thorlabstc200.py +++ b/doc/examples/ex_thorlabstc200.py @@ -1,10 +1,11 @@ -#Thorlabs Temperature Controller example +# Thorlabs Temperature Controller example import instruments as ik -import quantities -tc = ik.thorlabs.TC200.open_serial('/dev/tc200', 115200) +import instruments.units as u -tc.temperature_set = 70*quantities.degF +tc = ik.thorlabs.TC200.open_serial("/dev/tc200", 115200) + +tc.temperature_set = 70 * u.degF print("The current temperature is: ", tc.temperature) tc.mode = tc.Mode.normal @@ -22,7 +23,7 @@ tc.d = 2 print("The current d gain is: ", tc.d) -tc.degrees = quantities.degF +tc.degrees = u.degF print("The current degrees settings is: ", tc.degrees) tc.sensor = tc.Sensor.ptc100 @@ -31,10 +32,8 @@ tc.beta = 3900 print("The current beta settings is: ", tc.beta) -tc.max_temperature = 150*quantities.degC +tc.max_temperature = 150 * u.degC print("The current max temperature setting is: ", tc.max_temperature) -tc.max_power = 1000*quantities.mW +tc.max_power = 1000 * u.mW print("The current max power setting is: ", tc.max_power) - - diff --git a/doc/examples/ex_topticatopmode.py b/doc/examples/ex_topticatopmode.py index 3ef51a3eb..5cbc5d053 100644 --- a/doc/examples/ex_topticatopmode.py +++ b/doc/examples/ex_topticatopmode.py @@ -1,16 +1,16 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Toptica Topmode example """ import instruments as ik -import quantities as pq +import instruments.units as u from platform import system -if system() == 'Windows': - tm = ik.toptica.TopMode.open_serial('COM17', 115200) + +if system() == "Windows": + tm = ik.toptica.TopMode.open_serial("COM17", 115200) else: - tm = ik.toptica.TopMode.open_serial('/dev/ttyACM0', 115200) + tm = ik.toptica.TopMode.open_serial("/dev/ttyACM0", 115200) print("The top mode's firmware is: ", tm.firmware) print("The top mode's serial number is: ", tm.serial_number) @@ -28,30 +28,18 @@ print("The laser1's enable state is: ", tm.laser[0].enable) print("The laser1's up time is: ", tm.laser[0].on_time) print("The laser1's charm state is: ", tm.laser[0].charm_status) -print("The laser1's temperature controller state is: ", - tm.laser[0].temperature_control_status) -print("The laser1's current controller state is: ", - tm.laser[0].current_control_status) +print( + "The laser1's temperature controller state is: ", + tm.laser[0].temperature_control_status, +) +print("The laser1's current controller state is: ", tm.laser[0].current_control_status) print("The laser1's tec state is: ", tm.laser[0].tec_status) print("The laser1's intensity is: ", tm.laser[0].intensity) print("The laser1's mode hop state is: ", tm.laser[0].mode_hop) print("The laser1's correction status is: ", tm.laser[0].correction_status) print("The laser1's lock start time is: ", tm.laser[0].lock_start) print("The laser1's first mode hop time is: ", tm.laser[0].first_mode_hop_time) -print("The laser1's latest mode hop time is: ", - tm.laser[0].latest_mode_hop_time) +print("The laser1's latest mode hop time is: ", tm.laser[0].latest_mode_hop_time) print("The current emission state is: ", tm.enable) tm.laser[0].enable = True - - - - - - - - - - - - diff --git a/doc/examples/example2.py b/doc/examples/example2.py index 228bcad88..ba54978fe 100644 --- a/doc/examples/example2.py +++ b/doc/examples/example2.py @@ -2,23 +2,23 @@ # Filename: example2.py # Example 1: -# - Import required packages -# - Create object for our Tek TDS 224 -# - Transfer the waveform from the oscilloscope on channel 1 using binary block reading -# - Calculate the FFT of the transfered waveform -# - Graph resultant data +# - Import required packages +# - Create object for our Tek TDS 224 +# - Transfer the waveform from the oscilloscope on channel 1 using binary block reading +# - Calculate the FFT of the transfered waveform +# - Graph resultant data from instruments import * import numpy as np import matplotlib.pyplot as plt -tek = Tektds224('/dev/ttyUSB0',1,30) +tek = Tektds224("/dev/ttyUSB0", 1, 30) -[x,y] = tek.readWaveform('CH1','BINARY') -freq = np.fft.fft(y) # Calculate FFT -timestep = float( tek.query('WFMP:XIN?') ) # Query the timestep between data points -freqx = np.fft.fftfreq(freq.size,timestep) # Compute the x-axis for the FFT data -plt.plot(freqx,abs(freq)) # Plot the data using matplotlib -plt.ylim(0,500) # Adjust the vertical scale -plt.show() # Show the graph +[x, y] = tek.readWaveform("CH1", "BINARY") +freq = np.fft.fft(y) # Calculate FFT +timestep = float(tek.query("WFMP:XIN?")) # Query the timestep between data points +freqx = np.fft.fftfreq(freq.size, timestep) # Compute the x-axis for the FFT data +plt.plot(freqx, abs(freq)) # Plot the data using matplotlib +plt.ylim(0, 500) # Adjust the vertical scale +plt.show() # Show the graph diff --git a/doc/examples/minghe/ex_minghe_mhs5200.py b/doc/examples/minghe/ex_minghe_mhs5200.py new file mode 100644 index 000000000..7b4ef4fc2 --- /dev/null +++ b/doc/examples/minghe/ex_minghe_mhs5200.py @@ -0,0 +1,27 @@ +#!/usr/bin/python +from instruments.minghe import MHS5200 +import instruments.units as u + +mhs = MHS5200.open_serial(vid=6790, pid=29987, baud=57600) +print(mhs.serial_number) +mhs.channel[0].frequency = 3000000 * u.Hz +print(mhs.channel[0].frequency) +mhs.channel[0].function = MHS5200.Function.sawtooth_down +print(mhs.channel[0].function) +mhs.channel[0].amplitude = 9.0 * u.V +print(mhs.channel[0].amplitude) +mhs.channel[0].offset = -0.5 +print(mhs.channel[0].offset) +mhs.channel[0].phase = 90 +print(mhs.channel[0].phase) + +mhs.channel[1].frequency = 2000000 * u.Hz +print(mhs.channel[1].frequency) +mhs.channel[1].function = MHS5200.Function.square +print(mhs.channel[1].function) +mhs.channel[1].amplitude = 2.0 * u.V +print(mhs.channel[1].amplitude) +mhs.channel[1].offset = 0.0 +print(mhs.channel[1].offset) +mhs.channel[1].phase = 15 +print(mhs.channel[1].phase) diff --git a/doc/examples/qubitekk/ex_qubitekk_mc1.py b/doc/examples/qubitekk/ex_qubitekk_mc1.py index cd124e354..4e37ff0ad 100644 --- a/doc/examples/qubitekk/ex_qubitekk_mc1.py +++ b/doc/examples/qubitekk/ex_qubitekk_mc1.py @@ -3,14 +3,14 @@ from time import sleep from instruments.qubitekk import MC1 -import quantities as pq +import instruments.units as u if __name__ == "__main__": mc1 = MC1.open_serial(vid=1027, pid=24577, baud=9600, timeout=1) - mc1.step_size = 25*pq.ms - mc1.inertia = 10*pq.ms + mc1.step_size = 25 * u.ms + mc1.inertia = 10 * u.ms print("step size:", mc1.step_size) print("inertial force: ", mc1.inertia) @@ -20,18 +20,18 @@ mc1.center() while mc1.is_centering(): - print(str(mc1.metric_position)+" "+str(mc1.direction)) + print(str(mc1.metric_position) + " " + str(mc1.direction)) pass print("Stage Centered") # for the motor in the mechanical delay line, the travel is limited from # the full range of travel. Here's how to set the limits. - mc1.lower_limit = -260*pq.ms - mc1.upper_limit = 300*pq.ms - mc1.increment = 5*pq.ms + mc1.lower_limit = -260 * u.ms + mc1.upper_limit = 300 * u.ms + mc1.increment = 5 * u.ms x_pos = mc1.lower_limit while x_pos <= mc1.upper_limit: - print(str(mc1.metric_position)+" "+str(mc1.direction)) + print(str(mc1.metric_position) + " " + str(mc1.direction)) mc1.move(x_pos) while mc1.move_timeout > 0: sleep(0.5) diff --git a/doc/examples/srs_DG645.ipynb b/doc/examples/srs_DG645.ipynb index a19ad882f..0df90152c 100644 --- a/doc/examples/srs_DG645.ipynb +++ b/doc/examples/srs_DG645.ipynb @@ -43,7 +43,7 @@ "collapsed": false, "input": [ "from instruments.srs import SRSDG645\n", - "import quantities as pq" + "import instruments.units as u" ], "language": "python", "metadata": {}, @@ -100,7 +100,7 @@ "cell_type": "code", "collapsed": false, "input": [ - "ddg.channel[ddg.Channels.A].delay = (ddg.Channels.B, pq.Quantity(10, 'us'))" + "ddg.channel[ddg.Channels.A].delay = (ddg.Channels.B, u.Quantity(10, 'us'))" ], "language": "python", "metadata": {}, @@ -130,4 +130,4 @@ "metadata": {} } ] -} \ No newline at end of file +} diff --git a/doc/examples/srs_DG645.py b/doc/examples/srs_DG645.py index c79f675de..11587a48b 100644 --- a/doc/examples/srs_DG645.py +++ b/doc/examples/srs_DG645.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # 3.0 # @@ -15,13 +14,13 @@ # -# We start by importing the `srs` package from within the main `instruments` package, along with the `quantities` package +# We start by importing the `srs` package from within the main `instruments` package, along with the `instruments.units` package # that is used to track physical quantities. # from instruments.srs import SRSDG645 -import quantities as pq +import instruments.units as u # @@ -30,7 +29,7 @@ # -ddg = SRSDG645.open_gpibusb('/dev/ttyUSB0', 15) +ddg = SRSDG645.open_gpibusb("/dev/ttyUSB0", 15) # @@ -38,8 +37,6 @@ # -ddg.channel[ddg.Channels.A].delay = (ddg.Channels.B, pq.Quantity(10, 'us')) +ddg.channel[ddg.Channels.A].delay = (ddg.Channels.B, u.Quantity(10, "us")) # - - diff --git a/doc/source/acknowledgements.rst b/doc/source/acknowledgements.rst index 8b5b1ac70..f2418eadb 100644 --- a/doc/source/acknowledgements.rst +++ b/doc/source/acknowledgements.rst @@ -10,10 +10,9 @@ First off, I'd like to give special thanks to cgranade for his help with pretty much every step along the way. I would be hard pressed to find something that he had nothing to do with. -- ihincks for the fantastic property factories (used throughout all classes) and for the Tektronix DPO70000 series class. +- ihincks for the fantastic property factories (used throughout all classes) and for the Tektronix DPO70000 series class. - dijkstrw for contributing several classes (HP6632b, HP3456a, Keithley 580) as well as plenty of general IK testing. - CatherineH for the Qubitekk CC1, Thorlabs LCC25, SC10, and TC200 classes - silverchris for the TekTDS5xx class - wil-langford for the HP6652a class - whitewhim2718 for the Newport ESP 301 - diff --git a/doc/source/apiref/agilent.rst b/doc/source/apiref/agilent.rst index 12022f81f..1623124a9 100644 --- a/doc/source/apiref/agilent.rst +++ b/doc/source/apiref/agilent.rst @@ -1,8 +1,8 @@ .. TODO: put documentation license header here. - + .. currentmodule:: instruments.agilent - + ======= Agilent ======= @@ -13,7 +13,7 @@ Agilent .. autoclass:: Agilent33220a :members: :undoc-members: - + :class:`Agilent34410a` Digital Multimeter ========================================= diff --git a/doc/source/apiref/fluke.rst b/doc/source/apiref/fluke.rst new file mode 100644 index 000000000..2c4ece773 --- /dev/null +++ b/doc/source/apiref/fluke.rst @@ -0,0 +1,15 @@ +.. + TODO: put documentation license header here. + +.. currentmodule:: instruments.fluke + +===== +Fluke +===== + +:class:`Fluke3000` Industrial System +==================================== + +.. autoclass:: Fluke3000 + :members: + :undoc-members: diff --git a/doc/source/apiref/generic_scpi.rst b/doc/source/apiref/generic_scpi.rst index 4755a117b..b183c2663 100644 --- a/doc/source/apiref/generic_scpi.rst +++ b/doc/source/apiref/generic_scpi.rst @@ -1,9 +1,9 @@ .. TODO: put documentation license header here. - + .. _apiref-generic_scpi: .. currentmodule:: instruments.generic_scpi - + ======================== Generic SCPI Instruments ======================== @@ -14,7 +14,7 @@ Generic SCPI Instruments .. autoclass:: SCPIInstrument :members: :undoc-members: - + :class:`SCPIMultimeter` - Generic multimeter using SCPI commands ================================================================ @@ -28,4 +28,3 @@ Generic SCPI Instruments .. autoclass:: SCPIFunctionGenerator :members: :undoc-members: - diff --git a/doc/source/apiref/gentec-eo.rst b/doc/source/apiref/gentec-eo.rst new file mode 100644 index 000000000..aefd52c53 --- /dev/null +++ b/doc/source/apiref/gentec-eo.rst @@ -0,0 +1,12 @@ +.. currentmodule:: instruments.gentec_eo + +========= +Gentec-EO +========= + +:class:`Blu` Power Meter +======================================= + +.. autoclass:: Blu + :members: + :undoc-members: diff --git a/doc/source/apiref/glassman.rst b/doc/source/apiref/glassman.rst new file mode 100644 index 000000000..4b83e3a62 --- /dev/null +++ b/doc/source/apiref/glassman.rst @@ -0,0 +1,15 @@ +.. + TODO: put documentation license header here. + +.. currentmodule:: instruments.glassman + +======== +Glassman +======== + +:class:`GlassmanFR` Single Output Power Supply +============================================== + +.. autoclass:: GlassmanFR + :members: + :undoc-members: diff --git a/doc/source/apiref/holzworth.rst b/doc/source/apiref/holzworth.rst index 1d806c646..227e41449 100644 --- a/doc/source/apiref/holzworth.rst +++ b/doc/source/apiref/holzworth.rst @@ -1,6 +1,6 @@ .. TODO: put documentation license header here. - + .. currentmodule:: instruments.holzworth ========= @@ -13,4 +13,3 @@ Holzworth .. autoclass:: HS9000 :members: :undoc-members: - diff --git a/doc/source/apiref/hp.rst b/doc/source/apiref/hp.rst index 410548e62..52b99f26a 100644 --- a/doc/source/apiref/hp.rst +++ b/doc/source/apiref/hp.rst @@ -34,3 +34,10 @@ Hewlett-Packard .. autoclass:: HP6652a :members: :undoc-members: + +:class:`HPe3631a` Power Supply +============================== + +.. autoclass:: HPe3631a + :members: + :undoc-members: diff --git a/doc/source/apiref/index.rst b/doc/source/apiref/index.rst index cd9e4a3c1..45b401f7b 100644 --- a/doc/source/apiref/index.rst +++ b/doc/source/apiref/index.rst @@ -1,8 +1,8 @@ .. TODO: put documentation license header here. - + .. _apiref: - + InstrumentKit API Reference =========================== @@ -14,19 +14,24 @@ Contents: instrument generic_scpi agilent + fluke + gentec-eo + glassman holzworth hp keithley lakeshore + minghe newport - other + ondax oxford phasematrix picowatt qubitekk rigol srs - tektronix + tektronix + teledyne thorlabs toptica yokogawa diff --git a/doc/source/apiref/instrument.rst b/doc/source/apiref/instrument.rst index 5b8b9bc52..ad4b8f1f0 100644 --- a/doc/source/apiref/instrument.rst +++ b/doc/source/apiref/instrument.rst @@ -1,8 +1,8 @@ .. TODO: put documentation license header here. - + .. currentmodule:: instruments - + ======================= Instrument Base Classes ======================= @@ -13,11 +13,11 @@ Instrument Base Classes .. autoclass:: Instrument :members: :undoc-members: - -:class:`Multimeter` - Abstract class for multimeter instruments -=============================================================== -.. autoclass:: instruments.abstract_instruments.Multimeter +:class:`Electrometer` - Abstract class for electrometer instruments +=================================================================== + +.. autoclass:: instruments.abstract_instruments.Electrometer :members: :undoc-members: @@ -28,6 +28,34 @@ Instrument Base Classes :members: :undoc-members: +:class:`Multimeter` - Abstract class for multimeter instruments +=============================================================== + +.. autoclass:: instruments.abstract_instruments.Multimeter + :members: + :undoc-members: + +:class:`Oscilloscope` - Abstract class for oscilloscope instruments +=================================================================== + +.. autoclass:: instruments.abstract_instruments.Oscilloscope + :members: + :undoc-members: + +:class:`OpticalSpectrumAnalyzer` - Abstract class for optical spectrum analyzer instruments +=========================================================================================== + +.. autoclass:: instruments.abstract_instruments.OpticalSpectrumAnalyzer + :members: + :undoc-members: + +:class:`PowerSupply` - Abstract class for power supply instruments +================================================================== + +.. autoclass:: instruments.abstract_instruments.PowerSupply + :members: + :undoc-members: + :class:`SignalGenerator` - Abstract class for Signal Generators =============================================================== @@ -48,4 +76,3 @@ Instrument Base Classes .. autoclass:: instruments.abstract_instruments.signal_generator.SGChannel :members: :undoc-members: - diff --git a/doc/source/apiref/keithley.rst b/doc/source/apiref/keithley.rst index d1f466225..5e1ed4e73 100644 --- a/doc/source/apiref/keithley.rst +++ b/doc/source/apiref/keithley.rst @@ -1,8 +1,8 @@ .. TODO: put documentation license header here. - + .. currentmodule:: instruments.keithley - + ======== Keithley ======== @@ -13,7 +13,14 @@ Keithley .. autoclass:: Keithley195 :members: :undoc-members: - + +:class:`Keithley485` Picoammeter +================================ + +.. autoclass:: Keithley485 + :members: + :undoc-members: + :class:`Keithley580` Microohm Meter =================================== @@ -42,4 +49,4 @@ Keithley .. autoclass:: Keithley6514 :members: :undoc-members: -.. _Keithley 6514: http://www.tunl.duke.edu/documents/public/electronics/Keithley/keithley-6514-electrometer-manual.pdf \ No newline at end of file +.. _Keithley 6514: http://www.tunl.duke.edu/documents/public/electronics/Keithley/keithley-6514-electrometer-manual.pdf diff --git a/doc/source/apiref/lakeshore.rst b/doc/source/apiref/lakeshore.rst index f147dc579..a16bf29c1 100644 --- a/doc/source/apiref/lakeshore.rst +++ b/doc/source/apiref/lakeshore.rst @@ -1,8 +1,8 @@ .. TODO: put documentation license header here. - + .. currentmodule:: instruments.lakeshore - + ========= Lakeshore ========= @@ -13,7 +13,7 @@ Lakeshore .. autoclass:: Lakeshore340 :members: :undoc-members: - + :class:`Lakeshore370` AC Resistance Bridge ========================================== @@ -27,4 +27,3 @@ Lakeshore .. autoclass:: Lakeshore475 :members: :undoc-members: - diff --git a/doc/source/apiref/minghe.rst b/doc/source/apiref/minghe.rst new file mode 100644 index 000000000..9ad8f87c9 --- /dev/null +++ b/doc/source/apiref/minghe.rst @@ -0,0 +1,15 @@ +.. + TODO: put documentation license header here. + +.. currentmodule:: instruments.minghe + +====== +Minghe +====== + +:class:`MHS5200` Function Generator +=================================== + +.. autoclass:: MHS5200 + :members: + :undoc-members: diff --git a/doc/source/apiref/newport.rst b/doc/source/apiref/newport.rst index 1aaefae16..18165f107 100644 --- a/doc/source/apiref/newport.rst +++ b/doc/source/apiref/newport.rst @@ -1,26 +1,25 @@ .. TODO: put documentation license header here. - + .. currentmodule:: instruments.newport - + ======= Newport ======= +:class:`Agilis` Piezo Motor Controller +====================================== + +.. autoclass:: AGUC2 + :members: + :undoc-members: + :class:`NewportESP301` Motor Controller ======================================= .. autoclass:: NewportESP301 :members: :undoc-members: - -.. autoclass:: NewportESP301Axis - :members: - :undoc-members: - -.. autoclass:: NewportESP301HomeSearchMode - :members: - :undoc-members: :class:`NewportError` ===================== @@ -29,3 +28,9 @@ Newport :members: :undoc-members: +:class:`PicoMotorController8742` +================================ + +.. autoclass:: PicoMotorController8742 + :members: + :undoc-members: diff --git a/doc/source/apiref/ondax.rst b/doc/source/apiref/ondax.rst index 3320c2993..adc7c08a3 100644 --- a/doc/source/apiref/ondax.rst +++ b/doc/source/apiref/ondax.rst @@ -1,8 +1,8 @@ .. TODO: put documentation license header here. - + .. currentmodule:: instruments.ondax - + ===== Ondax ===== @@ -13,4 +13,3 @@ Ondax .. autoclass:: LM :members: :undoc-members: - diff --git a/doc/source/apiref/other.rst b/doc/source/apiref/other.rst deleted file mode 100644 index f5163df47..000000000 --- a/doc/source/apiref/other.rst +++ /dev/null @@ -1,39 +0,0 @@ -.. - TODO: put documentation license header here. - -.. currentmodule:: instruments.other - -================= -Other Instruments -================= - -:class:`NewportESP301` -====================== - -.. autoclass:: NewportESP301 - :members: - :undoc-members: - -.. autoclass:: NewportESP301Axis - :members: - :undoc-members: - -:class:`PhaseMatrixFSW0020` -=========================== - -.. autoclass:: PhaseMatrixFSW0020 - :members: - :undoc-members: - -Units ------ - -Units are identified to the Phase Matrix FSW-0020 using the -`~quantities.Quantity` class implemented by the `quantities` package. To support -the FSW-0020, we provide several additional unit quantities, listed here. - -.. autodata:: mHz - -.. autodata:: dBm - -.. autodata:: cBm diff --git a/doc/source/apiref/oxford.rst b/doc/source/apiref/oxford.rst index ca1c120e8..3c978d930 100644 --- a/doc/source/apiref/oxford.rst +++ b/doc/source/apiref/oxford.rst @@ -1,12 +1,12 @@ .. TODO: put documentation license header here. - + .. currentmodule:: instruments.oxford - + ====== Oxford ====== - + :class:`OxfordITC503` Temperature Controller ============================================ diff --git a/doc/source/apiref/phasematrix.rst b/doc/source/apiref/phasematrix.rst index 0843420b1..364f783eb 100644 --- a/doc/source/apiref/phasematrix.rst +++ b/doc/source/apiref/phasematrix.rst @@ -1,8 +1,8 @@ .. TODO: put documentation license header here. - + .. currentmodule:: instruments.phasematrix - + =========== PhaseMatrix =========== @@ -13,4 +13,3 @@ PhaseMatrix .. autoclass:: PhaseMatrixFSW0020 :members: :undoc-members: - diff --git a/doc/source/apiref/picowatt.rst b/doc/source/apiref/picowatt.rst index 32ea43674..f6c8c6a94 100644 --- a/doc/source/apiref/picowatt.rst +++ b/doc/source/apiref/picowatt.rst @@ -1,8 +1,8 @@ .. TODO: put documentation license header here. - + .. currentmodule:: instruments.picowatt - + ======== Picowatt ======== @@ -13,4 +13,3 @@ Picowatt .. autoclass:: PicowattAVS47 :members: :undoc-members: - diff --git a/doc/source/apiref/qubitekk.rst b/doc/source/apiref/qubitekk.rst index b86dc6253..8f80571f2 100644 --- a/doc/source/apiref/qubitekk.rst +++ b/doc/source/apiref/qubitekk.rst @@ -1,8 +1,8 @@ .. TODO: put documentation license header here. - + .. currentmodule:: instruments.qubitekk - + ======== Qubitekk ======== @@ -13,7 +13,7 @@ Qubitekk .. autoclass:: CC1 :members: :undoc-members: - + :class:`MC1` Motor Controller ============================= diff --git a/doc/source/apiref/rigol.rst b/doc/source/apiref/rigol.rst index 7e585d521..0feac2703 100644 --- a/doc/source/apiref/rigol.rst +++ b/doc/source/apiref/rigol.rst @@ -1,16 +1,15 @@ .. TODO: put documentation license header here. - + .. currentmodule:: instruments.rigol - + ===== Rigol ===== - + :class:`RigolDS1000Series` Oscilloscope ======================================= .. autoclass:: RigolDS1000Series :members: :undoc-members: - diff --git a/doc/source/apiref/srs.rst b/doc/source/apiref/srs.rst index 95fc6f6a2..31ce8f572 100644 --- a/doc/source/apiref/srs.rst +++ b/doc/source/apiref/srs.rst @@ -1,8 +1,8 @@ .. TODO: put documentation license header here. - + .. currentmodule:: instruments.srs - + ========================= Stanford Research Systems ========================= @@ -13,7 +13,7 @@ Stanford Research Systems .. autoclass:: SRS345 :members: :undoc-members: - + :class:`SRS830` Lock-In Amplifier ================================= @@ -34,7 +34,3 @@ Stanford Research Systems .. autoclass:: SRSDG645 :members: :undoc-members: - -.. autoclass:: _SRSDG645Channel - :members: - :undoc-members: diff --git a/doc/source/apiref/tektronix.rst b/doc/source/apiref/tektronix.rst index a14cbda5c..03123626b 100644 --- a/doc/source/apiref/tektronix.rst +++ b/doc/source/apiref/tektronix.rst @@ -1,8 +1,8 @@ .. TODO: put documentation license header here. - + .. currentmodule:: instruments.tektronix - + ========= Tektronix ========= @@ -13,21 +13,13 @@ Tektronix .. autoclass:: TekAWG2000 :members: :undoc-members: - + :class:`TekDPO4104` Oscilloscope ================================ .. autoclass:: TekDPO4104 :members: :undoc-members: - -.. autoclass:: _TekDPO4104DataSource - :members: - :undoc-members: - -.. autoclass:: _TekDPO4104Channel - :members: - :undoc-members: :class:`TekDPO70000` Oscilloscope ================================= diff --git a/doc/source/apiref/teledyne.rst b/doc/source/apiref/teledyne.rst new file mode 100644 index 000000000..55082019b --- /dev/null +++ b/doc/source/apiref/teledyne.rst @@ -0,0 +1,15 @@ +.. + TODO: put documentation license header here. + +.. currentmodule:: instruments.teledyne + +=============== +Teledyne-LeCroy +=============== + +:class:`MAUI` Oscilloscope Controller +======================================= + +.. autoclass:: MAUI + :members: + :undoc-members: diff --git a/doc/source/apiref/thorlabs.rst b/doc/source/apiref/thorlabs.rst index 6abf2f88c..bcae1c402 100644 --- a/doc/source/apiref/thorlabs.rst +++ b/doc/source/apiref/thorlabs.rst @@ -1,8 +1,8 @@ .. TODO: put documentation license header here. - + .. currentmodule:: instruments.thorlabs - + ======== ThorLabs ======== @@ -21,6 +21,10 @@ ThorLabs :members: :undoc-members: +.. autoclass:: APTPiezoInertiaActuator + :members: + :undoc-members: + .. autoclass:: APTPiezoStage :members: :undoc-members: diff --git a/doc/source/apiref/toptica.rst b/doc/source/apiref/toptica.rst index 91afaec0a..3ba100e4c 100644 --- a/doc/source/apiref/toptica.rst +++ b/doc/source/apiref/toptica.rst @@ -1,8 +1,8 @@ .. TODO: put documentation license header here. - + .. currentmodule:: instruments.toptica - + ======= Toptica ======= diff --git a/doc/source/apiref/yokogawa.rst b/doc/source/apiref/yokogawa.rst index 372ffd223..0fa66ac93 100644 --- a/doc/source/apiref/yokogawa.rst +++ b/doc/source/apiref/yokogawa.rst @@ -1,16 +1,22 @@ .. TODO: put documentation license header here. - + .. currentmodule:: instruments.yokogawa - + ======== Yokogawa ======== +:class:`Yokogawa6370` Optical Spectrum Analyzer +=============================================== + +.. autoclass:: Yokogawa6370 + :members: + :undoc-members: + :class:`Yokogawa7651` Power Supply ================================== .. autoclass:: Yokogawa7651 :members: :undoc-members: - diff --git a/doc/source/conf.py b/doc/source/conf.py index 49f94c4b0..4fa33ded7 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # InstrumentKit Library documentation build configuration file, created by # sphinx-quickstart on Fri Apr 5 10:37:03 2013. @@ -11,202 +10,213 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os -from instruments import __version__ +from importlib.metadata import version +import os +import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('../../')) +sys.path.insert(0, os.path.abspath("../../")) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.coverage', 'sphinx.ext.mathjax', 'sphinx.ext.viewcode'] +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.doctest", + "sphinx.ext.intersphinx", + "sphinx.ext.coverage", + "sphinx.ext.mathjax", + "sphinx.ext.viewcode", +] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'InstrumentKit Library' -copyright = u'2013-2016, Steven Casagrande' +project = "InstrumentKit Library" +copyright = "2013-2022, Steven Casagrande" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # -# The short X.Y version. -version = __version__ -# The full version, including alpha/beta/rc tags. -release = version +# The full release version +release = version("instrumentkit") +# The short X.Y version +version = ".".join(release.split(".")[:2]) # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all documents. -default_role = 'obj' +default_role = "obj" # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = "default" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'InstrumentKitLibrarydoc' +htmlhelp_basename = "InstrumentKitLibrarydoc" # -- Options for LaTeX output -------------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'InstrumentKitLibrary.tex', u'InstrumentKit Library Documentation', - u'Steven Casagrande', 'manual'), + ( + "index", + "InstrumentKitLibrary.tex", + "InstrumentKit Library Documentation", + "Steven Casagrande", + "manual", + ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output -------------------------------------------- @@ -214,12 +224,17 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'instrumentkitlibrary', u'InstrumentKit Library Documentation', - [u'Steven Casagrande'], 1) + ( + "index", + "instrumentkitlibrary", + "InstrumentKit Library Documentation", + ["Steven Casagrande"], + 1, + ) ] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ @@ -228,27 +243,33 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'InstrumentKitLibrary', u'InstrumentKit Library Documentation', - u'Steven Casagrande', 'InstrumentKitLibrary', 'One line description of project.', - 'Miscellaneous'), + ( + "index", + "InstrumentKitLibrary", + "InstrumentKit Library Documentation", + "Steven Casagrande", + "InstrumentKitLibrary", + "One line description of project.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { - 'http://docs.python.org/': None, - 'numpy': ('http://docs.scipy.org/doc/numpy', None), - 'serial': ('http://pyserial.sourceforge.net/', None), - 'quantities': ('http://pythonhosted.org/quantities/', None), + "http://docs.python.org/": None, + "numpy": ("http://docs.scipy.org/doc/numpy", None), + "serial": ("http://pyserial.sourceforge.net/", None), + "pint": ("https://pint.readthedocs.io/en/stable/", None), } -autodoc_member_order = 'groupwise' +autodoc_member_order = "groupwise" diff --git a/doc/source/devguide/code_style.rst b/doc/source/devguide/code_style.rst index 11247bde7..fa435abad 100644 --- a/doc/source/devguide/code_style.rst +++ b/doc/source/devguide/code_style.rst @@ -10,7 +10,7 @@ Data Types Numeric Data ------------ -When appropriate, use :class:`quantities.Quantity` objects to track units. +When appropriate, use :class:`pint.Quantity` objects to track units. If this is not possible or appropriate, use a bare `float` for scalars and `np.ndarray` for array-valued data. @@ -92,7 +92,7 @@ For example:: class SomeInstrument(Instrument): # If there's a more appropriate base class, please use it # in preference to object! - class Channel(object): + class Channel: # We use a three-argument initializer, # to remember which instrument this channel belongs to, # as well as its index or label on that instrument. @@ -114,7 +114,7 @@ and appears with the instrument in documentation. Since this convention is somewhat recent, you may find older code that uses a style more like this:: - class _SomeInstrumentChannel(object): + class _SomeInstrumentChannel: # stuff class SomeInstrument(Instrument): @@ -126,7 +126,7 @@ This can be redefined in a backwards-compatible way by bringing the channel class inside, then defining a new module-level variable for the old name:: class SomeInstrument(Instrument): - class Channel(object): + class Channel: # stuff @property diff --git a/doc/source/devguide/design_philosophy.rst b/doc/source/devguide/design_philosophy.rst index 11b2a9157..4cf935ccc 100644 --- a/doc/source/devguide/design_philosophy.rst +++ b/doc/source/devguide/design_philosophy.rst @@ -57,4 +57,3 @@ dimensionality of values to be sent to the device without regards for what the instrument expects; the unit conversions will be handled by InstrumentKit in a way that ensures that the expectations of the instrument are properly met, irrespective of what the user knows. - diff --git a/doc/source/devguide/index.rst b/doc/source/devguide/index.rst index d0ef44b4c..6f2aeff2e 100644 --- a/doc/source/devguide/index.rst +++ b/doc/source/devguide/index.rst @@ -9,7 +9,7 @@ InstrumentKit Development Guide code_style testing util_fns - + Introduction ============ @@ -21,21 +21,28 @@ Getting Started To get started with development for InstrumentKit, a few additional supporting packages must be installed. The core development packages can be found in -the supporting requirements file named ``dev-requirements.txt``. These will -allow you to run the tests and check that all your code changes follow our -linting rules (through `pylint`). +`setup.cfg` under the `dev` extras dependencies. These will allow you to run +the tests. + +This repo also contains a series of static code checks that are managed +via ``pre-commit``. This tool, once setup, will manage running all of these +checks prior to each commit on your local machine.:: + +$ pip install pre-commit +$ pre-commit install + +These checks are also run in CI, and must pass in order to generate +a passing build. It is suggested that you install the git hooks, but +they can be run manually on all files. See the ``pre-commit`` homepage +for more information. Required Development Dependencies --------------------------------- Using ``pip``, these requirements can be obtained automatically by using the -provided ``dev-requirements.txt``:: - -$ pip install -r dev-requirements.txt +provided project definitions:: -- mock -- nose -- pylint +$ pip install -e .[dev] Optional Development Dependencies --------------------------------- diff --git a/doc/source/devguide/testing.rst b/doc/source/devguide/testing.rst index e2ba4d9bc..47d6232a2 100644 --- a/doc/source/devguide/testing.rst +++ b/doc/source/devguide/testing.rst @@ -20,13 +20,13 @@ of InstrumentKit will not, in general, have access to each instrument that is supported--- we rely on automated testing to ensure that future changes do not cause invalid or undesired operation. -For InstrumentKit, we rely heavily on `nose`_, a mature and flexible +For InstrumentKit, we rely heavily on `pytest`_, a mature and flexible unit-testing framework for Python. When run from the command line via -``nosetests``, or when run by Travis CI, nose will automatically execute +``pytest``, or when run by Travis CI, pytest will automatically execute functions and methods whose names start with ``test`` in packages, modules and classes whose names start with ``test`` or ``Test``, depending. (Please -see the `nose`_ documentation for full details, as this is not intended -to be a guide to nose so much as a guide to how we use it in IK.) +see the `pytest`_ documentation for full details, as this is not intended +to be a guide to pytest so much as a guide to how we use it in IK.) Because of this, we keep all test cases in the ``instruments.tests`` package, under a subpackage named for the particular manufacturer, such as ``instruments.tests.test_srs``. The tests for each instrument should @@ -63,7 +63,7 @@ a simple test case for :class:`instruments.srs.SRSDG645``:: ], sep="\n" ) as ddg: - unit_eq(ddg.output['AB'].level_amplitude, pq.Quantity(3.2, "V")) + unit_eq(ddg.output['AB'].level_amplitude, u.Quantity(3.2, "V")) ddg.output['AB'].level_amplitude = 4.0 Here, we see that the test has a name beginning with ``test_``, has a simple @@ -88,4 +88,4 @@ Protocol Assertion Functions .. autofunction:: expected_protocol -.. _nose: https://nose.readthedocs.org/en/latest/ \ No newline at end of file +.. _pytest: https://docs.pytest.org/en/latest/ diff --git a/doc/source/devguide/util_fns.rst b/doc/source/devguide/util_fns.rst index 2d2719a91..afc81283a 100644 --- a/doc/source/devguide/util_fns.rst +++ b/doc/source/devguide/util_fns.rst @@ -74,13 +74,13 @@ These properties, when implemented in your class, might look like this:: voltage, voltage_min, voltage_max = bounded_unitful_property( voltage = unitful_property( "VOLT", - pq.volt, - valid_range=(0*quantities.volt, 10*quantities.volt) + u.volt, + valid_range=(0*u.volt, 10*u.volt) doc=""" Gets/sets the output voltage. :units: As specified, or assumed to be :math:`\\text{V}` otherwise. - :type: `float` or `~quantities.Quantity` + :type: `float` or `~pint.Quantity` """ ) @@ -129,18 +129,18 @@ the `~instruments.thorlabs.TC200` class:: temperature = unitful_property( "tact", - units=pq.degC, + units=u.degC, readonly=True, input_decoration=lambda x: x.replace( " C", "").replace(" F", "").replace(" K", ""), doc=""" Gets the actual temperature of the sensor - :units: As specified (if a `~quantities.quantity.Quantity`) or assumed + :units: As specified (if a `~pint.Quantity`) or assumed to be of units degrees C. - :type: `~quantities.quantity.Quantity` or `int` + :type: `~pint.Quantity` or `int` :return: the temperature (in degrees C) - :rtype: `~quantities.quantity.Quantity` + :rtype: `~pint.Quantity` """ ) @@ -183,3 +183,14 @@ String Property .. autofunction:: string_property +Named Structures +================ + +The :class:`~instruments.named_struct.NamedStruct` class can be used to represent +C-style structures for serializing and deserializing data. + +.. autoclass:: instruments.named_struct.NamedStruct + +.. autoclass:: instruments.named_struct.Field + +.. autoclass:: instruments.named_struct.Padding diff --git a/doc/source/index.rst b/doc/source/index.rst index 443e0d8f9..f8e884c38 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -1,6 +1,6 @@ .. TODO: put documentation license header here. - + Welcome to InstrumentKit Library's documentation! ================================================= @@ -22,4 +22,3 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` - diff --git a/doc/source/intro.rst b/doc/source/intro.rst index bc8c18ac8..b9bce4f85 100644 --- a/doc/source/intro.rst +++ b/doc/source/intro.rst @@ -1,6 +1,6 @@ .. TODO: put documentation license header here. - + ============ Introduction ============ @@ -19,39 +19,6 @@ Dependencies Most of the required and optional dependencies can be obtained using ``pip``. -Required Dependencies -~~~~~~~~~~~~~~~~~~~~~ - -Using ``pip``, these requirements can be obtained automatically by using the -provided ``requirements.txt``:: - -$ pip install -r requirements.txt - -- NumPy -- `PySerial`_ -- `quantities`_ -- `enum34`_ -- `future`_ -- `python-vxi11`_ -- `PyUSB`_ -- `python-usbtmc`_ -- `PyYAML`_ - -Optional Dependencies -~~~~~~~~~~~~~~~~~~~~~ - -- `PyVISA`_ (required for accessing instruments via VISA library) - -.. _PySerial: http://pyserial.sourceforge.net/ -.. _quantities: http://pythonhosted.org/quantities/ -.. _enum34: https://pypi.python.org/pypi/enum34 -.. _future: https://pypi.python.org/pypi/future -.. _PyYAML: https://bitbucket.org/xi/pyyaml -.. _PyUSB: http://sourceforge.net/apps/trac/pyusb/ -.. _PyVISA: http://pyvisa.sourceforge.net/ -.. _python-usbtmc: https://pypi.python.org/pypi/python-usbtmc -.. _python-vxi11: https://pypi.python.org/pypi/python-vxi11 - Getting Started =============== @@ -144,8 +111,7 @@ For instance, to add a Tektronix DPO 4104 oscilloscope with world-writable permissions, add the following to rules.d:: ATTRS{idVendor}=="0699", ATTRS{idProduct}=="0401", SYMLINK+="tekdpo4104", MODE="0666" - + .. warning:: This configuration causes the USB device to be world-writable. Do not do this on a multi-user system with untrusted users. - diff --git a/instruments/__init__.py b/instruments/__init__.py index 562ae3bae..9b85d235f 100644 --- a/instruments/__init__.py +++ b/instruments/__init__.py @@ -1,22 +1,26 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Defines globally-available subpackages and symbols for the instruments package. """ # IMPORTS #################################################################### -from __future__ import absolute_import +__all__ = ["units"] + from . import abstract_instruments from .abstract_instruments import Instrument from . import agilent from . import generic_scpi +from . import fluke +from . import gentec_eo +from . import glassman from . import holzworth from . import hp from . import keithley from . import lakeshore +from . import minghe from . import newport from . import oxford from . import phasematrix @@ -25,25 +29,10 @@ from . import rigol from . import srs from . import tektronix +from . import teledyne from . import thorlabs from . import toptica from . import yokogawa -from . import units from .config import load_instruments - -# VERSION METADATA ########################################################### -# In keeping with PEP-396, we define a version number of the form -# {major}.{minor}[.{postrelease}]{prerelease-tag} - -__version__ = "0.3.1" - -__title__ = "instrumentkit" -__description__ = "Test and measurement communication library" -__uri__ = "https://instrumentkit.readthedocs.org/" - -__author__ = "Steven Casagrande" -__email__ = "scasagrande@galvant.ca" - -__license__ = "AGPLv3" -__copyright__ = "Copyright (c) 2012-2016 Steven Casagrande" +from .units import ureg as units diff --git a/instruments/abstract_instruments/__init__.py b/instruments/abstract_instruments/__init__.py index 7b7eea482..42a9591f5 100644 --- a/instruments/abstract_instruments/__init__.py +++ b/instruments/abstract_instruments/__init__.py @@ -1,21 +1,13 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing instrument abstract base classes and communication layers """ -from __future__ import absolute_import from .instrument import Instrument -from .multimeter import Multimeter from .electrometer import Electrometer from .function_generator import FunctionGenerator -from .oscilloscope import ( - OscilloscopeChannel, - OscilloscopeDataSource, - Oscilloscope, -) -from .power_supply import ( - PowerSupplyChannel, - PowerSupply, -) +from .multimeter import Multimeter +from .oscilloscope import Oscilloscope +from .optical_spectrum_analyzer import OpticalSpectrumAnalyzer +from .power_supply import PowerSupply diff --git a/instruments/abstract_instruments/comm/__init__.py b/instruments/abstract_instruments/comm/__init__.py index 0cf070538..a3a4b1a60 100644 --- a/instruments/abstract_instruments/comm/__init__.py +++ b/instruments/abstract_instruments/comm/__init__.py @@ -1,19 +1,17 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing communication layers """ -from __future__ import absolute_import from .abstract_comm import AbstractCommunicator +from .file_communicator import FileCommunicator +from .gpib_communicator import GPIBCommunicator +from .loopback_communicator import LoopbackCommunicator +from .serial_communicator import SerialCommunicator from .socket_communicator import SocketCommunicator from .usb_communicator import USBCommunicator -from .serial_communicator import SerialCommunicator -from .visa_communicator import VisaCommunicator -from .loopback_communicator import LoopbackCommunicator -from .gi_gpib_communicator import GPIBCommunicator -from .file_communicator import FileCommunicator from .usbtmc_communicator import USBTMCCommunicator +from .visa_communicator import VisaCommunicator from .vxi11_communicator import VXI11Communicator diff --git a/instruments/abstract_instruments/comm/abstract_comm.py b/instruments/abstract_instruments/comm/abstract_comm.py index e15547760..8abd6cd13 100644 --- a/instruments/abstract_instruments/comm/abstract_comm.py +++ b/instruments/abstract_instruments/comm/abstract_comm.py @@ -1,24 +1,20 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides an abstract base class for file-like communication layer classes """ # IMPORTS #################################################################### -from __future__ import absolute_import -from __future__ import division -from __future__ import unicode_literals import abc +import codecs import logging - -from future.utils import with_metaclass +import struct # CLASSES #################################################################### -class AbstractCommunicator(with_metaclass(abc.ABCMeta, object)): +class AbstractCommunicator(metaclass=abc.ABCMeta): """ Abstract base class for electrometer instruments. @@ -42,9 +38,9 @@ def __init__(self, *args, **kwargs): # pylint: disable=unused-argument # FORMATTING METHODS # def __repr__(self): - return "<{} object at 0x{:X} "\ - "connected to {}>".format( - type(self).__name__, id(self), repr(self.address)) + return "<{} object at 0x{:X} " "connected to {}>".format( + type(self).__name__, id(self), repr(self.address) + ) # CONCRETE PROPERTIES # @@ -124,7 +120,6 @@ def read_raw(self, size=-1): :return: The read bytes :rtype: `bytes` """ - pass @abc.abstractmethod def write_raw(self, msg): @@ -134,7 +129,6 @@ def write_raw(self, msg): :param bytes msg: Bytes to be sent to the instrument over the connection. """ - pass @abc.abstractmethod def _sendcmd(self, msg): @@ -145,7 +139,6 @@ def _sendcmd(self, msg): Note that this is called by :class:`AbstractCommunicator.sendcmd`, which also handles debug, event and capture support. """ - pass @abc.abstractmethod def _query(self, msg, size=-1): @@ -162,7 +155,6 @@ def _query(self, msg, size=-1): Note that this is called by :class:`AbstractCommunicator.query`, which also handles debug, event and capture support. """ - pass @abc.abstractmethod def flush_input(self): @@ -202,7 +194,14 @@ def read(self, size=-1, encoding="utf-8"): :return: The read string from the connection :rtype: `str` """ - return self.read_raw(size).decode(encoding) + try: + codecs.lookup(encoding) + return self.read_raw(size).decode(encoding) + except LookupError: + if encoding == "IEEE-754/64": + return struct.unpack(">d", self.read_raw(size))[0] + else: + raise ValueError(f"Encoding {encoding} is not currently supported.") def sendcmd(self, msg): """ diff --git a/instruments/abstract_instruments/comm/file_communicator.py b/instruments/abstract_instruments/comm/file_communicator.py index b6022e7ce..b3084489a 100644 --- a/instruments/abstract_instruments/comm/file_communicator.py +++ b/instruments/abstract_instruments/comm/file_communicator.py @@ -1,22 +1,16 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides a communication layer for an instrument with a file on the filesystem """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division -from __future__ import unicode_literals import errno import io import time import logging -from builtins import str, bytes - from instruments.abstract_instruments.comm import AbstractCommunicator logger = logging.getLogger(__name__) @@ -40,9 +34,9 @@ class FileCommunicator(io.IOBase, AbstractCommunicator): """ def __init__(self, filelike): - super(FileCommunicator, self).__init__(self) + super().__init__(self) if isinstance(filelike, str): # pragma: no cover - filelike = open(filelike, 'rb+') + filelike = open(filelike, "rb+") self._filelike = filelike self._terminator = "\n" @@ -58,15 +52,16 @@ def address(self): :type: `str` """ - if hasattr(self._filelike, 'name'): + if hasattr(self._filelike, "name"): return self._filelike.name - else: - return None + + return None @address.setter def address(self, newval): - raise NotImplementedError("Changing addresses of a file communicator" - " is not yet supported.") + raise NotImplementedError( + "Changing addresses of a file communicator" " is not yet supported." + ) @property def terminator(self): @@ -82,8 +77,10 @@ def terminator(self, newval): if isinstance(newval, bytes): newval = newval.decode("utf-8") if not isinstance(newval, str) or len(newval) > 1: - raise TypeError("Terminator for socket communicator must be " - "specified as a single character string.") + raise TypeError( + "Terminator for socket communicator must be " + "specified as a single character string." + ) self._terminator = newval @property @@ -106,7 +103,7 @@ def close(self): """ try: self._filelike.close() - except IOError as e: # pragma: no cover + except OSError as e: # pragma: no cover logger.warning("Failed to close file, exception: %s", repr(e)) def read_raw(self, size=-1): @@ -120,10 +117,10 @@ def read_raw(self, size=-1): return self._filelike.read(size) elif size == -1: result = bytes() - c = b'' + c = b"" while c != self._terminator.encode("utf-8"): c = self._filelike.read(1) - if c == b'': + if c == b"": break if c != self._terminator.encode("utf-8"): result += c @@ -180,7 +177,7 @@ def _sendcmd(self, msg): self.write(msg) try: self.flush() - except IOError as e: + except OSError as e: logger.warning("Exception %s occured during flush().", repr(e)) def _query(self, msg, size=-1): @@ -209,9 +206,9 @@ def _query(self, msg, size=-1): break resp += nextchar if nextchar.endswith(self._terminator.encode("utf-8")): - resp = resp[:-len(self._terminator)] + resp = resp[: -len(self._terminator)] break - except IOError as ex: + except OSError as ex: if ex.errno == errno.ETIMEDOUT: # We don't mind timeouts if resp is nonempty, # and will just return what we have. @@ -220,11 +217,12 @@ def _query(self, msg, size=-1): elif ex.errno != errno.EPIPE: raise # Reraise the existing exception. else: # Give a more helpful and specific exception. - raise IOError( + raise OSError( "Pipe broken when reading from {}; this probably " "indicates that the driver " "providing the device file is unable to communicate with " "the instrument. Consider restarting the instrument.".format( self.address - )) + ) + ) return resp.decode("utf-8") diff --git a/instruments/abstract_instruments/comm/gi_gpib_communicator.py b/instruments/abstract_instruments/comm/gpib_communicator.py similarity index 70% rename from instruments/abstract_instruments/comm/gi_gpib_communicator.py rename to instruments/abstract_instruments/comm/gpib_communicator.py index ab686bb15..63bf382a7 100644 --- a/instruments/abstract_instruments/comm/gi_gpib_communicator.py +++ b/instruments/abstract_instruments/comm/gpib_communicator.py @@ -1,21 +1,17 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides a communication layer for an instrument connected via a Galvant -Industries GPIB adapter. +Industries or Prologix GPIB adapter. """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division -from __future__ import unicode_literals +from enum import Enum import io import time -from builtins import chr, str, bytes -import quantities as pq +from instruments.units import ureg as u from instruments.abstract_instruments.comm import AbstractCommunicator from instruments.util_fns import assume_units @@ -27,28 +23,43 @@ class GPIBCommunicator(io.IOBase, AbstractCommunicator): """ Communicates with a SocketCommunicator or SerialCommunicator object for - use with Galvant Industries GPIBUSB or GPIBETHERNET adapters. + use with Galvant Industries or Prologix GPIBUSB or GPIBETHERNET adapters. It essentially wraps those physical communication layers with the extra - overhead required by the Galvant GPIB adapters. + overhead required by the GPIB adapters. """ # pylint: disable=too-many-instance-attributes - def __init__(self, filelike, gpib_address): - super(GPIBCommunicator, self).__init__(self) - + def __init__(self, filelike, gpib_address, model="gi"): + super().__init__(self) + self._model = self.Model(model) self._file = filelike self._gpib_address = gpib_address self._file.terminator = "\r" - self._version = int(self._file.query("+ver")) + if self._model == GPIBCommunicator.Model.gi: + self._version = int(self._file.query("+ver")) + if self._model == GPIBCommunicator.Model.pl: + self._file.sendcmd("++auto 0") self._terminator = None self.terminator = "\n" self._eoi = True - self._timeout = 1000 * pq.millisecond - if self._version <= 4: + self._timeout = 1000 * u.millisecond + if self._model == GPIBCommunicator.Model.gi and self._version <= 4: self._eos = 10 else: - self._eos = "\n" # pylint: disable=redefined-variable-type + self._eos = "\n" + + # ENUMS # + + class Model(Enum): + """ + Enum containing the supported GPIB controller models + """ + + #: Galvant Industries + gi = "gi" + #: Prologix, LLC + pl = "pl" # PROPERTIES # @@ -87,22 +98,22 @@ def timeout(self): Gets/sets the timeeout of both the GPIB bus and the connection channel between the PC and the GPIB adapter. - :type: `~quantities.Quantity` + :type: `~pint.Quantity` :units: As specified, or assumed to be of units ``seconds`` """ return self._timeout @timeout.setter def timeout(self, newval): - newval = assume_units(newval, pq.second) - if self._version <= 4: - newval = newval.rescale(pq.second) - self._file.sendcmd('+t:{}'.format(newval.magnitude)) - elif self._version >= 5: - newval = newval.rescale(pq.millisecond) - self._file.sendcmd("++read_tmo_ms {}".format(newval.magnitude)) - self._file.timeout = newval.rescale(pq.second) - self._timeout = newval.rescale(pq.second) + newval = assume_units(newval, u.second) + if self._model == GPIBCommunicator.Model.gi and self._version <= 4: + newval = newval.to(u.second) + self._file.sendcmd(f"+t:{int(newval.magnitude)}") + else: + newval = newval.to(u.millisecond) + self._file.sendcmd(f"++read_tmo_ms {int(newval.magnitude)}") + self._file.timeout = newval.to(u.second) + self._timeout = newval.to(u.second) @property def terminator(self): @@ -117,8 +128,8 @@ def terminator(self): """ if not self._eoi: return self._terminator - else: - return 'eoi' + + return "eoi" @terminator.setter def terminator(self, newval): @@ -127,8 +138,8 @@ def terminator(self, newval): if isinstance(newval, str): newval = newval.lower() - if self._version <= 4: - if newval == 'eoi': + if self._model == GPIBCommunicator.Model.gi and self._version <= 4: + if newval == "eoi": self.eoi = True elif not isinstance(newval, int): if len(newval) == 1: @@ -136,19 +147,23 @@ def terminator(self, newval): self.eoi = False self.eos = newval else: - raise TypeError('GPIB termination must be integer 0-255 ' - 'represending decimal value of ASCII ' - 'termination character or a string' - 'containing "eoi".') + raise TypeError( + "GPIB termination must be integer 0-255 " + "represending decimal value of ASCII " + "termination character or a string" + 'containing "eoi".' + ) elif (newval < 0) or (newval > 255): - raise ValueError('GPIB termination must be integer 0-255 ' - 'represending decimal value of ASCII ' - 'termination character.') + raise ValueError( + "GPIB termination must be integer 0-255 " + "represending decimal value of ASCII " + "termination character." + ) else: self.eoi = False self.eos = newval self._terminator = chr(newval) - elif self._version >= 5: + else: if newval != "eoi": self.eos = newval self.eoi = False @@ -182,10 +197,10 @@ def eoi(self, newval): if not isinstance(newval, bool): raise TypeError("EOI status must be specified as a boolean") self._eoi = newval - if self._version >= 5: - self._file.sendcmd("++eoi {}".format('1' if newval else '0')) + if self._model == GPIBCommunicator.Model.gi and self._version <= 4: + self._file.sendcmd("+eoi:{}".format("1" if newval else "0")) else: - self._file.sendcmd("+eoi:{}".format('1' if newval else '0')) + self._file.sendcmd("++eoi {}".format("1" if newval else "0")) @property def eos(self): @@ -203,13 +218,12 @@ def eos(self): @eos.setter def eos(self, newval): - # pylint: disable=redefined-variable-type - if self._version <= 4: + if self._model == GPIBCommunicator.Model.gi and self._version <= 4: if isinstance(newval, (str, bytes)): newval = ord(newval) - self._file.sendcmd("+eos:{}".format(newval)) + self._file.sendcmd(f"+eos:{newval}") self._eos = newval - elif self._version >= 5: + else: if isinstance(newval, int): newval = str(chr(newval)) if newval == "\r\n": @@ -226,7 +240,7 @@ def eos(self, newval): newval = 3 else: raise ValueError("EOS must be CRLF, CR, LF, or None") - self._file.sendcmd("++eos {}".format(newval)) + self._file.sendcmd(f"++eos {newval}") # FILE-LIKE METHODS # @@ -300,17 +314,20 @@ def flush_input(self): def _sendcmd(self, msg): """ This is the implementation of ``sendcmd`` for communicating with - the Galvant Industries GPIB adapter. This function is in turn wrapped by - the concrete method `AbstractCommunicator.sendcmd` to provide consistent + the GPIB adapters. This function is in turn wrapped by the concrete + method `AbstractCommunicator.sendcmd` to provide consistent logging functionality across all communication layers. :param str msg: The command message to send to the instrument """ sleep_time = 0.01 - if msg == '': + if msg == "": return - self._file.sendcmd('+a:' + str(self._gpib_address)) + if self._model == GPIBCommunicator.Model.gi: + self._file.sendcmd(f"+a:{str(self._gpib_address)}") + else: + self._file.sendcmd(f"++addr {str(self._gpib_address)}") time.sleep(sleep_time) self.eoi = self.eoi time.sleep(sleep_time) @@ -324,13 +341,18 @@ def _sendcmd(self, msg): def _query(self, msg, size=-1): """ This is the implementation of ``query`` for communicating with - the Galvant Industries GPIB adapter. This function is in turn wrapped by - the concrete method `AbstractCommunicator.query` to provide consistent + the GPIB adapters. This function is in turn wrapped by the concrete + method `AbstractCommunicator.query` to provide consistent logging functionality across all communication layers. - If a ``?`` is not present in ``msg`` then the adapter will be - instructed to get the response from the instrument via the ``+read`` - command. + The Galvant Industries adaptor is set to automatically get a + response if a ``?`` is present in ``msg``. If it is not present, + then the adapter will be instructed to get the response from the + instrument via the ``+read`` command. + + The Prologix adapter is set to not get a response unless told to do + so. It is instructed to get a response from the instrument via the + ``++read`` command. :param str msg: The query message to send to the instrument :param int size: The number of bytes to read back from the instrument @@ -339,6 +361,8 @@ def _query(self, msg, size=-1): :rtype: `str` """ self.sendcmd(msg) - if '?' not in msg: - self._file.sendcmd('+read') + if self._model == GPIBCommunicator.Model.gi and "?" not in msg: + self._file.sendcmd("+read") + if self._model == GPIBCommunicator.Model.pl: + self._file.sendcmd("++read") return self._file.read(size).strip() diff --git a/instruments/abstract_instruments/comm/loopback_communicator.py b/instruments/abstract_instruments/comm/loopback_communicator.py index 85020ec3c..5687dca9a 100644 --- a/instruments/abstract_instruments/comm/loopback_communicator.py +++ b/instruments/abstract_instruments/comm/loopback_communicator.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides a loopback communicator, used for creating unit tests or for opening test connections to explore the InstrumentKit API. @@ -7,16 +6,10 @@ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals import io import sys -from builtins import input, bytes, str - from instruments.abstract_instruments.comm import AbstractCommunicator # CLASSES ##################################################################### @@ -32,7 +25,7 @@ class LoopbackCommunicator(io.IOBase, AbstractCommunicator): """ def __init__(self, stdin=None, stdout=None): - super(LoopbackCommunicator, self).__init__(self) + super().__init__(self) self._terminator = "\n" self._stdout = stdout self._stdin = stdin @@ -68,8 +61,10 @@ def terminator(self, newval): if isinstance(newval, bytes): newval = newval.decode("utf-8") if not isinstance(newval, str): - raise TypeError("Terminator for loopback communicator must be " - "specified as a byte or unicode string.") + raise TypeError( + "Terminator for loopback communicator must be " + "specified as a byte or unicode string." + ) self._terminator = newval @property @@ -94,7 +89,7 @@ def close(self): """ try: self._stdin.close() - except IOError: + except OSError: pass def read_raw(self, size=-1): @@ -108,17 +103,21 @@ def read_raw(self, size=-1): :rtype: `bytes` """ if self._stdin is not None: - if size >= 0: + if size == -1 or size is None: + result = bytes() + if self._terminator: + while result.endswith(self._terminator.encode("utf-8")) is False: + c = self._stdin.read(1) + if c == b"": + break + result += c + return result[: -len(self._terminator)] + return self._stdin.read(-1) + + elif size >= 0: input_var = self._stdin.read(size) return bytes(input_var) - elif size == -1: - result = bytes() - while result.endswith(self._terminator.encode("utf-8")) is False: - c = self._stdin.read(1) - if c == b'': - break - result += c - return result[:-len(self._terminator)] + else: raise ValueError("Must read a positive value of characters.") else: @@ -136,7 +135,7 @@ def write_raw(self, msg): if self._stdout is not None: self._stdout.write(msg) else: - print(" <- {} ".format(repr(msg))) + print(f" <- {repr(msg)} ") def seek(self, offset): # pylint: disable=unused-argument,no-self-use """ @@ -160,7 +159,6 @@ def flush_input(self): For the loopback communicator, this will do nothing and just `pass`. """ - pass # METHODS # @@ -173,8 +171,8 @@ def _sendcmd(self, msg): :param str msg: The command message to send to the instrument """ - if msg != '': - msg = "{}{}".format(msg, self._terminator) + if msg != "": + msg = f"{msg}{self._terminator}" self.write(msg) def _query(self, msg, size=-1): diff --git a/instruments/abstract_instruments/comm/serial_communicator.py b/instruments/abstract_instruments/comm/serial_communicator.py index 02b88f23c..459d6bd00 100644 --- a/instruments/abstract_instruments/comm/serial_communicator.py +++ b/instruments/abstract_instruments/comm/serial_communicator.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides a serial communicator for connecting with instruments over serial connections. @@ -7,16 +6,11 @@ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division -from __future__ import unicode_literals import io - -from builtins import bytes, str import serial -import quantities as pq +from instruments.units import ureg as u from instruments.abstract_instruments.comm import AbstractCommunicator from instruments.util_fns import assume_units @@ -32,15 +26,14 @@ class SerialCommunicator(io.IOBase, AbstractCommunicator): """ def __init__(self, conn): - super(SerialCommunicator, self).__init__(self) + super().__init__(self) if isinstance(conn, serial.Serial): self._conn = conn self._terminator = "\n" self._debug = False else: - raise TypeError("SerialCommunicator must wrap a serial.Serial " - "object.") + raise TypeError("SerialCommunicator must wrap a serial.Serial " "object.") # PROPERTIES # @@ -76,8 +69,10 @@ def terminator(self, newval): if isinstance(newval, bytes): newval = newval.decode("utf-8") if not isinstance(newval, str): - raise TypeError("Terminator for serial communicator must be " - "specified as a byte or unicode string.") + raise TypeError( + "Terminator for serial communicator must be " + "specified as a byte or unicode string." + ) self._terminator = newval @property @@ -85,14 +80,14 @@ def timeout(self): """ Gets/sets the communication timeout of the serial comm channel. - :type: `~quantities.Quantity` + :type: `~pint.Quantity` :units: As specified or assumed to be of units ``seconds`` """ - return self._conn.timeout * pq.second + return self._conn.timeout * u.second @timeout.setter def timeout(self, newval): - newval = assume_units(newval, pq.second).rescale(pq.second).magnitude + newval = assume_units(newval, u.second).to(u.second).magnitude self._conn.timeout = newval # FILE-LIKE METHODS # @@ -118,13 +113,21 @@ def read_raw(self, size=-1): return resp elif size == -1: result = bytes() - while result.endswith(self._terminator.encode("utf-8")) is False: + # If the terminator is empty, we can't use endswith, but must + # read as many bytes as are available. + # On the other hand, if terminator is nonempty, we can check + # that the tail end of the buffer matches it. + c = None + term = self._terminator.encode("utf-8") if self._terminator else None + while not (result.endswith(term) if term is not None else c == b""): c = self._conn.read(1) - if c == b'': - raise IOError("Serial connection timed out before reading " - "a termination character.") + if c == b"" and term is not None: + raise OSError( + "Serial connection timed out before reading " + "a termination character." + ) result += c - return result[:-len(self._terminator)] + return result[: -len(term)] if term is not None else result else: raise ValueError("Must read a positive value of characters.") diff --git a/instruments/abstract_instruments/comm/serial_manager.py b/instruments/abstract_instruments/comm/serial_manager.py index f82d74842..dcc6bec56 100644 --- a/instruments/abstract_instruments/comm/serial_manager.py +++ b/instruments/abstract_instruments/comm/serial_manager.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ This module handles creating the serial objects for the instrument classes. @@ -10,8 +9,6 @@ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division import weakref import serial @@ -54,15 +51,14 @@ def new_serial_connection(port, baud=460800, timeout=3, write_timeout=3): :rtype: `SerialCommunicator` """ if not isinstance(port, str): - raise TypeError('Serial port must be specified as a string.') + raise TypeError("Serial port must be specified as a string.") if port not in serialObjDict or serialObjDict[port] is None: - conn = SerialCommunicator(serial.Serial( - port, - baudrate=baud, - timeout=timeout, - writeTimeout=write_timeout - )) + conn = SerialCommunicator( + serial.Serial( + port, baudrate=baud, timeout=timeout, writeTimeout=write_timeout + ) + ) serialObjDict[port] = conn # pylint: disable=protected-access if not serialObjDict[port]._conn.isOpen(): diff --git a/instruments/abstract_instruments/comm/socket_communicator.py b/instruments/abstract_instruments/comm/socket_communicator.py index 2d68beab0..0972e09d6 100644 --- a/instruments/abstract_instruments/comm/socket_communicator.py +++ b/instruments/abstract_instruments/comm/socket_communicator.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides a tcpip socket communicator for connecting with instruments over raw ethernet connections. @@ -7,15 +6,11 @@ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division -from __future__ import unicode_literals import io import socket -from builtins import str, bytes -import quantities as pq +from instruments.units import ureg as u from instruments.abstract_instruments.comm import AbstractCommunicator from instruments.util_fns import assume_units @@ -33,15 +28,17 @@ class SocketCommunicator(io.IOBase, AbstractCommunicator): """ def __init__(self, conn): - super(SocketCommunicator, self).__init__(self) + super().__init__(self) if isinstance(conn, socket.socket): self._conn = conn self._terminator = "\n" else: - raise TypeError("SocketCommunicator must wrap a " - ":class:`socket.socket` object, instead got " - "{}".format(type(conn))) + raise TypeError( + "SocketCommunicator must wrap a " + ":class:`socket.socket` object, instead got " + "{}".format(type(conn)) + ) # PROPERTIES # @@ -65,8 +62,10 @@ def terminator(self, newval): if isinstance(newval, bytes): newval = newval.decode("utf-8") if not isinstance(newval, str): - raise TypeError("Terminator for socket communicator must be " - "specified as a byte or unicode string.") + raise TypeError( + "Terminator for socket communicator must be " + "specified as a byte or unicode string." + ) self._terminator = newval @property @@ -74,14 +73,14 @@ def timeout(self): """ Gets/sets the connection timeout of the socket comm channel. - :type: `~quantities.Quantity` + :type: `~pint.Quantity` :units: As specified or assumed to be of units ``seconds`` """ - return self._conn.gettimeout() * pq.second + return self._conn.gettimeout() * u.second @timeout.setter def timeout(self, newval): - newval = assume_units(newval, pq.second).rescale(pq.second).magnitude + newval = assume_units(newval, u.second).to(u.second).magnitude self._conn.settimeout(newval) # FILE-LIKE METHODS # @@ -91,7 +90,7 @@ def close(self): Shutdown and close the `socket.socket` connection. """ try: - self._conn.shutdown() + self._conn.shutdown(socket.SHUT_RDWR) finally: self._conn.close() @@ -110,11 +109,13 @@ def read_raw(self, size=-1): result = bytes() while result.endswith(self._terminator.encode("utf-8")) is False: c = self._conn.recv(1) - if c == b'': - raise IOError("Socket connection timed out before reading " - "a termination character.") + if c == b"": + raise OSError( + "Socket connection timed out before reading " + "a termination character." + ) result += c - return result[:-len(self._terminator)] + return result[: -len(self._terminator)] else: raise ValueError("Must read a positive value of characters.") diff --git a/instruments/abstract_instruments/comm/usb_communicator.py b/instruments/abstract_instruments/comm/usb_communicator.py index 3d7587537..643aa41d4 100644 --- a/instruments/abstract_instruments/comm/usb_communicator.py +++ b/instruments/abstract_instruments/comm/usb_communicator.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides a USB communicator for connecting with instruments over raw usb connections. @@ -7,14 +6,15 @@ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division -from __future__ import unicode_literals import io -from builtins import str + +import usb.core +import usb.util from instruments.abstract_instruments.comm import AbstractCommunicator +from instruments.units import ureg as u +from instruments.util_fns import assume_units # CLASSES ##################################################################### @@ -28,14 +28,48 @@ class USBCommunicator(io.IOBase, AbstractCommunicator): communicators such as `FileCommunicator` (usbtmc on Linux), `VisaCommunicator`, or `USBTMCCommunicator`. - .. warning:: The operational status of this communicator is unknown, - and it is suggested that it is not relied on. + .. warning:: The operational status of this communicator is poorly tested. """ - def __init__(self, conn): - super(USBCommunicator, self).__init__(self) - # TODO: Check to make sure this is a USB connection - self._conn = conn + def __init__(self, dev): + super().__init__(self) + if not isinstance(dev, usb.core.Device): + raise TypeError("USBCommunicator must wrap a usb.core.Device object.") + + # follow (mostly) pyusb tutorial + + # set the active configuration. With no arguments, the first + # configuration will be the active one + dev.set_configuration() + + # get an endpoint instance + cfg = dev.get_active_configuration() + intf = cfg[(0, 0)] + + # initialize in and out endpoints + ep_out = usb.util.find_descriptor( + intf, + # match the first OUT endpoint + custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress) + == usb.util.ENDPOINT_OUT, + ) + + ep_in = usb.util.find_descriptor( + intf, + # match the first OUT endpoint + custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress) + == usb.util.ENDPOINT_IN, + ) + + if (ep_in or ep_out) is None: + raise OSError("USB endpoint not found.") + + # read the maximum package size from the ENDPOINT_IN + self._max_packet_size = ep_in.wMaxPacketSize + + self._dev = dev + self._ep_in = ep_in + self._ep_out = ep_out self._terminator = "\n" # PROPERTIES # @@ -60,20 +94,26 @@ def terminator(self): @terminator.setter def terminator(self, newval): if not isinstance(newval, str): - raise TypeError("Terminator for USBCommunicator must be specified " - "as a single character string.") - if len(newval) > 1: - raise ValueError("Terminator for USBCommunicator must only be 1 " - "character long.") + raise TypeError( + "Terminator for USBCommunicator must be specified " + "as a character string." + ) self._terminator = newval @property def timeout(self): - raise NotImplementedError + """ + Gets/sets the communication timeout of the USB communicator. + + :type: `~pint.Quantity` + :units: As specified or assumed to be of units ``seconds`` + """ + return assume_units(self._dev.default_timeout, u.ms).to(u.second) @timeout.setter def timeout(self, newval): - raise NotImplementedError + newval = assume_units(newval, u.second).to(u.ms).magnitude + self._dev.default_timeout = newval # FILE-LIKE METHODS # @@ -81,40 +121,50 @@ def close(self): """ Shutdown and close the USB connection """ - try: - self._conn.shutdown() - finally: - self._conn.close() + self._dev.reset() + usb.util.dispose_resources(self._dev) def read_raw(self, size=-1): - raise NotImplementedError + """Read raw string back from device and return. - def read(self, size=-1, encoding="utf-8"): - raise NotImplementedError + String returned is most likely shorter than the size requested. Will + terminate by itself. + Read size of -1 will be transformed into 1000 bytes. - def write_raw(self, msg): + :param size: Size to read in bytes + :type size: int """ - Write bytes to the raw usb connection object. + if size == -1: + size = self._max_packet_size + term = self._terminator.encode("utf-8") + read_val = bytes(self._ep_in.read(size)) + if term not in read_val: + raise OSError( + f"Did not find the terminator in the returned string. " + f"Total size of {size} might not be enough." + ) + return read_val.rstrip(term) + + def write_raw(self, msg): + """Write bytes to the raw usb connection object. :param bytes msg: Bytes to be sent to the instrument over the usb connection. """ - self._conn.write(msg) + self._ep_out.write(msg) def seek(self, offset): # pylint: disable=unused-argument,no-self-use - return NotImplemented + raise NotImplementedError def tell(self): # pylint: disable=no-self-use - return NotImplemented + raise NotImplementedError def flush_input(self): """ Instruct the communicator to flush the input buffer, discarding the entirety of its contents. - - Not implemented for usb communicator """ - raise NotImplementedError + self._ep_in.read(self._max_packet_size) # METHODS # @@ -128,7 +178,7 @@ def _sendcmd(self, msg): :param str msg: The command message to send to the instrument """ msg += self._terminator - self._conn.sendall(msg) + self.write(msg) def _query(self, msg, size=-1): """ diff --git a/instruments/abstract_instruments/comm/usbtmc_communicator.py b/instruments/abstract_instruments/comm/usbtmc_communicator.py index 2f96194f6..b4f996edd 100644 --- a/instruments/abstract_instruments/comm/usbtmc_communicator.py +++ b/instruments/abstract_instruments/comm/usbtmc_communicator.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides a communicator that uses Python-USBTMC for connecting with TMC instruments. @@ -7,18 +6,14 @@ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division -from __future__ import unicode_literals import io -from builtins import str, bytes import usbtmc -import quantities as pq from instruments.abstract_instruments.comm import AbstractCommunicator from instruments.util_fns import assume_units +from instruments.units import ureg as u # CLASSES ##################################################################### @@ -32,7 +27,7 @@ class USBTMCCommunicator(io.IOBase, AbstractCommunicator): def __init__(self, *args, **kwargs): if usbtmc is None: raise ImportError("usbtmc is required for TMC instruments.") - super(USBTMCCommunicator, self).__init__(self) + super().__init__(self) self._filelike = usbtmc.Instrument(*args, **kwargs) self._terminator = "\n" @@ -43,8 +38,8 @@ def __init__(self, *args, **kwargs): def address(self): if hasattr(self._filelike, "name"): return id(self._filelike) # TODO: replace with something more useful. - else: - return None + + return None @property def terminator(self): @@ -54,15 +49,17 @@ def terminator(self): :type: `str` """ - return self._filelike.term_char + return chr(self._filelike.term_char) @terminator.setter def terminator(self, newval): if isinstance(newval, bytes): newval = newval.decode("utf-8") if not isinstance(newval, str) or len(newval) > 1: - raise TypeError("Terminator for loopback communicator must be " - "specified as a single character string.") + raise TypeError( + "Terminator for loopback communicator must be " + "specified as a single character string." + ) self._terminator = newval self._filelike.term_char = ord(newval) @@ -71,14 +68,14 @@ def timeout(self): """ Gets/sets the communication timeout of the usbtmc comm channel. - :type: `~quantities.Quantity` + :type: `~pint.Quantity` :units: As specified or assumed to be of units ``seconds`` """ - return self._filelike.timeout * pq.second + return self._filelike.timeout * u.second @timeout.setter def timeout(self, newval): - newval = assume_units(newval, pq.second).rescale(pq.ms).magnitude + newval = assume_units(newval, u.second).to(u.s).magnitude self._filelike.timeout = newval # FILE-LIKE METHODS # @@ -89,7 +86,7 @@ def close(self): """ try: self._filelike.close() - except IOError: + except OSError: pass def read(self, size=-1, encoding="utf-8"): @@ -150,7 +147,6 @@ def flush_input(self): For a USBTMC connection, this function does not actually do anything and simply returns. """ - pass # METHODS # diff --git a/instruments/abstract_instruments/comm/visa_communicator.py b/instruments/abstract_instruments/comm/visa_communicator.py index 391487757..d9d825ed9 100644 --- a/instruments/abstract_instruments/comm/visa_communicator.py +++ b/instruments/abstract_instruments/comm/visa_communicator.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides a VISA communicator for connecting with instruments via the VISA library. @@ -7,27 +6,14 @@ # IMPORTS ##################################################################### -# pylint: disable=wrong-import-position - -from __future__ import absolute_import -from __future__ import division -from __future__ import unicode_literals import io -from builtins import str -import quantities as pq +import pyvisa from instruments.abstract_instruments.comm import AbstractCommunicator from instruments.util_fns import assume_units - -if not getattr(__builtins__, "WindowsError", None): - class WindowsError(OSError): - pass -try: - import visa -except (ImportError, WindowsError, OSError): - visa = None +from instruments.units import ureg as u # CLASSES ##################################################################### @@ -40,15 +26,13 @@ class VisaCommunicator(io.IOBase, AbstractCommunicator): """ def __init__(self, conn): - super(VisaCommunicator, self).__init__(self) + super().__init__(self) - if visa is None: - raise ImportError("PyVISA required for accessing VISA instruments.") - - version = int(visa.__version__.replace(".", "").ljust(3, "0")) + version = int(pyvisa.__version__.replace(".", "").ljust(3, "0")) # pylint: disable=no-member - if (version < 160 and isinstance(conn, visa.Instrument)) or \ - (version >= 160 and isinstance(conn, visa.Resource)): + if (version < 160 and isinstance(conn, pyvisa.Instrument)) or ( + version >= 160 and isinstance(conn, pyvisa.Resource) + ): self._conn = conn self._terminator = "\n" else: @@ -71,8 +55,9 @@ def address(self): @address.setter def address(self, newval): - raise NotImplementedError("Changing addresses of a VISA Instrument " - "is not supported.") + raise NotImplementedError( + "Changing addresses of a VISA Instrument " "is not supported." + ) @property def terminator(self): @@ -86,20 +71,23 @@ def terminator(self): @terminator.setter def terminator(self, newval): if not isinstance(newval, str): - raise TypeError("Terminator for VisaCommunicator must be specified " - "as a single character string.") + raise TypeError( + "Terminator for VisaCommunicator must be specified " + "as a single character string." + ) if len(newval) > 1: - raise ValueError("Terminator for VisaCommunicator must only be 1 " - "character long.") + raise ValueError( + "Terminator for VisaCommunicator must only be 1 " "character long." + ) self._terminator = newval @property def timeout(self): - return self._conn.timeout * pq.second + return self._conn.timeout * u.second @timeout.setter def timeout(self, newval): - newval = assume_units(newval, pq.second).rescale(pq.second).magnitude + newval = assume_units(newval, u.second).to(u.second).magnitude self._conn.timeout = newval # FILE-LIKE METHODS # @@ -110,7 +98,7 @@ def close(self): """ try: self._conn.close() - except IOError: + except OSError: pass def read_raw(self, size=-1): @@ -124,22 +112,20 @@ def read_raw(self, size=-1): :rtype: `bytes` """ if size >= 0: - while len(self._buf) < size: - data = self._conn.read() - if data == "": - break - self._buf += data + self._buf += self._conn.read_bytes(size) msg = self._buf[:size] # Remove the front of the buffer. del self._buf[:size] + elif size == -1: # Read the whole contents, appending the buffer we've already read. - msg = self._buf + self._conn.read() + msg = self._buf + self._conn.read_raw() # Reset the contents of the buffer. self._buf = bytearray() else: - raise ValueError("Must read a positive value of characters, or " - "-1 for all characters.") + raise ValueError( + "Must read a positive value of characters, or " "-1 for all characters." + ) return msg def write_raw(self, msg): @@ -149,13 +135,13 @@ def write_raw(self, msg): :param bytes msg: Bytes to be sent to the instrument over the VISA connection. """ - self._conn.write(msg) + self._conn.write_raw(msg) def seek(self, offset): # pylint: disable=unused-argument,no-self-use - return NotImplemented + raise NotImplementedError def tell(self): # pylint: disable=no-self-use - return NotImplemented + raise NotImplementedError def flush_input(self): """ @@ -163,7 +149,6 @@ def flush_input(self): entirety of its contents. """ # TODO: Find out how to flush with pyvisa - pass # METHODS # @@ -193,4 +178,4 @@ def _query(self, msg, size=-1): :rtype: `str` """ msg += self._terminator - return self._conn.ask(msg) + return self._conn.query(msg) diff --git a/instruments/abstract_instruments/comm/vxi11_communicator.py b/instruments/abstract_instruments/comm/vxi11_communicator.py index 889e05628..1eb795c98 100644 --- a/instruments/abstract_instruments/comm/vxi11_communicator.py +++ b/instruments/abstract_instruments/comm/vxi11_communicator.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides a communication layer that uses python-vxi11 to interface with VXI11 devices. @@ -7,15 +6,10 @@ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division -from __future__ import unicode_literals import io import logging -from builtins import str, bytes - import vxi11 from instruments.abstract_instruments.comm import AbstractCommunicator @@ -45,10 +39,11 @@ class VXI11Communicator(io.IOBase, AbstractCommunicator): """ def __init__(self, *args, **kwargs): - super(VXI11Communicator, self).__init__(self) + super().__init__(self) if vxi11 is None: - raise ImportError("Package python-vxi11 is required for XVI11 " - "connected instruments.") + raise ImportError( + "Package python-vxi11 is required for XVI11 " "connected instruments." + ) AbstractCommunicator.__init__(self) self._inst = vxi11.Instrument(*args, **kwargs) @@ -81,12 +76,16 @@ def terminator(self, newval): if isinstance(newval, bytes): newval = newval.decode("utf-8") if not isinstance(newval, str): - raise TypeError("Terminator for VXI11 communicator must be " - "specified as a byte or unicode string.") + raise TypeError( + "Terminator for VXI11 communicator must be " + "specified as a byte or unicode string." + ) if len(newval) > 1: - logger.warning("VXI11 instruments only support termination" - "characters of length 1. The first character" - "specified will be used.") + logger.warning( + "VXI11 instruments only support termination" + "characters of length 1. The first character" + "specified will be used." + ) self._inst.term_char = newval @property @@ -94,7 +93,7 @@ def timeout(self): """ Gets/sets the communication timeout of the vxi11 comm channel. - :type: `~quantities.Quantity` + :type: `~pint.Quantity` :units: As specified or assumed to be of units ``seconds`` """ return self._inst.timeout @@ -111,7 +110,7 @@ def close(self): """ try: self._inst.close() - except IOError: + except OSError: pass def read_raw(self, size=-1): diff --git a/instruments/abstract_instruments/electrometer.py b/instruments/abstract_instruments/electrometer.py index 9f794cdaf..4f9e7fb78 100644 --- a/instruments/abstract_instruments/electrometer.py +++ b/instruments/abstract_instruments/electrometer.py @@ -1,23 +1,19 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides an abstract base class for electrometer instruments """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division import abc -from future.utils import with_metaclass from instruments.abstract_instruments import Instrument # CLASSES ##################################################################### -class Electrometer(with_metaclass(abc.ABCMeta, Instrument)): +class Electrometer(Instrument, metaclass=abc.ABCMeta): """ Abstract base class for electrometer instruments. @@ -37,7 +33,6 @@ def mode(self): :type: `~enum.Enum` """ - pass @mode.setter @abc.abstractmethod @@ -51,9 +46,8 @@ def unit(self): Gets/sets the measurement mode for the electrometer. This is an abstract method. - :type: `~quantities.UnitQuantity` + :type: `~pint.Unit` """ - pass @property @abc.abstractmethod @@ -64,7 +58,6 @@ def trigger_mode(self): :type: `~enum.Enum` """ - pass @trigger_mode.setter @abc.abstractmethod @@ -80,7 +73,6 @@ def input_range(self): :type: `~enum.Enum` """ - pass @input_range.setter @abc.abstractmethod @@ -96,7 +88,6 @@ def zero_check(self): :type: `bool` """ - pass @zero_check.setter @abc.abstractmethod @@ -112,7 +103,6 @@ def zero_correct(self): :type: `bool` """ - pass @zero_correct.setter @abc.abstractmethod diff --git a/instruments/abstract_instruments/function_generator.py b/instruments/abstract_instruments/function_generator.py index ffe73f3aa..3e5d97ab8 100644 --- a/instruments/abstract_instruments/function_generator.py +++ b/instruments/abstract_instruments/function_generator.py @@ -1,29 +1,24 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides an abstract base class for function generator instruments """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division import abc from enum import Enum -from future.utils import with_metaclass -import quantities as pq - +from pint.errors import DimensionalityError from instruments.abstract_instruments import Instrument -import instruments.units as u -from instruments.util_fns import assume_units +from instruments.units import ureg as u +from instruments.util_fns import assume_units, ProxyList # CLASSES ##################################################################### -class FunctionGenerator(with_metaclass(abc.ABCMeta, Instrument)): +class FunctionGenerator(Instrument, metaclass=abc.ABCMeta): """ Abstract base class for function generator instruments. @@ -32,57 +27,255 @@ class FunctionGenerator(with_metaclass(abc.ABCMeta, Instrument)): provide a consistent interface to the user. """ + def __init__(self, filelike): + super().__init__(filelike) + self._channel_count = 1 + + # pylint:disable=protected-access + class Channel(metaclass=abc.ABCMeta): + """ + Abstract base class for physical channels on a function generator. + + All applicable concrete instruments should inherit from this ABC to + provide a consistent interface to the user. + + Function generators that only have a single channel do not need to + define their own concrete implementation of this class. Ones with + multiple channels need their own definition of this class, where + this class contains the concrete implementations of the below + abstract methods. Instruments with 1 channel have their concrete + implementations at the parent instrument level. + """ + + def __init__(self, parent, name): + self._parent = parent + self._name = name + + # ABSTRACT PROPERTIES # + + @property + def frequency(self): + """ + Gets/sets the the output frequency of the function generator. This is + an abstract property. + + :type: `~pint.Quantity` + """ + if self._parent._channel_count == 1: + return self._parent.frequency + else: + raise NotImplementedError() + + @frequency.setter + def frequency(self, newval): + if self._parent._channel_count == 1: + self._parent.frequency = newval + else: + raise NotImplementedError() + + @property + def function(self): + """ + Gets/sets the output function mode of the function generator. This is + an abstract property. + + :type: `~enum.Enum` + """ + if self._parent._channel_count == 1: + return self._parent.function + else: + raise NotImplementedError() + + @function.setter + def function(self, newval): + if self._parent._channel_count == 1: + self._parent.function = newval + else: + raise NotImplementedError() + + @property + def offset(self): + """ + Gets/sets the output offset voltage of the function generator. This is + an abstract property. + + :type: `~pint.Quantity` + """ + if self._parent._channel_count == 1: + return self._parent.offset + else: + raise NotImplementedError() + + @offset.setter + def offset(self, newval): + if self._parent._channel_count == 1: + self._parent.offset = newval + else: + raise NotImplementedError() + + @property + def phase(self): + """ + Gets/sets the output phase of the function generator. This is an + abstract property. + + :type: `~pint.Quantity` + """ + if self._parent._channel_count == 1: + return self._parent.phase + else: + raise NotImplementedError() + + @phase.setter + def phase(self, newval): + if self._parent._channel_count == 1: + self._parent.phase = newval + else: + raise NotImplementedError() + + def _get_amplitude_(self): + if self._parent._channel_count == 1: + return self._parent._get_amplitude_() + else: + raise NotImplementedError() + + def _set_amplitude_(self, magnitude, units): + if self._parent._channel_count == 1: + self._parent._set_amplitude_(magnitude=magnitude, units=units) + else: + raise NotImplementedError() + + @property + def amplitude(self): + """ + Gets/sets the output amplitude of the function generator. + + If set with units of :math:`\\text{dBm}`, then no voltage mode can + be passed. + + If set with units of :math:`\\text{V}` as a `~pint.Quantity` or a + `float` without a voltage mode, then the voltage mode is assumed to be + peak-to-peak. + + :units: As specified, or assumed to be :math:`\\text{V}` if not + specified. + :type: Either a `tuple` of a `~pint.Quantity` and a + `FunctionGenerator.VoltageMode`, or a `~pint.Quantity` + if no voltage mode applies. + """ + mag, units = self._get_amplitude_() + + if units == self._parent.VoltageMode.dBm: + return u.Quantity(mag, u.dBm) + + return u.Quantity(mag, u.V), units + + @amplitude.setter + def amplitude(self, newval): + # Try and rescale to dBm... if it succeeds, set the magnitude + # and units accordingly, otherwise handle as a voltage. + try: + newval_dbm = newval.to(u.dBm) + mag = float(newval_dbm.magnitude) + units = self._parent.VoltageMode.dBm + except (AttributeError, ValueError, DimensionalityError): + # OK, we have volts. Now, do we have a tuple? If not, assume Vpp. + if not isinstance(newval, tuple): + mag = newval + units = self._parent.VoltageMode.peak_to_peak + else: + mag, units = newval + + # Finally, convert the magnitude out to a float. + mag = float(assume_units(mag, u.V).to(u.V).magnitude) + + self._set_amplitude_(mag, units) + + def sendcmd(self, cmd): + self._parent.sendcmd(cmd) + + def query(self, cmd, size=-1): + return self._parent.query(cmd, size) + # ENUMS # class VoltageMode(Enum): """ Enum containing valid voltage modes for many function generators """ - peak_to_peak = 'VPP' - rms = 'VRMS' - dBm = 'DBM' + + peak_to_peak = "VPP" + rms = "VRMS" + dBm = "DBM" class Function(Enum): """ Enum containg valid output function modes for many function generators """ - sinusoid = 'SIN' - square = 'SQU' - triangle = 'TRI' - ramp = 'RAMP' - noise = 'NOIS' - arbitrary = 'ARB' - # ABSTRACT METHODS # + sinusoid = "SIN" + square = "SQU" + triangle = "TRI" + ramp = "RAMP" + noise = "NOIS" + arbitrary = "ARB" + + @property + def channel(self): + """ + Gets a channel object for the function generator. This should use + `~instruments.util_fns.ProxyList` to achieve this. + + The number of channels accessable depends on the value + of FunctionGenerator._channel_count + + :rtype: `FunctionGenerator.Channel` + """ + return ProxyList(self, self.Channel, range(self._channel_count)) + + # PASSTHROUGH PROPERTIES # + + @property + def amplitude(self): + """ + Gets/sets the output amplitude of the first channel + of the function generator + + :type: `~pint.Quantity` + """ + return self.channel[0].amplitude + + @amplitude.setter + def amplitude(self, newval): + self.channel[0].amplitude = newval - @abc.abstractmethod def _get_amplitude_(self): - pass + raise NotImplementedError() - @abc.abstractmethod def _set_amplitude_(self, magnitude, units): - pass - - # ABSTRACT PROPERTIES # + raise NotImplementedError() @property - @abc.abstractmethod def frequency(self): """ Gets/sets the the output frequency of the function generator. This is an abstract property. - :type: `~quantities.Quantity` + :type: `~pint.Quantity` """ - pass + if self._channel_count > 1: + return self.channel[0].frequency + else: + raise NotImplementedError() @frequency.setter - @abc.abstractmethod def frequency(self, newval): - pass + if self._channel_count > 1: + self.channel[0].frequency = newval + else: + raise NotImplementedError() @property - @abc.abstractmethod def function(self): """ Gets/sets the output function mode of the function generator. This is @@ -90,90 +283,54 @@ def function(self): :type: `~enum.Enum` """ - pass + if self._channel_count > 1: + return self.channel[0].function + else: + raise NotImplementedError() @function.setter - @abc.abstractmethod def function(self, newval): - pass + if self._channel_count > 1: + self.channel[0].function = newval + else: + raise NotImplementedError() @property - @abc.abstractmethod def offset(self): """ Gets/sets the output offset voltage of the function generator. This is an abstract property. - :type: `~quantities.Quantity` + :type: `~pint.Quantity` """ - pass + if self._channel_count > 1: + return self.channel[0].offset + else: + raise NotImplementedError() @offset.setter - @abc.abstractmethod def offset(self, newval): - pass + if self._channel_count > 1: + self.channel[0].offset = newval + else: + raise NotImplementedError() @property - @abc.abstractmethod def phase(self): """ Gets/sets the output phase of the function generator. This is an abstract property. - :type: `~quantities.Quantity` + :type: `~pint.Quantity` """ - pass + if self._channel_count > 1: + return self.channel[0].phase + else: + raise NotImplementedError() @phase.setter - @abc.abstractmethod def phase(self, newval): - pass - - # CONCRETE PROPERTIES # - - @property - def amplitude(self): - """ - Gets/sets the output amplitude of the function generator. - - If set with units of :math:`\\text{dBm}`, then no voltage mode can - be passed. - - If set with units of :math:`\\text{V}` as a `~quantities.Quantity` or a - `float` without a voltage mode, then the voltage mode is assumed to be - peak-to-peak. - - :units: As specified, or assumed to be :math:`\\text{V}` if not - specified. - :type: Either a `tuple` of a `~quantities.Quantity` and a - `FunctionGenerator.VoltageMode`, or a `~quantities.Quantity` - if no voltage mode applies. - """ - mag, units = self._get_amplitude_() - - if units == self.VoltageMode.dBm: - return pq.Quantity(mag, u.dBm) + if self._channel_count > 1: + self.channel[0].phase = newval else: - return pq.Quantity(mag, pq.V), units - - @amplitude.setter - def amplitude(self, newval): - # Try and rescale to dBm... if it succeeds, set the magnitude - # and units accordingly, otherwise handle as a voltage. - try: - newval_dbm = newval.rescale(u.dBm) - mag = float(newval_dbm.magnitude) - units = self.VoltageMode.dBm - except (AttributeError, ValueError): - # OK, we have volts. Now, do we have a tuple? If not, assume Vpp. - if not isinstance(newval, tuple): - mag = newval - # pylint: disable=redefined-variable-type - units = self.VoltageMode.peak_to_peak - else: - mag, units = newval - - # Finally, convert the magnitude out to a float. - mag = float(assume_units(mag, pq.V).rescale(pq.V).magnitude) - - self._set_amplitude_(mag, units) + raise NotImplementedError() diff --git a/instruments/abstract_instruments/instrument.py b/instruments/abstract_instruments/instrument.py index 0e791142f..819165a4f 100644 --- a/instruments/abstract_instruments/instrument.py +++ b/instruments/abstract_instruments/instrument.py @@ -1,63 +1,46 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides the base Instrument class for all instruments. """ # IMPORTS ##################################################################### -# pylint: disable=wrong-import-position - -from __future__ import absolute_import -from __future__ import division -from __future__ import unicode_literals import os import collections import socket +import struct +import urllib.parse as parse -from builtins import map from serial import SerialException from serial.tools.list_ports import comports - -from future.standard_library import install_aliases -import numpy as np - -import usb +import pyvisa import usb.core -import usb.util - -install_aliases() -import urllib.parse as parse # pylint: disable=wrong-import-order,import-error - -if not getattr(__builtins__, "WindowsError", None): - class WindowsError(OSError): - pass -try: - import visa -except (ImportError, WindowsError, OSError): - visa = None from instruments.abstract_instruments.comm import ( - SocketCommunicator, USBCommunicator, VisaCommunicator, FileCommunicator, - LoopbackCommunicator, GPIBCommunicator, AbstractCommunicator, - USBTMCCommunicator, VXI11Communicator, serial_manager + SocketCommunicator, + USBCommunicator, + VisaCommunicator, + FileCommunicator, + LoopbackCommunicator, + GPIBCommunicator, + AbstractCommunicator, + USBTMCCommunicator, + VXI11Communicator, + serial_manager, ) +from instruments.optional_dep_finder import numpy from instruments.errors import AcknowledgementError, PromptError # CONSTANTS ################################################################### _DEFAULT_FORMATS = collections.defaultdict(lambda: ">b") -_DEFAULT_FORMATS.update({ - 1: ">b", - 2: ">h", - 4: ">i" -}) +_DEFAULT_FORMATS.update({1: ">b", 2: ">h", 4: ">i"}) # CLASSES ##################################################################### -class Instrument(object): +class Instrument: """ This is the base instrument class from which all others are derived from. @@ -71,9 +54,11 @@ def __init__(self, filelike): if isinstance(filelike, AbstractCommunicator): self._file = filelike else: - raise TypeError("Instrument must be initialized with a filelike " - "object that is a subclass of " - "AbstractCommunicator.") + raise TypeError( + "Instrument must be initialized with a filelike " + "object that is a subclass of " + "AbstractCommunicator." + ) # Record if we're using the Loopback Communicator and put class in # testing mode so we can disable sleeps in class implementations self._testing = isinstance(self._file, LoopbackCommunicator) @@ -94,7 +79,9 @@ def sendcmd(self, cmd): be sent. """ self._file.sendcmd(str(cmd)) - ack_expected_list = self._ack_expected(cmd) + ack_expected_list = self._ack_expected( + cmd + ) # pylint: disable=assignment-from-none if not isinstance(ack_expected_list, (list, tuple)): ack_expected_list = [ack_expected_list] for ack_expected in ack_expected_list: @@ -126,7 +113,9 @@ def query(self, cmd, size=-1): connected instrument. :rtype: `str` """ - ack_expected_list = self._ack_expected(cmd) + ack_expected_list = self._ack_expected( + cmd + ) # pylint: disable=assignment-from-none if not isinstance(ack_expected_list, (list, tuple)): ack_expected_list = [ack_expected_list] @@ -138,20 +127,18 @@ def query(self, cmd, size=-1): ack = self.read() if ack != ack_expected: raise AcknowledgementError( - "Incorrect ACK message received: got {} " - "expected {}".format(ack, ack_expected) + f"Incorrect ACK message received: got {ack} expected {ack_expected}" ) value = self.read(size) # Now read in our return data if self.prompt is not None: prompt = self.read(len(self.prompt)) if prompt != self.prompt: raise PromptError( - "Incorrect prompt message received: got {} " - "expected {}".format(prompt, self.prompt) + f"Incorrect prompt message received: got {prompt} expected {self.prompt}" ) return value - def read(self, size=-1): + def read(self, size=-1, encoding="utf-8"): """ Read the last line. @@ -161,7 +148,19 @@ def read(self, size=-1): connected instrument. :rtype: `str` """ - return self._file.read(size) + return self._file.read(size, encoding) + + def read_raw(self, size=-1): + """ + Read the raw last line. + + :param int size: Number of bytes to be read. Default is read until + termination character is found. + :return: The result of the read as returned by the + connected instrument. + :rtype: `str` + """ + return self._file.read_raw(size) # PROPERTIES # @@ -257,7 +256,7 @@ def write(self, msg): self._file.write(msg) def binblockread(self, data_width, fmt=None): - """" + """ " Read a binary data block from attached instrument. This requires that the instrument respond in a particular manner as EOL terminators naturally can not be used in binary transfers. @@ -276,9 +275,11 @@ def binblockread(self, data_width, fmt=None): # This needs to be a # symbol for valid binary block symbol = self._file.read_raw(1) if symbol != b"#": # Check to make sure block is valid - raise IOError("Not a valid binary block start. Binary blocks " - "require the first character to be #, instead got " - "{}".format(symbol)) + raise OSError( + "Not a valid binary block start. Binary blocks " + "require the first character to be #, instead got " + "{}".format(symbol) + ) else: # Read in the num of digits for next part digits = int(self._file.read_raw(1)) @@ -302,15 +303,28 @@ def binblockread(self, data_width, fmt=None): if old_len == len(data): tries -= 1 if tries == 0: - raise IOError("Did not read in the required number of bytes" - "during binblock read. Got {}, expected " - "{}".format(len(data), num_of_bytes)) - return np.frombuffer(data, dtype=fmt) + raise OSError( + "Did not read in the required number of bytes" + "during binblock read. Got {}, expected " + "{}".format(len(data), num_of_bytes) + ) + if numpy: + return numpy.frombuffer(data, dtype=fmt) + return struct.unpack(f"{fmt[0]}{int(len(data)/data_width)}{fmt[-1]}", data) # CLASS METHODS # - URI_SCHEMES = ["serial", "tcpip", "gpib+usb", - "gpib+serial", "visa", "file", "usbtmc", "vxi11"] + URI_SCHEMES = [ + "serial", + "tcpip", + "gpib+usb", + "gpib+serial", + "visa", + "file", + "usbtmc", + "vxi11", + "test", + ] @classmethod def open_from_uri(cls, uri): @@ -330,6 +344,7 @@ def open_from_uri(cls, uri): gpib+serial:///dev/ttyACM0/15 # Currently non-functional. visa://USB::0x0699::0x0401::C0000001::0::INSTR usbtmc://USB::0x0699::0x0401::C0000001::0::INSTR + test:// For the ``serial`` URI scheme, baud rates may be explicitly specified using the query parameter ``baud=``, as in the example @@ -375,26 +390,20 @@ def open_from_uri(cls, uri): else: kwargs["baud"] = 115200 - return cls.open_serial( - dev_name, - **kwargs) + return cls.open_serial(dev_name, **kwargs) elif parsed_uri.scheme == "tcpip": # Ex: tcpip://192.168.0.10:4100 host, port = parsed_uri.netloc.split(":") port = int(port) return cls.open_tcpip(host, port, **kwargs) - elif parsed_uri.scheme == "gpib+usb" \ - or parsed_uri.scheme == "gpib+serial": + elif parsed_uri.scheme == "gpib+usb" or parsed_uri.scheme == "gpib+serial": # Ex: gpib+usb://COM3/15 # scheme="gpib+usb", netloc="COM3", path="/15" # Make a new device path by joining the netloc (if any) # with all but the last segment of the path. uri_head, uri_tail = os.path.split(parsed_uri.path) dev_path = os.path.join(parsed_uri.netloc, uri_head) - return cls.open_gpibusb( - dev_path, - int(uri_tail), - **kwargs) + return cls.open_gpibusb(dev_path, int(uri_tail), **kwargs) elif parsed_uri.scheme == "visa": # Ex: visa://USB::{VID}::{PID}::{SERIAL}::0::INSTR # where {VID}, {PID} and {SERIAL} are to be replaced with @@ -406,18 +415,18 @@ def open_from_uri(cls, uri): # Ex: usbtmc can take URIs exactly like visa://. return cls.open_usbtmc(parsed_uri.netloc, **kwargs) elif parsed_uri.scheme == "file": - return cls.open_file(os.path.join( - parsed_uri.netloc, - parsed_uri.path - ), **kwargs) + return cls.open_file( + os.path.join(parsed_uri.netloc, parsed_uri.path), **kwargs + ) elif parsed_uri.scheme == "vxi11": # Examples: # vxi11://192.168.1.104 # vxi11://TCPIP::192.168.1.105::gpib,5::INSTR return cls.open_vxi11(parsed_uri.netloc, **kwargs) + elif parsed_uri.scheme == "test": + return cls.open_test(**kwargs) else: - raise NotImplementedError("Invalid scheme or not yet " - "implemented.") + raise NotImplementedError("Invalid scheme or not yet " "implemented.") @classmethod def open_tcpip(cls, host, port): @@ -440,8 +449,16 @@ def open_tcpip(cls, host, port): # pylint: disable=too-many-arguments @classmethod - def open_serial(cls, port=None, baud=9600, vid=None, pid=None, - serial_number=None, timeout=3, write_timeout=3): + def open_serial( + cls, + port=None, + baud=9600, + vid=None, + pid=None, + serial_number=None, + timeout=3, + write_timeout=3, + ): """ Opens an instrument, connecting via a physical or emulated serial port. Note that many instruments which connect via USB are exposed to the @@ -475,14 +492,18 @@ def open_serial(cls, port=None, baud=9600, vid=None, pid=None, `~serial.Serial` for description of `port`, baud rates and timeouts. """ if port is None and vid is None: - raise ValueError("One of port, or the USB VID/PID pair, must be " - "specified when ") + raise ValueError( + "One of port, or the USB VID/PID pair, must be " "specified when " + ) if port is not None and vid is not None: - raise ValueError("Cannot specify both a specific port, and a USB" - "VID/PID pair.") + raise ValueError( + "Cannot specify both a specific port, and a USB" "VID/PID pair." + ) if (vid is not None and pid is None) or (pid is not None and vid is None): - raise ValueError("Both VID and PID must be specified when opening" - "a serial connection via a USB VID/PID pair.") + raise ValueError( + "Both VID and PID must be specified when opening" + "a serial connection via a USB VID/PID pair." + ) if port is None: match_count = 0 @@ -501,31 +522,31 @@ def open_serial(cls, port=None, baud=9600, vid=None, pid=None, # If we found more than 1 vid/pid device, but no serial number, # raise an exception due to ambiguity if match_count > 1: - raise SerialException("Found more than one matching serial " - "port from VID/PID pair") + raise SerialException( + "Found more than one matching serial " "port from VID/PID pair" + ) # if the port is still None after that, raise an error. if port is None and vid is not None: - err_msg = "Could not find a port with the attributes vid: {vid}, " \ - "pid: {pid}, serial number: {serial_number}" + err_msg = ( + "Could not find a port with the attributes vid: {vid}, " + "pid: {pid}, serial number: {serial_number}" + ) raise ValueError( err_msg.format( vid=vid, pid=pid, - serial_number="any" if serial_number is None else serial_number + serial_number="any" if serial_number is None else serial_number, ) ) ser = serial_manager.new_serial_connection( - port, - baud=baud, - timeout=timeout, - write_timeout=write_timeout + port, baud=baud, timeout=timeout, write_timeout=write_timeout ) return cls(ser) @classmethod - def open_gpibusb(cls, port, gpib_address, timeout=3, write_timeout=3): + def open_gpibusb(cls, port, gpib_address, timeout=3, write_timeout=3, model="gi"): """ Opens an instrument, connecting via a `Galvant Industries GPIB-USB adapter`_. @@ -540,6 +561,8 @@ def open_gpibusb(cls, port, gpib_address, timeout=3, write_timeout=3): instrument before timing out. :param float write_timeout: Number of seconds to wait when writing to the instrument before timing out. + :param str model: The brand of adapter to be connected to. Currently supported + is "gi" for Galvant Industries, and "pl" for Prologix LLC. :rtype: `Instrument` :return: Object representing the connected instrument. @@ -550,23 +573,28 @@ def open_gpibusb(cls, port, gpib_address, timeout=3, write_timeout=3): .. _Galvant Industries GPIB-USB adapter: galvant.ca/#!/store/gpibusb """ ser = serial_manager.new_serial_connection( - port, - baud=460800, - timeout=timeout, - write_timeout=write_timeout + port, baud=460800, timeout=timeout, write_timeout=write_timeout ) - return cls(GPIBCommunicator(ser, gpib_address)) + return cls(GPIBCommunicator(ser, gpib_address, model)) @classmethod - def open_gpibethernet(cls, host, port, gpib_address): + def open_gpibethernet(cls, host, port, gpib_address, model="pl"): """ - .. warning:: The GPIB-Ethernet adapter that this connection would - use does not actually exist, and thus this class method should - not be used. + Opens an instrument, connecting via a Prologix GPIBETHERNET adapter. + + :param str host: Name or IP address of the instrument. + :param int port: TCP port on which the insturment is listening. + :param int gpib_address: Address on the connected GPIB bus assigned to + the instrument. + :param str model: The brand of adapter to be connected to. Currently supported + is "gi" for Galvant Industries, and "pl" for Prologix LLC. + + .. warning:: This function has been setup for use with the Prologix + GPIBETHERNET adapter but has not been tested as confirmed working. """ conn = socket.socket() conn.connect((host, port)) - return cls(GPIBCommunicator(conn, gpib_address)) + return cls(GPIBCommunicator(conn, gpib_address, model)) @classmethod def open_visa(cls, resource_name): @@ -587,16 +615,13 @@ def open_visa(cls, resource_name): .. _PyVISA: http://pyvisa.sourceforge.net/ """ - if visa is None: - raise ImportError("PyVISA is required for loading VISA " - "instruments.") - version = list(map(int, visa.__version__.split("."))) + version = list(map(int, pyvisa.__version__.split("."))) while len(version) < 3: version += [0] if version[0] >= 1 and version[1] >= 6: - ins = visa.ResourceManager().open_resource(resource_name) + ins = pyvisa.ResourceManager().open_resource(resource_name) else: - ins = visa.instrument(resource_name) #pylint: disable=no-member + ins = pyvisa.instrument(resource_name) # pylint: disable=no-member return cls(VisaCommunicator(ins)) @classmethod @@ -668,43 +693,16 @@ def open_usb(cls, vid, pid): method. :param str vid: Vendor ID of the USB device to open. - :param int pid: Product ID of the USB device to open. + :param str pid: Product ID of the USB device to open. :rtype: `Instrument` :return: Object representing the connected instrument. """ - # pylint: disable=no-member - if usb is None: - raise ImportError("USB support not imported. Do you have PyUSB " - "version 1.0 or later?") - dev = usb.core.find(idVendor=vid, idProduct=pid) if dev is None: - raise IOError("No such device found.") - - # Use the default configuration offered by the device. - dev.set_configuration() - - # Copied from the tutorial at: - # http://pyusb.sourceforge.net/docs/1.0/tutorial.html - cfg = dev.get_active_configuration() - interface_number = cfg[(0, 0)].bInterfaceNumber - alternate_setting = usb.control.get_interface(dev, interface_number) - intf = usb.util.find_descriptor( - cfg, bInterfaceNumber=interface_number, - bAlternateSetting=alternate_setting - ) - - ep = usb.util.find_descriptor( - intf, - custom_match=lambda e: - usb.util.endpoint_direction(e.bEndpointAddress) == - usb.util.ENDPOINT_OUT - ) - if ep is None: - raise IOError("USB descriptor not found.") + raise OSError("No such device found.") - return cls(USBCommunicator(ep)) + return cls(USBCommunicator(dev)) @classmethod def open_file(cls, filename): diff --git a/instruments/abstract_instruments/multimeter.py b/instruments/abstract_instruments/multimeter.py index 0bd9e90b8..2b01748d3 100644 --- a/instruments/abstract_instruments/multimeter.py +++ b/instruments/abstract_instruments/multimeter.py @@ -1,24 +1,19 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides an abstract base class for multimeter instruments """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division import abc -from future.utils import with_metaclass - from instruments.abstract_instruments import Instrument # CLASSES ##################################################################### -class Multimeter(with_metaclass(abc.ABCMeta, Instrument)): +class Multimeter(Instrument, metaclass=abc.ABCMeta): """ Abstract base class for multimeter instruments. @@ -38,7 +33,6 @@ def mode(self): :type: `~enum.Enum` """ - pass @mode.setter @abc.abstractmethod @@ -54,7 +48,6 @@ def trigger_mode(self): :type: `~enum.Enum` """ - pass @trigger_mode.setter @abc.abstractmethod @@ -70,7 +63,6 @@ def relative(self): :type: `bool` """ - pass @relative.setter @abc.abstractmethod @@ -84,9 +76,8 @@ def input_range(self): Gets/sets the current input range setting of the multimeter. This is an abstract method. - :type: `~quantities.quantity.Quantity` or `~enum.Enum` + :type: `~pint.Quantity` or `~enum.Enum` """ - pass @input_range.setter @abc.abstractmethod @@ -100,4 +91,3 @@ def measure(self, mode): """ Perform a measurement as specified by mode parameter. """ - pass diff --git a/instruments/abstract_instruments/optical_spectrum_analyzer.py b/instruments/abstract_instruments/optical_spectrum_analyzer.py new file mode 100644 index 000000000..085d80960 --- /dev/null +++ b/instruments/abstract_instruments/optical_spectrum_analyzer.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python +""" +Provides an abstract base class for optical spectrum analyzer instruments +""" + +# IMPORTS ##################################################################### + + +import abc + +from instruments.abstract_instruments import Instrument + +# CLASSES ##################################################################### + + +class OpticalSpectrumAnalyzer(Instrument, metaclass=abc.ABCMeta): + + """ + Abstract base class for optical spectrum analyzer instruments. + + All applicable concrete instruments should inherit from this ABC to + provide a consistent interface to the user. + """ + + class Channel(metaclass=abc.ABCMeta): + """ + Abstract base class for physical channels on an optical spectrum analyzer. + + All applicable concrete instruments should inherit from this ABC to + provide a consistent interface to the user. + """ + + # METHODS # + + @abc.abstractmethod + def wavelength(self, bin_format=True): + """ + Gets the x-axis of the specified data source channel. This is an + abstract property. + + :param bool bin_format: If the waveform should be transfered in binary + (``True``) or ASCII (``False``) formats. + :return: The wavelength component of the waveform. + :rtype: `numpy.ndarray` + """ + raise NotImplementedError + + @abc.abstractmethod + def data(self, bin_format=True): + """ + Gets the y-axis of the specified data source channel. This is an + abstract property. + + :param bool bin_format: If the waveform should be transfered in binary + (``True``) or ASCII (``False``) formats. + :return: The y-component of the waveform. + :rtype: `numpy.ndarray` + """ + raise NotImplementedError + + # PROPERTIES # + + @property + @abc.abstractmethod + def channel(self): + """ + Gets an iterator or list for easy Pythonic access to the various + channel objects on the OSA instrument. Typically generated + by the `~instruments.util_fns.ProxyList` helper. + """ + raise NotImplementedError + + @property + @abc.abstractmethod + def start_wl(self): + """ + Gets/sets the the start wavelength of the OSA. This is + an abstract property. + + :type: `~pint.Quantity` + """ + raise NotImplementedError + + @start_wl.setter + @abc.abstractmethod + def start_wl(self, newval): + raise NotImplementedError + + @property + @abc.abstractmethod + def stop_wl(self): + """ + Gets/sets the the stop wavelength of the OSA. This is + an abstract property. + + :type: `~pint.Quantity` + """ + raise NotImplementedError + + @stop_wl.setter + @abc.abstractmethod + def stop_wl(self, newval): + raise NotImplementedError + + @property + @abc.abstractmethod + def bandwidth(self): + """ + Gets/sets the the bandwidth of the OSA. This is + an abstract property. + + :type: `~pint.Quantity` + """ + raise NotImplementedError + + @bandwidth.setter + @abc.abstractmethod + def bandwidth(self, newval): + raise NotImplementedError + + # METHODS # + + @abc.abstractmethod + def start_sweep(self): + """ + Forces a start sweep on the attached OSA. + """ + raise NotImplementedError diff --git a/instruments/abstract_instruments/oscilloscope.py b/instruments/abstract_instruments/oscilloscope.py index 960f28b28..34dcbe8cf 100644 --- a/instruments/abstract_instruments/oscilloscope.py +++ b/instruments/abstract_instruments/oscilloscope.py @@ -1,124 +1,120 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides an abstract base class for oscilloscope instruments """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division import abc -from future.utils import with_metaclass - from instruments.abstract_instruments import Instrument # CLASSES ##################################################################### -class OscilloscopeDataSource(with_metaclass(abc.ABCMeta, object)): +class Oscilloscope(Instrument, metaclass=abc.ABCMeta): """ - Abstract base class for data sources (physical channels, math, ref) on - an oscilloscope. + Abstract base class for oscilloscope instruments. All applicable concrete instruments should inherit from this ABC to provide a consistent interface to the user. """ - def __init__(self, parent, name): - self._parent = parent - self._name = name - self._old_dsrc = None - - def __enter__(self): - self._old_dsrc = self._parent.data_source - if self._old_dsrc != self: - # Set the new data source, and let __exit__ cleanup. - self._parent.data_source = self - else: - # There's nothing to do or undo in this case. - self._old_dsrc = None + class Channel(metaclass=abc.ABCMeta): + """ + Abstract base class for physical channels on an oscilloscope. - def __exit__(self, type, value, traceback): - if self._old_dsrc is not None: - self._parent.data_source = self._old_dsrc + All applicable concrete instruments should inherit from this ABC to + provide a consistent interface to the user. + """ - def __eq__(self, other): - if not isinstance(other, type(self)): - return NotImplemented - else: - return other.name == self.name + # PROPERTIES # - # PROPERTIES # + @property + @abc.abstractmethod + def coupling(self): + """ + Gets/sets the coupling setting for the oscilloscope. This is an + abstract method. - @abc.abstractproperty - def name(self): - """ - Gets the name of the channel. This is an abstract property. + :type: `~enum.Enum` + """ + raise NotImplementedError - :type: `str` - """ - raise NotImplementedError + @coupling.setter + @abc.abstractmethod + def coupling(self, newval): + raise NotImplementedError - # METHODS # + class DataSource(metaclass=abc.ABCMeta): - @abc.abstractmethod - def read_waveform(self, bin_format=True): """ - Gets the waveform of the specified data source channel. This is an - abstract property. + Abstract base class for data sources (physical channels, math, ref) on + an oscilloscope. - :param bool bin_format: If the waveform should be transfered in binary - (``True``) or ASCII (``False``) formats. - :return: The waveform with both x and y components. - :rtype: `numpy.ndarray` + All applicable concrete instruments should inherit from this ABC to + provide a consistent interface to the user. """ - raise NotImplementedError + def __init__(self, parent, name): + self._parent = parent + self._name = name + self._old_dsrc = None -class OscilloscopeChannel(with_metaclass(abc.ABCMeta, object)): + def __enter__(self): + self._old_dsrc = self._parent.data_source + if self._old_dsrc != self: + # Set the new data source, and let __exit__ cleanup. + self._parent.data_source = self + else: + # There's nothing to do or undo in this case. + self._old_dsrc = None - """ - Abstract base class for physical channels on an oscilloscope. + def __exit__(self, type, value, traceback): + if self._old_dsrc is not None: + self._parent.data_source = self._old_dsrc - All applicable concrete instruments should inherit from this ABC to - provide a consistent interface to the user. - """ + def __eq__(self, other): + if not isinstance(other, type(self)): + return NotImplemented - # PROPERTIES # + return other.name == self.name - @property - @abc.abstractmethod - def coupling(self): - """ - Gets/sets the coupling setting for the oscilloscope. This is an - abstract method. + __hash__ = None - :type: `~enum.Enum` - """ - raise NotImplementedError + # PROPERTIES # - @coupling.setter - @abc.abstractmethod - def coupling(self, newval): - raise NotImplementedError + @property + @abc.abstractmethod + def name(self): + """ + Gets the name of the channel. This is an abstract property. + :type: `str` + """ + raise NotImplementedError -class Oscilloscope(with_metaclass(abc.ABCMeta, Instrument)): + # METHODS # - """ - Abstract base class for oscilloscope instruments. + @abc.abstractmethod + def read_waveform(self, bin_format=True): + """ + Gets the waveform of the specified data source channel. This is an + abstract property. - All applicable concrete instruments should inherit from this ABC to - provide a consistent interface to the user. - """ + :param bool bin_format: If the waveform should be transfered in binary + (``True``) or ASCII (``False``) formats. + :return: The waveform with both x and y components. + :rtype: `numpy.ndarray` + """ + raise NotImplementedError # PROPERTIES # - @abc.abstractproperty + @property + @abc.abstractmethod def channel(self): """ Gets an iterator or list for easy Pythonic access to the various @@ -127,7 +123,8 @@ def channel(self): """ raise NotImplementedError - @abc.abstractproperty + @property + @abc.abstractmethod def ref(self): """ Gets an iterator or list for easy Pythonic access to the various @@ -136,7 +133,8 @@ def ref(self): """ raise NotImplementedError - @abc.abstractproperty + @property + @abc.abstractmethod def math(self): """ Gets an iterator or list for easy Pythonic access to the various diff --git a/instruments/abstract_instruments/power_supply.py b/instruments/abstract_instruments/power_supply.py index 4b54e951b..32fa6d5aa 100644 --- a/instruments/abstract_instruments/power_supply.py +++ b/instruments/abstract_instruments/power_supply.py @@ -1,107 +1,94 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides an abstract base class for power supply instruments """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division - import abc -from future.utils import with_metaclass - from instruments.abstract_instruments import Instrument # CLASSES ##################################################################### -class PowerSupplyChannel(with_metaclass(abc.ABCMeta, object)): - +class PowerSupply(Instrument, metaclass=abc.ABCMeta): """ - Abstract base class for power supply output channels. + Abstract base class for power supply instruments. All applicable concrete instruments should inherit from this ABC to provide a consistent interface to the user. """ - # PROPERTIES # - - @property - @abc.abstractmethod - def mode(self): - """ - Gets/sets the output mode for the power supply channel. This is an - abstract method. - - :type: `~enum.Enum` - """ - pass - - @mode.setter - @abc.abstractmethod - def mode(self, newval): - pass - - @property - @abc.abstractmethod - def voltage(self): - """ - Gets/sets the output voltage for the power supply channel. This is an - abstract method. - - :type: `~quantities.quantity.Quantity` - """ - pass - - @voltage.setter - @abc.abstractmethod - def voltage(self, newval): - pass - - @property - @abc.abstractmethod - def current(self): + class Channel(metaclass=abc.ABCMeta): """ - Gets/sets the output current for the power supply channel. This is an - abstract method. + Abstract base class for power supply output channels. - :type: `~quantities.quantity.Quantity` + All applicable concrete instruments should inherit from this ABC to + provide a consistent interface to the user. """ - pass - @current.setter - @abc.abstractmethod - def current(self, newval): - pass - - @property - @abc.abstractmethod - def output(self): - """ - Gets/sets the output status for the power supply channel. This is an - abstract method. - - :type: `bool` - """ - pass - - @output.setter - @abc.abstractmethod - def output(self, newval): - pass - - -class PowerSupply(with_metaclass(abc.ABCMeta, Instrument)): - - """ - Abstract base class for power supply instruments. - - All applicable concrete instruments should inherit from this ABC to - provide a consistent interface to the user. - """ + # PROPERTIES # + + @property + @abc.abstractmethod + def mode(self): + """ + Gets/sets the output mode for the power supply channel. This is an + abstract method. + + :type: `~enum.Enum` + """ + + @mode.setter + @abc.abstractmethod + def mode(self, newval): + pass + + @property + @abc.abstractmethod + def voltage(self): + """ + Gets/sets the output voltage for the power supply channel. This is an + abstract method. + + :type: `~pint.Quantity` + """ + + @voltage.setter + @abc.abstractmethod + def voltage(self, newval): + pass + + @property + @abc.abstractmethod + def current(self): + """ + Gets/sets the output current for the power supply channel. This is an + abstract method. + + :type: `~pint.Quantity` + """ + + @current.setter + @abc.abstractmethod + def current(self, newval): + pass + + @property + @abc.abstractmethod + def output(self): + """ + Gets/sets the output status for the power supply channel. This is an + abstract method. + + :type: `bool` + """ + + @output.setter + @abc.abstractmethod + def output(self, newval): + pass # PROPERTIES # @@ -114,7 +101,7 @@ def channel(self): This is an abstract method. - :rtype: `PowerSupplyChannel` + :rtype: `PowerSupply.Channel` """ raise NotImplementedError @@ -125,9 +112,8 @@ def voltage(self): Gets/sets the output voltage for all channel on the power supply. This is an abstract method. - :type: `~quantities.quantity.Quantity` + :type: `~pint.Quantity` """ - pass @voltage.setter @abc.abstractmethod @@ -141,9 +127,8 @@ def current(self): Gets/sets the output current for all channel on the power supply. This is an abstract method. - :type: `~quantities.quantity.Quantity` + :type: `~pint.Quantity` """ - pass @current.setter @abc.abstractmethod diff --git a/instruments/abstract_instruments/signal_generator/__init__.py b/instruments/abstract_instruments/signal_generator/__init__.py index 6eaa9b7a5..b8181de07 100644 --- a/instruments/abstract_instruments/signal_generator/__init__.py +++ b/instruments/abstract_instruments/signal_generator/__init__.py @@ -1,10 +1,8 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing signal generator abstract base classes """ -from __future__ import absolute_import from .signal_generator import SignalGenerator from .single_channel_sg import SingleChannelSG diff --git a/instruments/abstract_instruments/signal_generator/channel.py b/instruments/abstract_instruments/signal_generator/channel.py index 79334349c..cc0ed626e 100644 --- a/instruments/abstract_instruments/signal_generator/channel.py +++ b/instruments/abstract_instruments/signal_generator/channel.py @@ -1,22 +1,16 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides an abstract base class for signal generator output channels """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division - import abc -from future.utils import with_metaclass - # CLASSES ##################################################################### -class SGChannel(with_metaclass(abc.ABCMeta, object)): +class SGChannel(metaclass=abc.ABCMeta): """ Python abstract base class representing a single channel for a signal @@ -34,9 +28,8 @@ def frequency(self): """ Gets/sets the output frequency of the signal generator channel - :type: `~quantities.quantity.Quantity` + :type: `~pint.Quantity` """ - pass @frequency.setter @abc.abstractmethod @@ -49,9 +42,8 @@ def power(self): """ Gets/sets the output power of the signal generator channel - :type: `~quantities.quantity.Quantity` + :type: `~pint.Quantity` """ - pass @power.setter @abc.abstractmethod @@ -64,9 +56,8 @@ def phase(self): """ Gets/sets the output phase of the signal generator channel - :type: `~quantities.quantity.Quantity` + :type: `~pint.Quantity` """ - pass @phase.setter @abc.abstractmethod @@ -81,7 +72,6 @@ def output(self): :type: `bool` """ - pass @output.setter @abc.abstractmethod diff --git a/instruments/abstract_instruments/signal_generator/signal_generator.py b/instruments/abstract_instruments/signal_generator/signal_generator.py index a8b9748d8..8665d187d 100644 --- a/instruments/abstract_instruments/signal_generator/signal_generator.py +++ b/instruments/abstract_instruments/signal_generator/signal_generator.py @@ -1,24 +1,19 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides an abstract base class for signal generator instruments """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division import abc -from future.utils import with_metaclass - from instruments.abstract_instruments import Instrument # CLASSES ##################################################################### -class SignalGenerator(with_metaclass(abc.ABCMeta, Instrument)): +class SignalGenerator(Instrument, metaclass=abc.ABCMeta): """ Python abstract base class for signal generators (eg microwave sources). @@ -31,7 +26,8 @@ class SignalGenerator(with_metaclass(abc.ABCMeta, Instrument)): # PROPERTIES # - @abc.abstractproperty + @property + @abc.abstractmethod def channel(self): """ Gets a specific channel object for the SignalGenerator. diff --git a/instruments/abstract_instruments/signal_generator/single_channel_sg.py b/instruments/abstract_instruments/signal_generator/single_channel_sg.py index f478c7013..e6dad4880 100644 --- a/instruments/abstract_instruments/signal_generator/single_channel_sg.py +++ b/instruments/abstract_instruments/signal_generator/single_channel_sg.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides an abstract base class for signal generators with only a single output channel. @@ -7,8 +6,6 @@ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division from instruments.abstract_instruments.signal_generator import SignalGenerator from instruments.abstract_instruments.signal_generator.channel import SGChannel @@ -37,4 +34,4 @@ class SingleChannelSG(SignalGenerator, SGChannel): @property def channel(self): - return self, + return (self,) diff --git a/instruments/agilent/__init__.py b/instruments/agilent/__init__.py index 15796603a..741856d1b 100644 --- a/instruments/agilent/__init__.py +++ b/instruments/agilent/__init__.py @@ -1,10 +1,8 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing Agilent instruments """ -from __future__ import absolute_import from instruments.agilent.agilent33220a import Agilent33220a from instruments.agilent.agilent34410a import Agilent34410a diff --git a/instruments/agilent/agilent33220a.py b/instruments/agilent/agilent33220a.py index 7a2c56b7c..8bb4451d6 100644 --- a/instruments/agilent/agilent33220a.py +++ b/instruments/agilent/agilent33220a.py @@ -1,27 +1,26 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides support for the Agilent 33220a function generator. """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division -from builtins import range - from enum import Enum -import quantities as pq from instruments.generic_scpi import SCPIFunctionGenerator +from instruments.units import ureg as u from instruments.util_fns import ( - enum_property, int_property, bool_property, assume_units + enum_property, + int_property, + bool_property, + assume_units, ) # CLASSES ##################################################################### + class Agilent33220a(SCPIFunctionGenerator): """ @@ -33,19 +32,16 @@ class Agilent33220a(SCPIFunctionGenerator): Example usage: >>> import instruments as ik - >>> import quantities as pq + >>> import instruments.units as u >>> inst = ik.agilent.Agilent33220a.open_gpibusb('/dev/ttyUSB0', 1) >>> inst.function = inst.Function.sinusoid - >>> inst.frequency = 1 * pq.kHz + >>> inst.frequency = 1 * u.kHz >>> inst.output = True .. _Agilent/Keysight 33220a: http://www.keysight.com/en/pd-127539-pn-33220A """ - def __init__(self, filelike): - super(Agilent33220a, self).__init__(filelike) - # ENUMS # class Function(Enum): @@ -53,6 +49,7 @@ class Function(Enum): """ Enum containing valid functions for the Agilent/Keysight 33220a """ + sinusoid = "SIN" square = "SQU" ramp = "RAMP" @@ -66,6 +63,7 @@ class LoadResistance(Enum): """ Enum containing valid load resistance for the Agilent/Keysight 33220a """ + minimum = "MIN" maximum = "MAX" high_impedance = "INF" @@ -76,32 +74,25 @@ class OutputPolarity(Enum): Enum containg valid output polarity modes for the Agilent/Keysight 33220a """ + normal = "NORM" inverted = "INV" # PROPERTIES # - @property - def frequency(self): - return super(Agilent33220a, self).frequency - - @frequency.setter - def frequency(self, newval): - super(Agilent33220a, self).frequency = newval - function = enum_property( - name="FUNC", + command="FUNC", enum=Function, doc=""" Gets/sets the output function of the function generator :type: `Agilent33220a.Function` """, - set_fmt="{}:{}" + set_fmt="{}:{}", ) duty_cycle = int_property( - name="FUNC:SQU:DCYC", + command="FUNC:SQU:DCYC", doc=""" Gets/sets the duty cycle of a square wave. @@ -110,11 +101,11 @@ def frequency(self, newval): :type: `int` """, - valid_set=range(101) + valid_set=range(101), ) ramp_symmetry = int_property( - name="FUNC:RAMP:SYMM", + command="FUNC:RAMP:SYMM", doc=""" Gets/sets the ramp symmetry for ramp waves. @@ -123,11 +114,11 @@ def frequency(self, newval): :type: `int` """, - valid_set=range(101) + valid_set=range(101), ) output = bool_property( - name="OUTP", + command="OUTP", inst_true="ON", inst_false="OFF", doc=""" @@ -137,28 +128,28 @@ def frequency(self, newval): the output being off. :type: `bool` - """ + """, ) output_sync = bool_property( - name="OUTP:SYNC", + command="OUTP:SYNC", inst_true="ON", inst_false="OFF", doc=""" Gets/sets the enabled status of the front panel sync connector. :type: `bool` - """ + """, ) output_polarity = enum_property( - name="OUTP:POL", + command="OUTP:POL", enum=OutputPolarity, doc=""" Gets/sets the polarity of the waveform relative to the offset voltage. :type: `~Agilent33220a.OutputPolarity` - """ + """, ) @property @@ -171,13 +162,13 @@ def load_resistance(self): function allows the instrument to compensate of the voltage divider and accurately report the voltage across the attached load. - :units: As specified (if a `~quantities.quantity.Quantity`) or assumed + :units: As specified (if a `~pint.Quantity`) or assumed to be of units :math:`\\Omega` (ohm). - :type: `~quantities.quantity.Quantity` or `Agilent33220a.LoadResistance` + :type: `~pint.Quantity` or `Agilent33220a.LoadResistance` """ value = self.query("OUTP:LOAD?") try: - return int(value) * pq.ohm + return int(value) * u.ohm except ValueError: return self.LoadResistance(value.strip()) @@ -185,14 +176,11 @@ def load_resistance(self): def load_resistance(self, newval): if isinstance(newval, self.LoadResistance): newval = newval.value - elif isinstance(newval, int): - if (newval < 0) or (newval > 10000): - raise ValueError( - "Load resistance must be between 0 and 10,000") - newval = assume_units(newval, pq.ohm).rescale(pq.ohm).magnitude else: - raise TypeError("Not a valid load resistance type.") - self.sendcmd("OUTP:LOAD {}".format(newval)) + newval = assume_units(newval, u.ohm).to(u.ohm).magnitude + if (newval < 0) or (newval > 10000): + raise ValueError("Load resistance must be between 0 and 10,000") + self.sendcmd(f"OUTP:LOAD {newval}") @property def phase(self): diff --git a/instruments/agilent/agilent34410a.py b/instruments/agilent/agilent34410a.py index 329ad8abf..2f0925c60 100644 --- a/instruments/agilent/agilent34410a.py +++ b/instruments/agilent/agilent34410a.py @@ -1,18 +1,13 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides support for the Agilent 34410a digital multimeter. """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division -from builtins import map - -import quantities as pq - from instruments.generic_scpi import SCPIMultimeter +from instruments.optional_dep_finder import numpy +from instruments.units import ureg as u # CLASSES ##################################################################### @@ -28,16 +23,13 @@ class Agilent34410a(SCPIMultimeter): # pylint: disable=abstract-method Example usage: >>> import instruments as ik - >>> import quantities as pq + >>> import instruments.units as u >>> dmm = ik.agilent.Agilent34410a.open_gpibusb('/dev/ttyUSB0', 1) >>> print(dmm.measure(dmm.Mode.resistance)) .. _Keysight website: http://www.keysight.com/ """ - def __init__(self, filelike): - super(Agilent34410a, self).__init__(filelike) - # PROPERTIES # @property @@ -48,7 +40,7 @@ def data_point_count(self): :rtype: `int` """ - return int(self.query('DATA:POIN?')) + return int(self.query("DATA:POIN?")) # STATE MANAGEMENT METHODS # @@ -61,13 +53,13 @@ def init(self): Note that this command will also clear the previous set of readings from memory. """ - self.sendcmd('INIT') + self.sendcmd("INIT") def abort(self): """ Abort all measurements currently in progress. """ - self.sendcmd('ABOR') + self.sendcmd("ABOR") # MEMORY MANAGEMENT METHODS # @@ -75,7 +67,7 @@ def clear_memory(self): """ Clears the non-volatile memory of the Agilent 34410a. """ - self.sendcmd('DATA:DEL NVMEM') + self.sendcmd("DATA:DEL NVMEM") def r(self, count): """ @@ -86,20 +78,23 @@ def r(self, count): :param int count: Number of samples to take. - :rtype: `~quantities.quantity.Quantity` with `numpy.array` + :rtype: `tuple`[`~pint.Quantity`, ...] + or if numpy is installed, `~pint.Quantity` with `numpy.array` data """ mode = self.mode units = UNITS[mode] if not isinstance(count, int): raise TypeError('Parameter "count" must be an integer') if count == 0: - msg = 'R?' + msg = "R?" else: - msg = 'R? ' + str(count) - self.sendcmd('FORM:DATA REAL,64') + msg = "R? " + str(count) + self.sendcmd("FORM:DATA REAL,64") self.sendcmd(msg) data = self.binblockread(8, fmt=">d") - return data * units + if numpy: + return data * units + return tuple(val * units for val in data) # DATA READING METHODS # @@ -115,10 +110,14 @@ def fetch(self): recommended to transfer a large number of data points using this method. - :rtype: `list` of `~quantities.quantity.Quantity` elements + :rtype: `tuple`[`~pint.Quantity`, ...] + or if numpy is installed, `~pint.Quantity` with `numpy.array` data """ units = UNITS[self.mode] - return list(map(float, self.query('FETC?').split(','))) * units + data = list(map(float, self.query("FETC?").split(","))) + if numpy: + return data * units + return tuple(val * units for val in data) def read_data(self, sample_count): """ @@ -131,7 +130,8 @@ def read_data(self, sample_count): output buffer. If set to -1, all points in memory will be transfered. - :rtype: `list` of `~quantities.quantity.Quantity` elements + :rtype: `tuple`[`~pint.Quantity`, ...] + or if numpy is installed, `~pint.Quantity` with `numpy.array` data """ if not isinstance(sample_count, int): raise TypeError('Parameter "sample_count" must be an integer.') @@ -139,19 +139,25 @@ def read_data(self, sample_count): if sample_count == -1: sample_count = self.data_point_count units = UNITS[self.mode] - self.sendcmd('FORM:DATA ASC') - data = self.query('DATA:REM? {}'.format(sample_count)).split(',') - return list(map(float, data)) * units + self.sendcmd("FORM:DATA ASC") + data = self.query(f"DATA:REM? {sample_count}").split(",") + data = list(map(float, data)) + if numpy: + return data * units + return tuple(val * units for val in data) def read_data_nvmem(self): """ Returns all readings in non-volatile memory (NVMEM). - :rtype: `list` of `~quantities.quantity.Quantity` elements + :rtype: `tuple`[`~pint.Quantity`, ...] + or if numpy is installed, `~pint.Quantity` with `numpy.array` data """ units = UNITS[self.mode] - data = list(map(float, self.query('DATA:DATA? NVMEM').split(','))) - return data * units + data = list(map(float, self.query("DATA:DATA? NVMEM").split(","))) + if numpy: + return data * units + return tuple(val * units for val in data) def read_last_data(self): """ @@ -161,22 +167,22 @@ def read_last_data(self): returned. :units: As specified by the data returned by the instrument. - :rtype: `~quantities.quantity.Quantity` + :rtype: `~pint.Quantity` """ - data = self.query('DATA:LAST?') + data = self.query("DATA:LAST?") unit_map = { "VDC": "V", "VAC": "V", } - if data == '9.91000000E+37': - return int(data) + if data == "9.91000000E+37": + return float(data) else: data = data.split(" ") data[0] = float(data[0]) if data[1] in unit_map: data[1] = unit_map[data[1]] - return pq.Quantity(*data) + return u.Quantity(*data) def read_meter(self): """ @@ -187,25 +193,26 @@ def read_meter(self): This is similar to calling `~Agilent34410a.init` and then immediately following `~Agilent34410a.fetch`. - :rtype: `~quantities.Quantity` + :rtype: `~pint.Quantity` """ mode = self.mode units = UNITS[mode] - return float(self.query('READ?')) * units + return float(self.query("READ?")) * units + # UNITS ####################################################################### UNITS = { - Agilent34410a.Mode.capacitance: pq.farad, - Agilent34410a.Mode.voltage_dc: pq.volt, - Agilent34410a.Mode.voltage_ac: pq.volt, - Agilent34410a.Mode.diode: pq.volt, - Agilent34410a.Mode.current_ac: pq.amp, - Agilent34410a.Mode.current_dc: pq.amp, - Agilent34410a.Mode.resistance: pq.ohm, - Agilent34410a.Mode.fourpt_resistance: pq.ohm, - Agilent34410a.Mode.frequency: pq.hertz, - Agilent34410a.Mode.period: pq.second, - Agilent34410a.Mode.temperature: pq.kelvin, - Agilent34410a.Mode.continuity: 1, + Agilent34410a.Mode.capacitance: u.farad, + Agilent34410a.Mode.voltage_dc: u.volt, + Agilent34410a.Mode.voltage_ac: u.volt, + Agilent34410a.Mode.diode: u.volt, + Agilent34410a.Mode.current_ac: u.amp, + Agilent34410a.Mode.current_dc: u.amp, + Agilent34410a.Mode.resistance: u.ohm, + Agilent34410a.Mode.fourpt_resistance: u.ohm, + Agilent34410a.Mode.frequency: u.hertz, + Agilent34410a.Mode.period: u.second, + Agilent34410a.Mode.temperature: u.kelvin, + Agilent34410a.Mode.continuity: 1, } diff --git a/instruments/config.py b/instruments/config.py index 74d09499b..094957e12 100644 --- a/instruments/config.py +++ b/instruments/config.py @@ -1,20 +1,27 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing support for loading instruments from configuration files. """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division import warnings try: - import yaml + import ruamel.yaml as yaml except ImportError: - yaml = None + # Some versions of ruamel.yaml are named ruamel_yaml, so try that + # too. + # + # In either case, we've observed issues with pylint where it will raise + # a false positive from its import-error checker, so we locally disable + # it here. Once the cause for the false positive has been identified, + # the import-error check should be re-enabled. + import ruamel_yaml as yaml # pylint: disable=import-error + +from instruments.units import ureg as u +from instruments.util_fns import setattr_expression, split_unit_str # FUNCTIONS ################################################################### @@ -37,14 +44,30 @@ def walk_dict(d, path): # Treat as a base case that the path is empty. if not path: return d - if isinstance(path, str): + if not isinstance(path, list): path = path.split("/") + if not path[0]: # If the first part of the path is empty, do nothing. return walk_dict(d, path[1:]) - else: - # Otherwise, resolve that segment and recurse. - return walk_dict(d[path[0]], path[1:]) + + # Otherwise, resolve that segment and recurse. + return walk_dict(d[path[0]], path[1:]) + + +def quantity_constructor(loader, node): + """ + Constructs a `u.Quantity` instance from a PyYAML + node tagged as ``!Q``. + """ + # Follows the example of http://stackoverflow.com/a/43081967/267841. + value = loader.construct_scalar(node) + return u.Quantity(*split_unit_str(value)) + + +# We avoid having to register !Q every time by doing as soon as the +# relevant constructor is defined. +yaml.add_constructor("!Q", quantity_constructor) def load_instruments(conf_file_name, conf_path="/"): @@ -63,6 +86,28 @@ def load_instruments(conf_file_name, conf_path="/"): the form ``{'ddg': instruments.srs.SRSDG645.open_from_uri('gpib+usb://COM7/15')}``. + Each instrument configuration section can also specify one or more attributes + to set. These attributes are specified using a ``attrs`` section as well as the + required ``class`` and ``uri`` sections. For instance, the following + dictionary creates a ThorLabs APT motor controller instrument with a single motor + model configured:: + + rot_stage: + class: !!python/name:instruments.thorabsapt.APTMotorController + uri: serial:///dev/ttyUSB0?baud=115200 + attrs: + channel[0].motor_model: PRM1-Z8 + + Unitful attributes can be specified by using the ``!Q`` tag to quickly create + instances of `u.Quantity`. In the example above, for instance, we can set a motion + timeout as a unitful quantity:: + + attrs: + motion_timeout: !Q 1 minute + + When using the ``!Q`` tag, any text before a space is taken to be the magnitude + of the quantity, and text following is taken to be the unit specification. + By specifying a path within the configuration file, one can load only a part of the given file. For instance, consider the configuration:: @@ -78,7 +123,7 @@ def load_instruments(conf_file_name, conf_path="/"): all other keys in the YAML file. :param str conf_file_name: Name of the configuration file to load - instruments from. + instruments from. Alternatively, a file-like object may be provided. :param str conf_path: ``"/"`` separated path to the section in the configuration file to load. @@ -95,23 +140,36 @@ def load_instruments(conf_file_name, conf_path="/"): """ if yaml is None: - raise ImportError("Could not import PyYAML, which is required " - "for this function.") + raise ImportError( + "Could not import ruamel.yaml, which is required " "for this function." + ) - with open(conf_file_name, 'r') as f: - conf_dict = yaml.load(f) + if isinstance(conf_file_name, str): + with open(conf_file_name) as f: + conf_dict = yaml.load(f, Loader=yaml.Loader) + else: + conf_dict = yaml.load(conf_file_name, Loader=yaml.Loader) conf_dict = walk_dict(conf_dict, conf_path) inst_dict = {} - for name, value in conf_dict.iteritems(): + for name, value in conf_dict.items(): try: inst_dict[name] = value["class"].open_from_uri(value["uri"]) - except IOError as ex: + + if "attrs" in value: + # We have some attrs we can set on the newly created instrument. + for attr_name, attr_value in value["attrs"].items(): + setattr_expression(inst_dict[name], attr_name, attr_value) + + except OSError as ex: # FIXME: need to subclass Warning so that repeated warnings # aren't ignored. - warnings.warn("Exception occured loading device URI " - "{}:\n\t{}.".format(value["uri"], ex), RuntimeWarning) + warnings.warn( + "Exception occured loading device with URI " + "{}:\n\t{}.".format(value["uri"], ex), + RuntimeWarning, + ) inst_dict[name] = None return inst_dict diff --git a/instruments/errors.py b/instruments/errors.py index 3e1eba2d1..5ae4a0e25 100644 --- a/instruments/errors.py +++ b/instruments/errors.py @@ -1,12 +1,10 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing custom exception errors used by various instruments. """ # IMPORTS ##################################################################### -from __future__ import absolute_import # CLASSES ##################################################################### @@ -16,7 +14,6 @@ class AcknowledgementError(IOError): This error is raised when an instrument fails to send the expected acknowledgement string. """ - pass class PromptError(IOError): @@ -26,4 +23,3 @@ class PromptError(IOError): these characters, but some do in a misguided attempt to be more "user friendly". """ - pass diff --git a/instruments/fluke/__init__.py b/instruments/fluke/__init__.py new file mode 100644 index 000000000..20649d602 --- /dev/null +++ b/instruments/fluke/__init__.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python +""" +Module containing Fluke instruments +""" + + +from .fluke3000 import Fluke3000 diff --git a/instruments/fluke/fluke3000.py b/instruments/fluke/fluke3000.py new file mode 100644 index 000000000..3b275ba7c --- /dev/null +++ b/instruments/fluke/fluke3000.py @@ -0,0 +1,500 @@ +#!/usr/bin/env python +# +# fluke3000.py: Driver for the Fluke 3000 FC Industrial System +# +# © 2019 Francois Drielsma (francois.drielsma@gmail.com). +# +# This file is a part of the InstrumentKit project. +# Licensed under the AGPL version 3. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +""" +Driver for the Fluke 3000 FC Industrial System + +Originally contributed and copyright held by Francois Drielsma +(francois.drielsma@gmail.com) + +An unrestricted license has been provided to the maintainers of the Instrument +Kit project. +""" + +# IMPORTS ##################################################################### + +import time +from enum import Enum + +from instruments.abstract_instruments import Multimeter +from instruments.units import ureg as u + +# CLASSES ##################################################################### + + +class Fluke3000(Multimeter): + + """The `Fluke3000` is an ecosystem of devices produced by Fluke that may be + connected simultaneously to a Fluke PC3000 wireless adapter which exposes + a serial port to the computer to send and receive commands. + + The `Fluke3000` ecosystem supports the following instruments: + - Fluke 3000 FC Series Wireless Multimeter + - Fluke v3000 FC Wireless AC Voltage Module + - Fluke v3001 FC Wireless DC Voltage Module + - Fluke t3000 FC Wireless Temperature Module + + `Fluke3000` is a USB instrument that communicates through a serial port + via the PC3000 dongle. The commands used to communicate to the dongle + do not follow the SCPI standard. + + When the device is reset, it searches for available wireless modules + and binds them to the PC3000 dongle. At each initialization, this class + checks what device has been bound and saves their module number. + + This class has been tested with the 3000 FC Wireless Multimeter and + the t3000 FC Wireless Temperature Module. They have been operated + separately and simultaneously. It does not support the Wireless AC/DC + Voltage Modules as the author did not have them on hand. + + It is important to note that the mode of the multimeter cannot be set + remotely. If must be set on the device prior to the measurement. If + the measurement read back from the multimeter is not expressed in the + expected units, this module will raise an error. + + Example usage: + + >>> import instruments as ik + >>> mult = ik.fluke.Fluke3000.open_serial("/dev/ttyUSB0", 115200) + >>> mult.measure(mult.Mode.voltage_dc) # Measures the DC voltage + array(12.345) * V + + It is crucial not to kill this program in the process of making a measurement, + as for the Fluke 3000 FC Wireless Multimeter, one has to open continuous + readout, make a read and close it. If the process is killed, the read out + may not be closed and the serial cache will be constantly filled with measurements + that will interfere with any status query. If the multimeter is stuck in + continuous trigger after a bad kill, simply do: + + >>> mult.reset() + >>> mult.flush() + >>> mult.connect() + + Follow the same procedure if you want to add/remove an instrument to/from + the readout chain as the code will not look for new instruments if some + have already been connected to the PC3000 dongle. + """ + + def __init__(self, filelike): + """ + Initialize the instrument, and set the properties needed for communication. + """ + super().__init__(filelike) + self.timeout = 3 * u.second + self.terminator = "\r" + self.positions = {} + self.connect() + + # ENUMS ## + + class Module(Enum): + """ + Enum containing the supported modules serial numbers. + """ + + #: Multimeter + m3000 = 46333030304643 + #: Temperature module + t3000 = 54333030304643 + + class Mode(Enum): + """ + Enum containing the supported mode codes. + """ + + #: AC Voltage + voltage_ac = "01" + #: DC Voltage + voltage_dc = "02" + #: AC Current + current_ac = "03" + #: DC Current + current_dc = "04" + #: Frequency + frequency = "05" + #: Temperature + temperature = "07" + #: Resistance + resistance = "0B" + #: Capacitance + capacitance = "0F" + + # PROPERTIES ## + + @property + def mode(self): + """ + Gets/sets the measurement mode for the multimeter. + + The measurement mode of the multimeter must be set on the + device manually and cannot be set remotely. If a multimeter + is bound to the PC3000, returns its measurement mode by + making a measurement and checking the units bytes in response. + + :rtype: `Fluke3000.Mode` + """ + if self.Module.m3000 not in self.positions: + raise KeyError("No `Fluke3000` FC multimeter is bound") + port_id = self.positions[self.Module.m3000] + value = self.query_lines(f"rfemd 0{port_id} 1", 2)[-1] + self.query(f"rfemd 0{port_id} 2") + data = value.split("PH=")[-1] + return self.Mode(self._parse_mode(data)) + + @property + def trigger_mode(self): + """ + Gets/sets the trigger mode for the multimeter. + + The only supported mode is to trigger the device once when a + measurement is queried. This device does support continuous + triggering but it would quickly flood the serial input cache as + readouts do not overwrite each other and are accumulated. + + :rtype: `str` + """ + raise AttributeError( + "The `Fluke3000` only supports single trigger when queried" + ) + + @property + def relative(self): + """ + Gets/sets the status of relative measuring mode for the multimeter. + + The `Fluke3000` FC does not support relative measurements. + + :rtype: `bool` + """ + raise AttributeError( + "The `Fluke3000` FC does not support relative measurements" + ) + + @property + def input_range(self): + """ + Gets/sets the current input range setting of the multimeter. + + The `Fluke3000` FC is an autoranging only multimeter. + + :rtype: `str` + """ + raise AttributeError("The `Fluke3000` FC is an autoranging only multimeter") + + # METHODS # + + def connect(self): + """ + Connect to available modules and returns a dictionary + of the modules found and their port ID. + """ + self.scan() # Look for connected devices + if not self.positions: + self.reset() # Reset the PC3000 dongle + timeout = self.timeout # Store default timeout + self.timeout = ( + 30 * u.second + ) # PC 3000 can take a while to bind with wireless devices + self.query_lines("rfdis", 3) # Discover available modules and bind them + self.timeout = timeout # Restore default timeout + self.scan() # Look for connected devices + + if not self.positions: + raise ValueError("No `Fluke3000` modules available") + + def scan(self): + """ + Search for available modules and reformat. Returns a dictionary + of the modules found and their port ID. + """ + # Loop over possible channels, store device locations + positions = {} + for port_id in range(1, 7): + # Check if a device is connected to port port_id + output = self.query(f"rfebd 0{port_id} 0") + if "RFEBD" not in output: + continue + + # If it is, identify the device + self.read() + output = self.query_lines(f"rfgus 0{port_id}", 2)[-1] + module_id = int(output.split("PH=")[-1]) + if module_id == self.Module.m3000.value: + positions[self.Module.m3000] = port_id + elif module_id == self.Module.t3000.value: + positions[self.Module.t3000] = port_id + else: + raise NotImplementedError(f"Module ID {module_id} not implemented") + + self.positions = positions + + def reset(self): + """ + Resets the device and unbinds all modules. + """ + self.query_lines("ri", 3) # Resets the device + self.query_lines("rfsm 1", 2) # Turns comms on + + def read_lines(self, nlines=1): + """ + Function that keeps reading until reaches a termination + character a set amount of times. This is implemented + to handle the mutiline output of the PC3000. + + :param nlines: Number of termination characters to reach + + :type nlines: 'int' + + :return: Array of lines read out + :rtype: Array of `str` + + """ + return [self.read() for _ in range(nlines)] + + def query_lines(self, cmd, nlines=1): + """ + Function used to send a query to the instrument while allowing + for the multiline output of the PC3000. + + :param cmd: Command that will be sent to the instrument + :param nlines: Number of termination characters to reach + + :type cmd: 'str' + :type nlines: 'int' + + :return: The multiline result from the query + :rtype: Array of `str` + + """ + self.sendcmd(cmd) + return self.read_lines(nlines) + + def flush(self): + """ + Flushes the serial input cache. + + This device outputs a terminator after each output line. + The serial input cache is flushed by repeatedly reading + until a terminator is not found. + """ + timeout = self.timeout + self.timeout = 0.1 * u.second + init_time = time.time() + while time.time() - init_time < 1.0: + try: + self.read() + except OSError: + break + self.timeout = timeout + + def measure(self, mode): + """Instruct the Fluke3000 to perform a one time measurement. + + :param mode: Desired measurement mode. + + :type mode: `Fluke3000.Mode` + + :return: A measurement from the multimeter. + :rtype: `~pint.Quantity` + + """ + # Check that the mode is supported + if not isinstance(mode, self.Mode): + raise ValueError(f"Mode {mode} is not supported") + + # Check that the module associated with this mode is available + module = self._get_module(mode) + if module not in self.positions: + raise ValueError(f"Device necessary to measure {mode} is not available") + + # Query the module + value = "" + port_id = self.positions[module] + init_time = time.time() + while time.time() - init_time < 3.0: + # Read out + if mode == self.Mode.temperature: + # The temperature module supports single readout + value = self.query_lines(f"rfemd 0{port_id} 0", 2)[-1] + else: + # The multimeter does not support single readout, + # have to open continuous readout, read, then close it + value = self.query_lines(f"rfemd 0{port_id} 1", 2)[-1] + self.query(f"rfemd 0{port_id} 2") + + # Check that value is consistent with the request, break + if "PH" in value: + data = value.split("PH=")[-1] + if self._parse_mode(data) != mode.value: + if self.Module.m3000 in self.positions.keys(): + self.query(f"rfemd 0{self.positions[self.Module.m3000]} 2") + self.flush() + else: + break + + # Parse the output + value = self._parse(value, mode) + + # Return with the appropriate units + units = UNITS[mode] + return u.Quantity(value, units) + + def _get_module(self, mode): + """Gets the module associated with this measurement mode. + + :param mode: Desired measurement mode. + + :type mode: `Fluke3000.Mode` + + :return: A Fluke3000 module. + :rtype: `Fluke3000.Module` + + """ + if mode == self.Mode.temperature: + return self.Module.t3000 + + return self.Module.m3000 + + def _parse(self, result, mode): + """Parses the module output. + + :param result: Output of the query. + :param mode: Desired measurement mode. + + :type result: `string` + :type mode: `Fluke3000.Mode` + + :return: A measurement from the multimeter. + :rtype: `Quantity` + + """ + # Check that a value is contained + if "PH" not in result: + raise ValueError( + "Cannot parse a string that does not contain a return value" + ) + + # Isolate the data string from the output + data = result.split("PH=")[-1] + + # Check that the multimeter is in the right mode (fifth byte) + if self._parse_mode(data) != mode.value: + error = ( + f"Mode {mode.name} was requested but the Fluke 3000FC Multimeter is in " + f"mode {self.Mode(data[8:10]).name} instead. Could not read the requested quantity." + ) + raise ValueError(error) + + # Extract the value from the first two bytes + value = self._parse_value(data) + + # Extract the prefactor from the fourth byte + scale = self._parse_factor(data) + + # Combine and return + return scale * value + + @staticmethod + def _parse_mode(data): + """Parses the measurement mode. + + :param data: Measurement output. + + :type data: `str` + + :return: A Mode string. + :rtype: `str` + + """ + # The fixth dual hex byte encodes the measurement mode + return data[8:10] + + @staticmethod + def _parse_value(data): + """Parses the measurement value. + + :param data: Measurement output. + + :type data: `str` + + :return: A value. + :rtype: `float` + + """ + # The second dual hex byte is the most significant byte + return int(data[2:4] + data[:2], 16) + + @staticmethod + def _parse_factor(data): + """Parses the measurement prefactor. + + :param data: Measurement output. + + :type data: `str` + + :return: A prefactor. + :rtype: `float` + + """ + # Convert the fourth dual hex byte to an 8 bits string + byte = format(int(data[6:8], 16), "08b") + + # The first bit encodes the sign (0 positive, 1 negative) + sign = 1 if byte[0] == "0" else -1 + + # The second to fourth bits encode the metric prefix + code = int(byte[1:4], 2) + if code not in PREFIXES: + raise ValueError(f"Metric prefix not recognized: {code}") + prefix = PREFIXES[code] + + # The sixth and seventh bit encode the decimal place + scale = 10 ** (-int(byte[5:7], 2)) + + # Return the combination + return sign * prefix * scale + + +# UNITS ####################################################################### + +UNITS = { + None: 1, + Fluke3000.Mode.voltage_ac: u.volt, + Fluke3000.Mode.voltage_dc: u.volt, + Fluke3000.Mode.current_ac: u.amp, + Fluke3000.Mode.current_dc: u.amp, + Fluke3000.Mode.frequency: u.hertz, + Fluke3000.Mode.temperature: u.degC, + Fluke3000.Mode.resistance: u.ohm, + Fluke3000.Mode.capacitance: u.farad, +} + +# METRIC PREFIXES ############################################################# + +PREFIXES = { + 0: 1e0, # None + 2: 1e6, # Mega + 3: 1e3, # Kilo + 4: 1e-3, # milli + 5: 1e-6, # micro + 6: 1e-9, # nano +} diff --git a/instruments/generic_scpi/__init__.py b/instruments/generic_scpi/__init__.py index f91dac4e8..39e4897c7 100644 --- a/instruments/generic_scpi/__init__.py +++ b/instruments/generic_scpi/__init__.py @@ -1,10 +1,8 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing generic SCPI instruments """ -from __future__ import absolute_import from .scpi_instrument import SCPIInstrument from .scpi_multimeter import SCPIMultimeter diff --git a/instruments/generic_scpi/scpi_function_generator.py b/instruments/generic_scpi/scpi_function_generator.py index fcad54c04..31bc5c21b 100644 --- a/instruments/generic_scpi/scpi_function_generator.py +++ b/instruments/generic_scpi/scpi_function_generator.py @@ -1,15 +1,12 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides support for SCPI compliant function generators """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division -import quantities as pq +from instruments.units import ureg as u from instruments.abstract_instruments import FunctionGenerator from instruments.generic_scpi import SCPIInstrument @@ -27,24 +24,20 @@ class SCPIFunctionGenerator(FunctionGenerator, SCPIInstrument): Example usage: >>> import instruments as ik - >>> import quantities as pq + >>> import instruments.units as u >>> inst = ik.generic_scpi.SCPIFunctionGenerator.open_tcpip("192.168.1.1") - >>> inst.frequency = 1 * pq.kHz + >>> inst.frequency = 1 * u.kHz """ - def __init__(self, filelike): - super(SCPIFunctionGenerator, self).__init__(filelike) - # CONSTANTS # _UNIT_MNEMONICS = { FunctionGenerator.VoltageMode.peak_to_peak: "VPP", - FunctionGenerator.VoltageMode.rms: "VRMS", - FunctionGenerator.VoltageMode.dBm: "DBM", + FunctionGenerator.VoltageMode.rms: "VRMS", + FunctionGenerator.VoltageMode.dBm: "DBM", } - _MNEMONIC_UNITS = dict((mnem, unit) - for unit, mnem in _UNIT_MNEMONICS.items()) + _MNEMONIC_UNITS = {mnem: unit for unit, mnem in _UNIT_MNEMONICS.items()} # FunctionGenerator CONTRACT # @@ -58,10 +51,7 @@ def _get_amplitude_(self): """ units = self.query("VOLT:UNIT?").strip() - return ( - float(self.query("VOLT?").strip()), - self._MNEMONIC_UNITS[units] - ) + return (float(self.query("VOLT?").strip()), self._MNEMONIC_UNITS[units]) def _set_amplitude_(self, magnitude, units): """ @@ -72,44 +62,44 @@ def _set_amplitude_(self, magnitude, units): :param units: The type of voltage measurements units :type units: `FunctionGenerator.VoltageMode` """ - self.sendcmd("VOLT:UNIT {}".format(self._UNIT_MNEMONICS[units])) - self.sendcmd("VOLT {}".format(magnitude)) + self.sendcmd(f"VOLT:UNIT {self._UNIT_MNEMONICS[units]}") + self.sendcmd(f"VOLT {magnitude}") # PROPERTIES # frequency = unitful_property( - name="FREQ", - units=pq.Hz, + command="FREQ", + units=u.Hz, doc=""" Gets/sets the output frequency. :units: As specified, or assumed to be :math:`\\text{Hz}` otherwise. - :type: `float` or `~quantities.quantity.Quantity` - """ + :type: `float` or `~pint.Quantity` + """, ) function = enum_property( - name="FUNC", - enum=lambda: Function, # pylint: disable=undefined-variable + command="FUNC", + enum=FunctionGenerator.Function, doc=""" Gets/sets the output function of the function generator :type: `SCPIFunctionGenerator.Function` - """ + """, ) offset = unitful_property( - name="VOLT:OFFS", - units=pq.volt, + command="VOLT:OFFS", + units=u.volt, doc=""" Gets/sets the offset voltage of the function generator. Set value should be within correct bounds of instrument. - :units: As specified (if a `~quntities.quantity.Quantity`) or assumed + :units: As specified (if a `~pint.Quantity`) or assumed to be of units volts. - :type: `~quantities.quantity.Quantity` with units volts. - """ + :type: `~pint.Quantity` with units volts. + """, ) @property diff --git a/instruments/generic_scpi/scpi_instrument.py b/instruments/generic_scpi/scpi_instrument.py index 30d16f670..e5395addf 100644 --- a/instruments/generic_scpi/scpi_instrument.py +++ b/instruments/generic_scpi/scpi_instrument.py @@ -1,20 +1,14 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides support for SCPI compliant instruments """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division - -from builtins import map - from enum import IntEnum -import quantities as pq from instruments.abstract_instruments import Instrument +from instruments.units import ureg as u from instruments.util_fns import assume_units # CLASSES ##################################################################### @@ -37,9 +31,6 @@ class SCPIInstrument(Instrument): >>> print(inst.name) """ - def __init__(self, filelike): - super(SCPIInstrument, self).__init__(filelike) - # PROPERTIES # @property @@ -50,7 +41,7 @@ def name(self): :rtype: `str` """ - return self.query('*IDN?') + return self.query("*IDN?") @property def scpi_version(self): @@ -68,7 +59,7 @@ def op_complete(self): :rtype: `bool` """ - result = self.query('*OPC?') + result = self.query("*OPC?") return bool(int(result)) @property @@ -78,19 +69,19 @@ def power_on_status(self): :type: `bool` """ - result = self.query('*PSC?') + result = self.query("*PSC?") return bool(int(result)) @power_on_status.setter def power_on_status(self, newval): - on = ['on', '1', 1, True] - off = ['off', '0', 0, False] + on = ["on", "1", 1, True] + off = ["off", "0", 0, False] if isinstance(newval, str): newval = newval.lower() if newval in on: - self.sendcmd('*PSC 1') + self.sendcmd("*PSC 1") elif newval in off: - self.sendcmd('*PSC 0') + self.sendcmd("*PSC 0") else: raise ValueError @@ -102,7 +93,7 @@ def self_test_ok(self): :rtype: `bool` """ - result = self.query('*TST?') + result = self.query("*TST?") try: result = int(result) return result == 0 @@ -116,14 +107,14 @@ def reset(self): Reset instrument. On many instruments this is a factory reset and will revert all settings to default. """ - self.sendcmd('*RST') + self.sendcmd("*RST") def clear(self): """ Clear instrument. Consult manual for specifics related to that instrument. """ - self.sendcmd('*CLS') + self.sendcmd("*CLS") def trigger(self): """ @@ -134,14 +125,14 @@ def trigger(self): This software trigger usually performs the same action as a hardware trigger to your instrument. """ - self.sendcmd('*TRG') + self.sendcmd("*TRG") def wait_to_continue(self): """ Instruct the instrument to wait until it has completed all received commands before continuing. """ - self.sendcmd('*WAI') + self.sendcmd("*WAI") # SYSTEM COMMANDS ## @@ -152,18 +143,15 @@ def line_frequency(self): :return: The power line frequency :units: Hertz - :type: `~quantities.quantity.Quantity` + :type: `~pint.Quantity` """ - return pq.Quantity( - float(self.query("SYST:LFR?")), - "Hz" - ) + return u.Quantity(float(self.query("SYST:LFR?")), "Hz") @line_frequency.setter def line_frequency(self, newval): - self.sendcmd("SYST:LFR {}".format( - assume_units(newval, "Hz").rescale("Hz").magnitude - )) + self.sendcmd( + "SYST:LFR {}".format(assume_units(newval, "Hz").to("Hz").magnitude) + ) # ERROR QUEUE HANDLING ## # NOTE: This functionality is still quite incomplete, and could be fleshed @@ -178,6 +166,7 @@ class ErrorCodes(IntEnum): Enumeration describing error codes as defined by SCPI 1999.0. Error codes that are equal to 0 mod 100 are defined to be *generic*. """ + # NOTE: this class may be overriden by subclasses, since the only access # to this enumeration from within SCPIInstrument is by "self," # not by explicit name. Thus, if an instrument supports additional @@ -278,9 +267,8 @@ def display_brightness(self): @display_brightness.setter def display_brightness(self, newval): if newval < 0 or newval > 1: - raise ValueError("Display brightness must be a number between 0" - " and 1.") - self.sendcmd("DISP:BRIG {}".format(newval)) + raise ValueError("Display brightness must be a number between 0" " and 1.") + self.sendcmd(f"DISP:BRIG {newval}") @property def display_contrast(self): @@ -295,6 +283,5 @@ def display_contrast(self): @display_contrast.setter def display_contrast(self, newval): if newval < 0 or newval > 1: - raise ValueError("Display brightness must be a number between 0" - " and 1.") - self.sendcmd("DISP:CONT {}".format(newval)) + raise ValueError("Display contrast must be a number between 0" " and 1.") + self.sendcmd(f"DISP:CONT {newval}") diff --git a/instruments/generic_scpi/scpi_multimeter.py b/instruments/generic_scpi/scpi_multimeter.py index 213d9b3d7..1899cc490 100644 --- a/instruments/generic_scpi/scpi_multimeter.py +++ b/instruments/generic_scpi/scpi_multimeter.py @@ -1,17 +1,14 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides support for SCPI compliant multimeters """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division from enum import Enum -import quantities as pq +from instruments.units import ureg as u from instruments.abstract_instruments import Multimeter from instruments.generic_scpi import SCPIInstrument @@ -19,15 +16,15 @@ # CONSTANTS ################################################################### -VALID_FRES_NAMES = ['4res', '4 res', 'four res', 'f res'] +VALID_FRES_NAMES = ["4res", "4 res", "four res", "f res"] -UNITS_CAPACITANCE = ['cap'] -UNITS_VOLTAGE = ['volt:dc', 'volt:ac', 'diod'] -UNITS_CURRENT = ['curr:dc', 'curr:ac'] -UNITS_RESISTANCE = ['res', 'fres'] + VALID_FRES_NAMES -UNITS_FREQUENCY = ['freq'] -UNITS_TIME = ['per'] -UNITS_TEMPERATURE = ['temp'] +UNITS_CAPACITANCE = ["cap"] +UNITS_VOLTAGE = ["volt:dc", "volt:ac", "diod"] +UNITS_CURRENT = ["curr:dc", "curr:ac"] +UNITS_RESISTANCE = ["res", "fres"] + VALID_FRES_NAMES +UNITS_FREQUENCY = ["freq"] +UNITS_TIME = ["per"] +UNITS_TEMPERATURE = ["temp"] # CLASSES ##################################################################### @@ -45,9 +42,6 @@ class SCPIMultimeter(SCPIInstrument, Multimeter): >>> print(inst.measure(inst.Mode.resistance)) """ - def __init__(self, filelike): - super(SCPIMultimeter, self).__init__(filelike) - # ENUMS ## class Mode(Enum): @@ -55,6 +49,7 @@ class Mode(Enum): """ Enum of valid measurement modes for (most) SCPI compliant multimeters """ + capacitance = "CAP" continuity = "CONT" current_ac = "CURR:AC" @@ -82,6 +77,7 @@ class TriggerMode(Enum): "Bus": Causes the instrument to trigger when a ``*TRG`` command is sent by software. This means calling the trigger() function. """ + immediate = "IMM" external = "EXT" bus = "BUS" @@ -91,6 +87,7 @@ class InputRange(Enum): """ Valid device range parameters outside of directly specifying the range. """ + minimum = "MIN" maximum = "MAX" default = "DEF" @@ -102,6 +99,7 @@ class Resolution(Enum): Valid measurement resolution parameters outside of directly the resolution. """ + minimum = "MIN" maximum = "MAX" default = "DEF" @@ -111,6 +109,7 @@ class TriggerCount(Enum): """ Valid trigger count parameters outside of directly the value. """ + minimum = "MIN" maximum = "MAX" default = "DEF" @@ -121,6 +120,7 @@ class SampleCount(Enum): """ Valid sample count parameters outside of directly the value. """ + minimum = "MIN" maximum = "MAX" default = "DEF" @@ -137,14 +137,15 @@ class SampleSource(Enum): #. "timer": Successive samples start one sample interval after the START of the previous sample. """ + immediate = "IMM" timer = "TIM" # PROPERTIES ## - # pylint: disable=unnecessary-lambda + # pylint: disable=unnecessary-lambda,undefined-variable mode = enum_property( - name="CONF", + command="CONF", enum=Mode, doc=""" Gets/sets the current measurement mode for the multimeter. @@ -156,11 +157,11 @@ class SampleSource(Enum): :type: `~SCPIMultimeter.Mode` """, input_decoration=lambda x: SCPIMultimeter._mode_parse(x), - set_fmt="{}:{}" + set_fmt="{}:{}", ) trigger_mode = enum_property( - name="TRIG:SOUR", + command="TRIG:SOUR", enum=TriggerMode, doc=""" Gets/sets the SCPI Multimeter trigger mode. @@ -170,7 +171,8 @@ class SampleSource(Enum): >>> dmm.trigger_mode = dmm.TriggerMode.external :type: `~SCPIMultimeter.TriggerMode` - """) + """, + ) @property def input_range(self): @@ -181,12 +183,12 @@ def input_range(self): Example usages: >>> dmm.input_range = dmm.InputRange.automatic - >>> dmm.input_range = 1 * pq.millivolt + >>> dmm.input_range = 1 * u.millivolt :units: As appropriate for the current mode setting. - :type: `~quantities.Quantity`, or `~SCPIMultimeter.InputRange` + :type: `~pint.Quantity`, or `~SCPIMultimeter.InputRange` """ - value = self.query('CONF?') + value = self.query("CONF?") mode = self.Mode(self._mode_parse(value)) value = value.split(" ")[1].split(",")[0] # Extract device range try: @@ -202,8 +204,8 @@ def input_range(self, newval): if isinstance(newval, self.InputRange): newval = newval.value else: - newval = assume_units(newval, units).rescale(units).magnitude - self.sendcmd("CONF:{} {}".format(mode.value, newval)) + newval = assume_units(newval, units).to(units).magnitude + self.sendcmd(f"CONF:{mode.value} {newval}") @property def resolution(self): @@ -219,7 +221,7 @@ def resolution(self): :type: `int`, `float` or `~SCPIMultimeter.Resolution` """ - value = self.query('CONF?') + value = self.query("CONF?") value = value.split(" ")[1].split(",")[1] # Extract resolution try: return float(value) @@ -234,9 +236,11 @@ def resolution(self, newval): if isinstance(newval, self.Resolution): newval = newval.value elif not isinstance(newval, (float, int)): - raise TypeError("Resolution must be specified as an int, float, " - "or SCPIMultimeter.Resolution value.") - self.sendcmd("CONF:{} {},{}".format(mode.value, input_range, newval)) + raise TypeError( + "Resolution must be specified as an int, float, " + "or SCPIMultimeter.Resolution value." + ) + self.sendcmd(f"CONF:{mode.value} {input_range},{newval}") @property def trigger_count(self): @@ -263,7 +267,7 @@ def trigger_count(self): :type: `int` or `~SCPIMultimeter.TriggerCount` """ - value = self.query('TRIG:COUN?') + value = self.query("TRIG:COUN?") try: return int(value) except ValueError: @@ -274,9 +278,11 @@ def trigger_count(self, newval): if isinstance(newval, self.TriggerCount): newval = newval.value elif not isinstance(newval, int): - raise TypeError("Trigger count must be specified as an int " - "or SCPIMultimeter.TriggerCount value.") - self.sendcmd("TRIG:COUN {}".format(newval)) + raise TypeError( + "Trigger count must be specified as an int " + "or SCPIMultimeter.TriggerCount value." + ) + self.sendcmd(f"TRIG:COUN {newval}") @property def sample_count(self): @@ -304,7 +310,7 @@ def sample_count(self): :type: `int` or `~SCPIMultimeter.SampleCount` """ - value = self.query('SAMP:COUN?') + value = self.query("SAMP:COUN?") try: return int(value) except ValueError: @@ -315,24 +321,26 @@ def sample_count(self, newval): if isinstance(newval, self.SampleCount): newval = newval.value elif not isinstance(newval, int): - raise TypeError("Sample count must be specified as an int " - "or SCPIMultimeter.SampleCount value.") - self.sendcmd("SAMP:COUN {}".format(newval)) + raise TypeError( + "Sample count must be specified as an int " + "or SCPIMultimeter.SampleCount value." + ) + self.sendcmd(f"SAMP:COUN {newval}") trigger_delay = unitful_property( - name="TRIG:DEL", - units=pq.second, + command="TRIG:DEL", + units=u.second, doc=""" Gets/sets the time delay which the multimeter will use following receiving a trigger event before starting the measurement. :units: As specified, or assumed to be of units seconds otherwise. - :type: `~quantities.Quantity` - """ + :type: `~pint.Quantity` + """, ) sample_source = enum_property( - name="SAMP:SOUR", + command="SAMP:SOUR", enum=SampleSource, doc=""" Gets/sets the multimeter sample source. This determines whether the @@ -343,12 +351,12 @@ def sample_count(self, newval): after the trigger event. After that, it depends on which mode is used. :type: `SCPIMultimeter.SampleSource` - """ + """, ) sample_timer = unitful_property( - name="SAMP:TIM", - units=pq.second, + command="SAMP:TIM", + units=u.second, doc=""" Gets/sets the sample interval when the sample counter is greater than one and when the sample source is set to timer (see @@ -359,8 +367,8 @@ def sample_count(self, newval): `~SCPIMultimeter.trigger_delay` property. :units: As specified, or assumed to be of units seconds otherwise. - :type: `~quantities.Quantity` - """ + :type: `~pint.Quantity` + """, ) @property @@ -391,10 +399,12 @@ def measure(self, mode=None): if mode is None: mode = self.mode if not isinstance(mode, SCPIMultimeter.Mode): - raise TypeError("Mode must be specified as a SCPIMultimeter.Mode " - "value, got {} instead.".format(type(mode))) + raise TypeError( + "Mode must be specified as a SCPIMultimeter.Mode " + "value, got {} instead.".format(type(mode)) + ) # pylint: disable=no-member - value = float(self.query('MEAS:{}?'.format(mode.value))) + value = float(self.query(f"MEAS:{mode.value}?")) return value * UNITS[mode] # INTERNAL FUNCTIONS ## @@ -418,19 +428,20 @@ def _mode_parse(val): val = "VOLT:DC" return val + # UNITS ####################################################################### UNITS = { - SCPIMultimeter.Mode.capacitance: pq.farad, - SCPIMultimeter.Mode.voltage_dc: pq.volt, - SCPIMultimeter.Mode.voltage_ac: pq.volt, - SCPIMultimeter.Mode.diode: pq.volt, - SCPIMultimeter.Mode.current_ac: pq.amp, - SCPIMultimeter.Mode.current_dc: pq.amp, - SCPIMultimeter.Mode.resistance: pq.ohm, - SCPIMultimeter.Mode.fourpt_resistance: pq.ohm, - SCPIMultimeter.Mode.frequency: pq.hertz, - SCPIMultimeter.Mode.period: pq.second, - SCPIMultimeter.Mode.temperature: pq.kelvin, - SCPIMultimeter.Mode.continuity: 1, + SCPIMultimeter.Mode.capacitance: u.farad, + SCPIMultimeter.Mode.voltage_dc: u.volt, + SCPIMultimeter.Mode.voltage_ac: u.volt, + SCPIMultimeter.Mode.diode: u.volt, + SCPIMultimeter.Mode.current_ac: u.amp, + SCPIMultimeter.Mode.current_dc: u.amp, + SCPIMultimeter.Mode.resistance: u.ohm, + SCPIMultimeter.Mode.fourpt_resistance: u.ohm, + SCPIMultimeter.Mode.frequency: u.hertz, + SCPIMultimeter.Mode.period: u.second, + SCPIMultimeter.Mode.temperature: u.kelvin, + SCPIMultimeter.Mode.continuity: 1, } diff --git a/instruments/gentec_eo/__init__.py b/instruments/gentec_eo/__init__.py new file mode 100644 index 000000000..85200fda3 --- /dev/null +++ b/instruments/gentec_eo/__init__.py @@ -0,0 +1,3 @@ +"""Module containing Gentec-eo instruments.""" + +from .blu import Blu diff --git a/instruments/gentec_eo/blu.py b/instruments/gentec_eo/blu.py new file mode 100644 index 000000000..bae1b18a8 --- /dev/null +++ b/instruments/gentec_eo/blu.py @@ -0,0 +1,653 @@ +"""Support for Gentec-EO Blu devices.""" + + +# IMPORTS ##################################################################### + +from enum import Enum +from time import sleep + +from instruments.abstract_instruments import Instrument +from instruments.units import ureg as u +from instruments.util_fns import assume_units + +# CLASSES ##################################################################### + + +class Blu(Instrument): + """Communicate with Gentec-eo BLU power / energy meter interfaces. + + These instruments communicate via USB or via bluetooth. The + bluetooth sender / receiver that is provided with the instrument is + simply emulating a COM port. This routine cannot pair the device + with bluetooth, but once it is paired, it can communicate with the + port. Alternatively, you can plug the device into the computer using + a USB cable. + + .. warning:: If commands are issued too fast, the device will not + answer. Experimentally, a 1 ms delay should be enough to get the + device into answering mode. Keep this in mind when issuing many + commands at once. No wait time included in this class. + + .. note:: The instrument also has a possiblity to read a continuous + data stream. This is currently not implemented here since it + would have to be threaded out. + + Example: + >>> import instruments as ik + >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') + >>> inst.current_value + 3.004 W + """ + + def __init__(self, filelike): + super().__init__(filelike) + + # use a terminator for blu, even though none required + self.terminator = "\r\n" + + # define the power mode + self._power_mode = None + + # acknowledgement message + self._ack_message = "ACK" + + def _ack_expected(self, msg=""): + """Set up acknowledgement checking.""" + return self._ack_message + + # ENUMS # + + class Scale(Enum): + """Available scales for Blu devices. + + The following list maps available scales of the Blu devices + to the respective indexes. All scales are either in watts or + joules, depending if power or energy mode is activated. + Furthermore, the maximum value that can be measured determines + the name of the scale to be set. Prefixes are given in the + `enum` class while the unit is omitted since it depends on the + mode the head is in. + """ + + max1pico = "00" + max3pico = "01" + max10pico = "02" + max30pico = "03" + max100pico = "04" + max300pico = "05" + max1nano = "06" + max3nano = "07" + max10nano = "08" + max30nano = "09" + max100nano = "10" + max300nano = "11" + max1micro = "12" + max3micro = "13" + max10micro = "14" + max30micro = "15" + max100micro = "16" + max300micro = "17" + max1milli = "18" + max3milli = "19" + max10milli = "20" + max30milli = "21" + max100milli = "22" + max300milli = "23" + max1 = "24" + max3 = "25" + max10 = "26" + max30 = "27" + max100 = "28" + max300 = "29" + max1kilo = "30" + max3kilo = "31" + max10kilo = "32" + max30kilo = "33" + max100kilo = "34" + max300kilo = "35" + max1Mega = "36" + max3Mega = "37" + max10Mega = "38" + max30Mega = "39" + max100Mega = "40" + max300Mega = "41" + + # PROPERTIES # + + @property + def anticipation(self): + """Get / Set anticipation. + + This command is used to enable or disable the anticipation + processing when the device is reading from a wattmeter. The + anticipation is a software-based acceleration algorithm that + provides faster readings using the detector’s calibration. + + :return: Is anticipation enabled or not. + :rtype: bool + + Example: + >>> import instruments as ik + >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') + >>> inst.anticipation + True + >>> inst.anticipation = False + """ + return self._value_query("*GAN", tp=int) == 1 + + @anticipation.setter + def anticipation(self, newval): + sendval = 1 if newval else 0 + self.sendcmd(f"*ANT{sendval}") + + @property + def auto_scale(self): + """Get / Set auto scale on the device. + + :return: Status of auto scale enabled feature. + :rtype: bool + + :raises ValueError: The command was not acknowledged by the + device. + + Example: + >>> import instruments as ik + >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') + >>> inst.auto_scale + True + >>> inst.auto_scale = False + """ + resp = self._value_query("*GAS", tp=int) + return resp == 1 + + @auto_scale.setter + def auto_scale(self, newval): + sendval = 1 if newval else 0 + self.sendcmd(f"*SAS{sendval}") + + @property + def available_scales(self): + """Get available scales from connected device. + + :return: Scales currently available on device. + :rtype: :class:`Blu.Scale` + + Example: + >>> import instruments as ik + >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') + >>> inst.available_scales + [, , + , , , + , ] + """ + # set no terminator and a 1 second timeout + _terminator = self.terminator + self.terminator = "" + _timeout = self.timeout + self.timeout = u.Quantity(1, u.s) + + try: + # get the response + resp = self._no_ack_query("*DVS").split("\r\n") + finally: + # set back terminator and 3 second timeout + self.terminator = _terminator + self.timeout = _timeout + + # prepare return + retlist = [] # init return list of enums + for line in resp: + if len(line) > 0: # account for empty lines + index = line[line.find("[") + 1 : line.find("]")] + retlist.append(self.Scale(index)) + return retlist + + @property + def battery_state(self): + """Get the charge state of the battery. + + :return: Charge state of battery + :rtype: u.percent + + Example: + >>> import instruments as ik + >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') + >>> inst.battery_state + array(100.) * % + """ + resp = self._no_ack_query("*QSO").rstrip() + resp = float(resp[resp.find("=") + 1 : len(resp)]) + return u.Quantity(resp, u.percent) + + @property + def current_value(self): + """Get the currently measured value (unitful). + + :return: Currently measured value + :rtype: u.Quantity + + Example: + >>> import instruments as ik + >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') + >>> inst.current_value + 3.004 W + """ + if self._power_mode is None: + _ = self.measure_mode # determine the power mode + sleep(0.01) + + unit = u.W if self._power_mode else u.J + return u.Quantity(float(self._no_ack_query("*CVU")), unit) + + @property + def head_type(self): + """Get the head type information. + + :return: Type of instrument head. + :rtype: str + + Example: + >>> import instruments as ik + >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') + >>> inst.head_type + 'NIG : 104552, Wattmeter, V1.95' + """ + return self._no_ack_query("*GFW") + + @property + def measure_mode(self): + """Get the current measurement mode. + + Potential return values are 'power', which inidcates power mode + in W and 'sse', indicating single shot energy mode in J. + + :return: 'power' if in power mode, 'sse' if in single shot + energy mode. + :rtype: str + + Example: + >>> import instruments as ik + >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') + >>> inst.measure_mode + 'power' + """ + resp = self._value_query("*GMD", tp=int) + if resp == 0: + self._power_mode = True + return "power" + else: + self._power_mode = False + return "sse" + + @property + def new_value_ready(self): + """Get status if a new value is ready. + + This command is used to check whether a new value is available + from the device. Though optional, its use is recommended when + used with single pulse operation. + + :return: Is a new value ready? + :rtype: bool + + Example: + >>> import instruments as ik + >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') + >>> inst.new_value_ready + False + """ + resp = self._no_ack_query("*NVU") + return False if resp.find("Not") > -1 else True + + @property + def scale(self): + """Get / Set measurement scale. + + The measurement scales are chosen from the the `Scale` enum + class. Scales are either in watts or joules, depending on what + state the power meter is currently in. + + .. note:: Setting a scale manually will automatically turn of + auto scale. + + :return: Scale that is currently set. + :rtype: :class:`Blu.Scale` + + :raises ValueError: The command was not acknowledged by the + device. A scale that is not available might have been + selected. Use `available_scales` to display scales that + are possible on your device. + + Example: + >>> import instruments as ik + >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') + >>> inst.scale = inst.Scale.max3 + >>> inst.scale + + """ + return self.Scale(self._value_query("*GCR")) + + @scale.setter + def scale(self, newval): + self.sendcmd(f"*SCS{newval.value}") + + @property + def single_shot_energy_mode(self): + """Get / Set single shot energy mode. + + :return: Is single shot energy mode turned on? + :rtype: bool + + Example: + >>> import instruments as ik + >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') + >>> inst.single_shot_energy_mode + False + >>> inst.single_shot_energy_mode = True + """ + val = self._value_query("*GSE", tp=int) == 1 + self._power_mode = False if val else True + return val + + @single_shot_energy_mode.setter + def single_shot_energy_mode(self, newval): + sendval = 1 if newval else 0 # set send value + self._power_mode = False if newval else True # set power mode + self.sendcmd(f"*SSE{sendval}") + + @property + def trigger_level(self): + """Get / Set trigger level when in energy mode. + + The trigger level must be between 0.001 and 0.998. + + :return: Trigger level (absolute) with respect to the currently + set scale + :rtype: float + + :raise ValueError: Trigger level out of range. + + Example: + >>> import instruments as ik + >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') + >>> inst.trigger_level = 0.153 + >>> inst.trigger_level + 0.153 + + """ + level = self._no_ack_query("*GTL") + # get the percent + retval = float(level[level.find(":") + 1 : level.find("%")]) / 100 + return retval + + @trigger_level.setter + def trigger_level(self, newval): + if newval < 0.001 or newval > 0.99: + raise ValueError( + "Trigger level {} is out of range. It must be " + "between 0.001 and 0.998.".format(newval) + ) + + newval = newval * 100.0 + if newval >= 10: + newval = str(round(newval, 1)).zfill(4) + else: + newval = str(round(newval, 2)).zfill(4) + + self.sendcmd(f"*STL{newval}") + + @property + def usb_state(self): + """Get status if USB cable is connected. + + :return: Is a USB cable connected? + :rtype: bool + + Example: + >>> import instruments as ik + >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') + >>> inst.usb_state + True + """ + return self._value_query("*USB", tp=int) == 1 + + @property + def user_multiplier(self): + """Get / Set user multiplier. + + :return: User multiplier + :rtype: u.Quantity + + Example: + >>> import instruments as ik + >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') + >>> inst.user_multiplier = 10 + >>> inst.user_multiplier + 10.0 + """ + return self._value_query("*GUM", tp=float) + + @user_multiplier.setter + def user_multiplier(self, newval): + sendval = _format_eight(newval) # sendval: 8 characters long + self.sendcmd(f"*MUL{sendval}") + + @property + def user_offset(self): + """Get / Set user offset. + + The user offset can be set unitful in watts or joules and set + to the device. + + :return: User offset + :rtype: u.Quantity + + :raises ValueError: Unit not supported or value for offset is + out of range. + + Example: + >>> import instruments as ik + >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') + >>> inst.user_offset = 10 + >>> inst.user_offset + array(10.) * W + """ + if self._power_mode is None: + _ = self.measure_mode # determine the power mode + sleep(0.01) + + if self._power_mode: + return assume_units(self._value_query("*GUO", tp=float), u.W) + else: + return assume_units(self._value_query("*GUO", tp=float), u.J) + + @user_offset.setter + def user_offset(self, newval): + # if unitful, try to rescale and grab magnitude + if isinstance(newval, u.Quantity): + if newval.is_compatible_with(u.W): + newval = newval.to(u.W).magnitude + elif newval.is_compatible_with(u.J): + newval = newval.to(u.J).magnitude + else: + raise ValueError( + "Value must be given in watts, " "joules, or unitless." + ) + sendval = _format_eight(newval) # sendval: 8 characters long + self.sendcmd(f"*OFF{sendval}") + + @property + def version(self): + """Get device information. + + :return: Version and device type + :rtype: str + + Example: + >>> import instruments as ik + >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') + >>> inst.version + 'Blu firmware Version 1.95' + """ + return self._no_ack_query("*VER") + + @property + def wavelength(self): + """Get / Set the wavelength. + + The wavelength can be set unitful. Specifying zero as a + wavelength or providing an out-of-bound value as a parameter + restores the default settings, typically 1064nm. If no units + are provided, nm are assumed. + + :return: Wavelength in nm + :rtype: u.Quantity + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') + >>> inst.wavelength = u.Quantity(527, u.nm) + >>> inst.wavelength + array(527) * nm + """ + return u.Quantity(self._value_query("*GWL", tp=int), u.nm) + + @wavelength.setter + def wavelength(self, newval): + val = round(assume_units(newval, u.nm).to(u.nm).magnitude) + if val >= 1000000 or val < 0: # can only send 5 digits + val = 0 # out of bound anyway + val = str(int(val)).zfill(5) + self.sendcmd(f"*PWC{val}") + + @property + def zero_offset(self): + """Get / Set zero offset. + + Gets the status if zero offset is enabled. When set to `True`, + the device will read the current level immediately for around + three seconds and then set the baseline to the averaged value. + If activated and set to `True` again, a new value for the + baseline will be established. + + :return: Is zero offset enabled? + :rtype: bool + + Example: + >>> import instruments as ik + >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') + >>> inst.zero_offset + True + >>> inst.zero_offset = False + """ + return self._value_query("*GZO", tp=int) == 1 + + @zero_offset.setter + def zero_offset(self, newval): + if newval: + self.sendcmd("*SOU") + else: + self.sendcmd("*COU") + + # METHODS # + + def confirm_connection(self): + """Confirm a connection to the device. + + Turns of bluetooth searching by confirming a connection. + + Example: + >>> import instruments as ik + >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') + >>> inst.confirm_connection() + """ + self.sendcmd("*RDY") + + def disconnect(self): + """Disconnect the device. + + Example: + >>> import instruments as ik + >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') + >>> inst.disconnect() + """ + self.sendcmd("*BTD") + + def scale_down(self): + """Set scale to next lower level. + + Sets the power meter to the next lower scale. If already at + the lowest possible scale, no change will be made. + + Example: + >>> import instruments as ik + >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') + >>> inst.scale_down() + """ + self.sendcmd("*SSD") + + def scale_up(self): + """Set scale to next higher level. + + Sets the power meter to the next higher scale. If already at + the highest possible scale, no change will be made. + + Example: + >>> import instruments as ik + >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') + >>> inst.scale_up() + """ + self.sendcmd("*SSU") + + # PRIVATE METHODS # + + def _no_ack_query(self, cmd, size=-1): + """Query a value and don't expect an ACK message.""" + self._ack_message = None + try: + value = self.query(cmd, size=size) + finally: + self._ack_message = "ACK" + return value + + def _value_query(self, cmd, tp=str): + """Query one specific value and return it. + + :param cmd: Command to send to self._no_ack_query. + :type cmd: str + :param tp: Type of the value to be returned, default: str + :type tp: type + + :return: Single value of query. + :rtype: tp (selected type) + + :raises ValueError: Conversion of response into given type was + unsuccessful. + """ + resp = self._no_ack_query(cmd).rstrip() # strip \r\n + resp = resp.split(":")[1] # strip header off + resp = resp.replace(" ", "") # strip white space + if isinstance(resp, tp): + return resp + else: + return tp(resp) + + +def _format_eight(value): + """Formats a value to eight characters total. + + :param value: value to be formatted, > -1e100 and < 1e100 + :type value: int,float + + :return: Value formatted to 8 characters + :rtype: str + """ + if len(str(value)) > 8: + if value < 0: + value = f"{value:.2g}".zfill(8) # make space for - + else: + value = f"{value:.3g}".zfill(8) + else: + value = str(value).zfill(8) + return value diff --git a/instruments/glassman/__init__.py b/instruments/glassman/__init__.py new file mode 100644 index 000000000..05d01775b --- /dev/null +++ b/instruments/glassman/__init__.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python +""" +Module containing Glassman power supplies +""" + + +from .glassmanfr import GlassmanFR diff --git a/instruments/glassman/glassmanfr.py b/instruments/glassman/glassmanfr.py new file mode 100644 index 000000000..381d366b2 --- /dev/null +++ b/instruments/glassman/glassmanfr.py @@ -0,0 +1,487 @@ +#!/usr/bin/env python +# +# hpe3631a.py: Driver for the Glassman FR Series Power Supplies +# +# © 2019 Francois Drielsma (francois.drielsma@gmail.com). +# +# This file is a part of the InstrumentKit project. +# Licensed under the AGPL version 3. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +""" +Driver for the Glassman FR Series Power Supplies + +Originally contributed and copyright held by Francois Drielsma +(francois.drielsma@gmail.com) + +An unrestricted license has been provided to the maintainers of the Instrument +Kit project. +""" +# IMPORTS ##################################################################### + +from struct import unpack +from enum import Enum + +from instruments.abstract_instruments import PowerSupply +from instruments.units import ureg as u +from instruments.util_fns import assume_units + +# CLASSES ##################################################################### + + +class GlassmanFR(PowerSupply, PowerSupply.Channel): + + """ + The GlassmanFR is a single output power supply. + + Because it is a single channel output, this object inherits from both + PowerSupply and PowerSupply.Channel. + + This class should work for any of the Glassman FR Series power supplies + and is also likely to work for the EJ, ET, EY and FJ Series which seem + to share their communication protocols. The code has only been tested + by the author with an Glassman FR50R6 power supply. + + Before this power supply can be remotely operated, remote communication + must be enabled and the HV must be on. Please refer to the manual. + + Example usage: + + >>> import instruments as ik + >>> psu = ik.glassman.GlassmanFR.open_serial('/dev/ttyUSB0', 9600) + >>> psu.voltage = 100 # Sets output voltage to 100V. + >>> psu.voltage + array(100.0) * V + >>> psu.output = True # Turns on the power supply + >>> psu.voltage_sense < 200 * u.volt + True + + This code uses default values of `voltage_max`, `current_max` and + `polarity` that are only valid of the FR50R6 in its positive setting. + If your power supply differs, reset those values by calling: + + >>> import instruments.units as u + >>> psu.voltage_max = 40.0 * u.kilovolt + >>> psu.current_max = 7.5 * u.milliamp + >>> psu.polarity = -1 + """ + + def __init__(self, filelike): + """ + Initialize the instrument, and set the properties needed for communication. + """ + super().__init__(filelike) + self.terminator = "\r" + self.voltage_max = 50.0 * u.kilovolt + self.current_max = 6.0 * u.milliamp + self.polarity = +1 + self._device_timeout = False + self._voltage = 0.0 * u.volt + self._current = 0.0 * u.amp + + # ENUMS ## + + class Mode(Enum): + """ + Enum containing the possible modes of operations of the instrument + """ + + #: Constant voltage mode + voltage = "0" + #: Constant current mode + current = "1" + + class ResponseCode(Enum): + """ + Enum containing the possible reponse codes returned by the instrument. + """ + + #: A set command expects an acknowledge response (`A`) + S = "A" + #: A query command expects a response packet (`R`) + Q = "R" + #: A version query expects a different response packet (`B`) + V = "B" + #: A configure command expects an acknowledge response (`A`) + C = "A" + + class ErrorCode(Enum): + """ + Enum containing the possible error codes returned by the instrument. + """ + + #: Undefined command received (not S, Q, V or C) + undefined_command = "1" + #: The checksum calculated by the instrument does not correspond to the one received + checksum_error = "2" + #: The command was longer than expected + extra_bytes = "3" + #: The digital control byte set was not one of HV On, HV Off or Power supply Reset + illegal_control = "4" + #: A send command was sent without a reset byte while the power supply is faulted + illegal_while_fault = "5" + #: Command valid, error while executing it + processing_error = "6" + + # PROPERTIES ## + + @property + def channel(self): + """ + Return the channel (which in this case is the entire instrument, since + there is only 1 channel on the GlassmanFR.) + + :rtype: 'tuple' of length 1 containing a reference back to the parent + GlassmanFR object. + """ + return [self] + + @property + def voltage(self): + """ + Gets/sets the output voltage setting. + + :units: As specified, or assumed to be :math:`\\text{V}` otherwise. + :type: `float` or `~pint.Quantity` + """ + return self.polarity * self._voltage + + @voltage.setter + def voltage(self, newval): + self.set_status(voltage=assume_units(newval, u.volt)) + + @property + def current(self): + """ + Gets/sets the output current setting. + + :units: As specified, or assumed to be :math:`\\text{A}` otherwise. + :type: `float` or `~pint.Quantity` + """ + return self.polarity * self._current + + @current.setter + def current(self, newval): + self.set_status(current=assume_units(newval, u.amp)) + + @property + def voltage_sense(self): + """ + Gets the output voltage as measured by the sense wires. + + :units: As specified, or assumed to be :math:`\\text{V}` otherwise. + :type: `~pint.Quantity` + """ + return self.get_status()["voltage"] + + @property + def current_sense(self): + """ + Gets/sets the output current as measured by the sense wires. + + :units: As specified, or assumed to be :math:`\\text{A}` otherwise. + :type: `~pint.Quantity` + """ + return self.get_status()["current"] + + @property + def mode(self): + """ + Gets/sets the mode for the specified channel. + + The constant-voltage/constant-current modes of the power supply + are selected automatically depending on the load (resistance) + connected to the power supply. If the load greater than the set + V/I is connected, a voltage V is applied and the current flowing + is lower than I. If the load is smaller than V/I, the set current + I acts as a current limiter and the voltage is lower than V. + + :type: `GlassmanFR.Mode` + """ + return self.get_status()["mode"] + + @property + def output(self): + """ + Gets/sets the output status. + + This is a toggle setting. True will turn on the instrument output + while False will turn it off. + + :type: `bool` + """ + return self.get_status()["output"] + + @output.setter + def output(self, newval): + if not isinstance(newval, bool): + raise TypeError("Output status mode must be a boolean.") + self.set_status(output=newval) + + @property + def fault(self): + """ + Gets the output status. + + Returns True if the instrument has a fault. + + :type: `bool` + """ + return self.get_status()["fault"] + + @property + def version(self): + """ + The software revision level of the power supply's + data intereface via the `V` command + + :rtype: `str` + """ + return self.query("V") + + @property + def device_timeout(self): + """ + Gets/sets the timeout instrument side. + + This is a toggle setting. ON will set the timeout to 1.5 + seconds while OFF will disable it. + + :type: `bool` + """ + return self._device_timeout + + @device_timeout.setter + def device_timeout(self, newval): + if not isinstance(newval, bool): + raise TypeError("Device timeout mode must be a boolean.") + self.query(f"C{int(not newval)}") # Device acknowledges + self._device_timeout = newval + + # METHODS ## + + def sendcmd(self, cmd): + """ + Overrides the default `setcmd` by padding the front of each + command sent to the instrument with an SOH character and the + back of it with a checksum. + + :param str cmd: The command message to send to the instrument + """ + checksum = self._get_checksum(cmd) + self._file.sendcmd("\x01" + cmd + checksum) # Add SOH and checksum + + def query(self, cmd, size=-1): + """ + Overrides the default `query` by padding the front of each + command sent to the instrument with an SOH character and the + back of it with a checksum. + + This implementation also automatically check that the checksum + returned by the instrument is consistent with the message. If + the message returned is an error, it parses it and raises. + + :param str cmd: The query message to send to the instrument + :param int size: The number of bytes to read back from the instrument + response. + :return: The instrument response to the query + :rtype: `str` + """ + self.sendcmd(cmd) + result = self._file.read(size) + if result[0] != getattr(self.ResponseCode, cmd[0]).value and result[0] != "E": + raise ValueError(f"Invalid response code: {result}") + if result[0] == "A": + return "Acknowledged" + if not self._verify_checksum(result): + raise ValueError(f"Invalid checksum: {result}") + if result[0] == "E": + error_name = self.ErrorCode(result[1]).name + raise ValueError(f"Instrument responded with error: {error_name}") + + return result[1:-2] # Remove SOH and checksum + + def reset(self): + """ + Reset device to default status (HV Off, V=0.0, A=0.0) + """ + self.set_status(reset=True) + + def set_status(self, voltage=None, current=None, output=None, reset=False): + """ + Sets the requested variables on the instrument. + + This instrument can only set all of its variables simultaneously, + if some of them are omitted in this function, they will simply be + kept as what they were set to previously. + """ + if reset: + self._voltage = 0.0 * u.volt + self._current = 0.0 * u.amp + cmd = format(4, "013d") + else: + # The maximum value is encoded as the maximum of three hex characters (4095) + cmd = "" + value_max = int(0xFFF) + + # If the voltage is not specified, keep it as is + voltage = ( + assume_units(voltage, u.volt) if voltage is not None else self.voltage + ) + ratio = float(voltage.to(u.volt) / self.voltage_max.to(u.volt)) + voltage_int = int(round(value_max * ratio)) + self._voltage = self.voltage_max * float(voltage_int) / value_max + assert 0.0 * u.volt <= self._voltage <= self.voltage_max + cmd += format(voltage_int, "03X") + + # If the current is not specified, keep it as is + current = ( + assume_units(current, u.amp) if current is not None else self.current + ) + ratio = float(current.to(u.amp) / self.current_max.to(u.amp)) + current_int = int(round(value_max * ratio)) + self._current = self.current_max * float(current_int) / value_max + assert 0.0 * u.amp <= self._current <= self.current_max + cmd += format(current_int, "03X") + + # If the output status is not specified, keep it as is + output = output if output is not None else self.output + control = f"00{int(output)}{int(not output)}" + cmd += format(int(control, 2), "07X") + + self.query("S" + cmd) # Device acknowledges + + def get_status(self): + """ + Gets and parses the response packet. + + Returns a `dict` with the following keys: + ``{voltage,current,mode,fault,output}`` + + :rtype: `dict` + """ + return self._parse_response(self.query("Q")) + + def _parse_response(self, response): + """ + Parse the response packet returned by the power supply. + + Returns a `dict` with the following keys: + ``{voltage,current,mode,fault,output}`` + + :param response: Byte string to be unpacked and parsed + :type: `str` + + :rtype: `dict` + """ + (voltage, current, monitors) = unpack("@3s3s3x1c2x", bytes(response, "utf-8")) + + try: + voltage = self._parse_voltage(voltage) + current = self._parse_current(current) + mode, fault, output = self._parse_monitors(monitors) + except: + raise RuntimeError("Cannot parse response " "packet: {}".format(response)) + + return { + "voltage": voltage, + "current": current, + "mode": mode, + "fault": fault, + "output": output, + } + + def _parse_voltage(self, word): + """ + Converts the three-bytes voltage word returned in the + response packet to a single voltage quantity. + + :param word: Byte string to be parsed + :type: `bytes` + + :rtype: `~pint.Quantity` + """ + value = int(word.decode("utf-8"), 16) + value_max = int(0x3FF) + return self.polarity * self.voltage_max * float(value) / value_max + + def _parse_current(self, word): + """ + Converts the three-bytes current word returned in the + response packet to a single current quantity. + + :param word: Byte string to be parsed + :type: `bytes` + + :rtype: `~pint.Quantity` + """ + value = int(word.decode("utf-8"), 16) + value_max = int(0x3FF) + return self.polarity * self.current_max * float(value) / value_max + + def _parse_monitors(self, word): + """ + Converts the monitors byte returned in the response packet + to a mode, a fault boolean and an output boolean. + + :param word: Byte to be parsed + :type: `byte` + + :rtype: `str, bool, bool` + """ + bits = format(int(word, 16), "04b") + mode = self.Mode(bits[-1]) + fault = bits[-2] == "1" + output = bits[-3] == "1" + return mode, fault, output + + def _verify_checksum(self, word): + """ + Calculates the modulo 256 checksum of a string of characters + and compares it to the one returned by the instrument. + + Returns True if they agree, False otherwise. + + :param word: Byte string to be checked + :type: `str` + + :rtype: `bool` + """ + data = word[1:-2] + inst_checksum = word[-2:] + calc_checksum = self._get_checksum(data) + return inst_checksum == calc_checksum + + @staticmethod + def _get_checksum(data): + """ + Calculates the modulo 256 checksum of a string of characters. + This checksum, expressed in hexadecimal, is used in every + communication of this instrument, as a sanity check. + + Returns a string corresponding to the hexademical value + of the checksum, without the `0x` prefix. + + :param data: Byte string to be checksummed + :type: `str` + + :rtype: `str` + """ + chrs = list(data) + total = 0 + for c in chrs: + total += ord(c) + + return format(total % 256, "02X") diff --git a/instruments/holzworth/__init__.py b/instruments/holzworth/__init__.py index 9b4fd1d3d..9b7f77528 100644 --- a/instruments/holzworth/__init__.py +++ b/instruments/holzworth/__init__.py @@ -1,9 +1,7 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing Holzworth instruments """ -from __future__ import absolute_import from .holzworth_hs9000 import HS9000 diff --git a/instruments/holzworth/holzworth_hs9000.py b/instruments/holzworth/holzworth_hs9000.py index 35608b3af..99cf0e0bd 100644 --- a/instruments/holzworth/holzworth_hs9000.py +++ b/instruments/holzworth/holzworth_hs9000.py @@ -1,24 +1,20 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides support for the Holzworth HS9000 """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division -import quantities as pq +from instruments.units import ureg as u -from instruments.abstract_instruments.signal_generator import ( - SignalGenerator, - SGChannel -) +from instruments.abstract_instruments.signal_generator import SignalGenerator, SGChannel from instruments.util_fns import ( - ProxyList, split_unit_str, bounded_unitful_property, bool_property + ProxyList, + split_unit_str, + bounded_unitful_property, + bool_property, ) -from instruments.units import dBm # CLASSES ##################################################################### @@ -52,8 +48,7 @@ def __init__(self, hs, idx_chan): # Some channel names, like "REF", are special and are preserved # as strs. self._ch_name = ( - idx_chan if isinstance(idx_chan, str) - else "CH{}".format(idx_chan + 1) + idx_chan if isinstance(idx_chan, str) else f"CH{idx_chan + 1}" ) # PRIVATE METHODS # @@ -66,7 +61,7 @@ def sendcmd(self, cmd): :param str cmd: Command that will be sent to the instrument after being prefixed with the channel identifier """ - self._hs.sendcmd(":{ch}:{cmd}".format(ch=self._ch_name, cmd=cmd)) + self._hs.sendcmd(f":{self._ch_name}:{cmd}") def query(self, cmd): """ @@ -78,7 +73,7 @@ def query(self, cmd): :return: The result from the query :rtype: `str` """ - return self._hs.query(":{ch}:{cmd}".format(ch=self._ch_name, cmd=cmd)) + return self._hs.query(f":{self._ch_name}:{cmd}") # STATE METHODS # @@ -123,15 +118,15 @@ def temperature(self): Gets the current temperature of the specified channel. :units: As specified by the instrument. - :rtype: `~quantities.quantity.Quantity` + :rtype: `~pint.Quantity` """ val, units = split_unit_str(self.query("TEMP?")) - units = "deg{}".format(units) - return pq.Quantity(val, units) + units = f"deg{units}" + return u.Quantity(val, units) frequency, frequency_min, frequency_max = bounded_unitful_property( "FREQ", - units=pq.GHz, + units=u.GHz, doc=""" Gets/sets the frequency of the specified channel. When setting, values are bounded between what is returned by `frequency_min` @@ -144,13 +139,13 @@ def temperature(self): >>> print(hs.channel[0].frequency_min) >>> print(hs.channel[0].frequency_max) - :type: `~quantities.quantity.Quantity` + :type: `~pint.Quantity` :units: As specified or assumed to be of units GHz - """ + """, ) power, power_min, power_max = bounded_unitful_property( "PWR", - units=dBm, + units=u.dBm, doc=""" Gets/sets the output power of the specified channel. When setting, values are bounded between what is returned by `power_min` @@ -163,13 +158,13 @@ def temperature(self): >>> print(hs.channel[0].power_min) >>> print(hs.channel[0].power_max) - :type: `~quantities.quantity.Quantity` + :type: `~pint.Quantity` :units: `instruments.units.dBm` - """ + """, ) phase, phase_min, phase_max = bounded_unitful_property( "PHASE", - units=pq.degree, + units=u.degree, doc=""" Gets/sets the output phase of the specified channel. When setting, values are bounded between what is returned by `phase_min` @@ -182,9 +177,9 @@ def temperature(self): >>> print(hs.channel[0].phase_min) >>> print(hs.channel[0].phase_max) - :type: `~quantities.quantity.Quantity` + :type: `~pint.Quantity` :units: As specified or assumed to be of units degrees - """ + """, ) output = bool_property( @@ -204,7 +199,7 @@ def temperature(self): >>> hs.channel[0].output = True :type: `bool` - """ + """, ) # PROXY LIST ## @@ -224,8 +219,8 @@ def _channel_idxs(self): return [ ( int(ch_name.replace("CH", "")) - 1 - if ch_name.startswith('CH') else - ch_name.strip() + if ch_name.startswith("CH") + else ch_name.strip() ) for ch_name in self.query(":ATTACH?").split(":") if ch_name diff --git a/instruments/hp/__init__.py b/instruments/hp/__init__.py index f8864cea6..e92efbdb2 100644 --- a/instruments/hp/__init__.py +++ b/instruments/hp/__init__.py @@ -1,12 +1,11 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing HP instruments """ -from __future__ import absolute_import from .hp3456a import HP3456a from .hp6624a import HP6624a from .hp6632b import HP6632b from .hp6652a import HP6652a +from .hpe3631a import HPe3631a diff --git a/instruments/hp/hp3456a.py b/instruments/hp/hp3456a.py index 27ea43153..1548cb5a1 100644 --- a/instruments/hp/hp3456a.py +++ b/instruments/hp/hp3456a.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # # hp3456a.py: Driver for the HP3456a Digital Voltmeter. # @@ -32,16 +31,11 @@ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division import time -from builtins import range - from enum import Enum, IntEnum -import quantities as pq - from instruments.abstract_instruments import Multimeter +from instruments.units import ureg as u from instruments.util_fns import assume_units, bool_property, enum_property # CLASSES ##################################################################### @@ -66,8 +60,8 @@ def __init__(self, filelike): Initialise the instrument, and set the required eos, eoi needed for communication. """ - super(HP3456a, self).__init__(filelike) - self.timeout = 15 * pq.second + super().__init__(filelike) + self.timeout = 15 * u.second self.terminator = "\r" self.sendcmd("HO0T4SO1") self._null = False @@ -79,6 +73,7 @@ class MathMode(IntEnum): """ Enum with the supported math modes """ + off = 0 pass_fail = 1 statistic = 2 @@ -95,6 +90,7 @@ class Mode(Enum): """ Enum containing the supported mode codes """ + #: DC voltage dcv = "S0F1" #: AC voltage @@ -121,6 +117,7 @@ class Register(Enum): """ Enum with the register names for all `HP3456a` internal registers. """ + number_of_readings = "N" number_of_digits = "G" nplc = "I" @@ -139,6 +136,7 @@ class TriggerMode(IntEnum): """ Enum with valid trigger modes. """ + internal = 1 external = 2 single = 3 @@ -151,6 +149,7 @@ class ValidRange(Enum): powerline cycles to integrate over. """ + voltage = (1e-1, 1e0, 1e1, 1e2, 1e3) resistance = (1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9) nplc = (1e-1, 1e0, 1e1, 1e2) @@ -165,7 +164,8 @@ class ValidRange(Enum): :type: `HP3456a.Mode` """, writeonly=True, - set_fmt="{}{}") + set_fmt="{}{}", + ) autozero = bool_property( "Z", @@ -184,7 +184,8 @@ class ValidRange(Enum): suitable for high impendance measurements since no input switching is done.""", writeonly=True, - set_fmt="{}{}") + set_fmt="{}{}", + ) filter = bool_property( "FL", @@ -199,7 +200,8 @@ class ValidRange(Enum): ac converter and input amplifier. In these modes select the filter for measurements below 400Hz.""", writeonly=True, - set_fmt="{}{}") + set_fmt="{}{}", + ) math_mode = enum_property( "M", @@ -214,7 +216,8 @@ class ValidRange(Enum): :type: `HP3456a.MathMode` """, writeonly=True, - set_fmt="{}{}") + set_fmt="{}{}", + ) trigger_mode = enum_property( "T", @@ -228,7 +231,8 @@ class ValidRange(Enum): """, writeonly=True, - set_fmt="{}{}") + set_fmt="{}{}", + ) @property def number_of_readings(self): @@ -261,8 +265,9 @@ def number_of_digits(self): def number_of_digits(self, newval): newval = int(newval) if newval not in range(3, 7): - raise ValueError("Valid number_of_digits are: " - "{}".format(list(range(3, 7)))) + raise ValueError( + "Valid number_of_digits are: " "{}".format(list(range(3, 7))) + ) self._register_write(HP3456a.Register.number_of_digits, newval) @@ -286,8 +291,7 @@ def nplc(self, newval): if newval in valid: self._register_write(HP3456a.Register.nplc, newval) else: - raise ValueError("Valid nplc settings are: " - "{}".format(valid)) + raise ValueError("Valid nplc settings are: " "{}".format(valid)) @property def delay(self): @@ -298,11 +302,11 @@ def delay(self): :rtype: `~quantaties.Quantity.s` """ - return self._register_read(HP3456a.Register.delay) * pq.s + return self._register_read(HP3456a.Register.delay) * u.s @delay.setter def delay(self, value): - delay = assume_units(value, pq.s).rescale(pq.s).magnitude + delay = assume_units(value, u.s).to(u.s).magnitude self._register_write(HP3456a.Register.delay, delay) @property @@ -417,11 +421,11 @@ def z(self, value): def input_range(self): """Set the input range to be used. - The `HP3456a` has separate ranges for `~quantities.ohm` and for - `~quantities.volt`. The range value sent to the instrument depends on + The `HP3456a` has separate ranges for `ohm` and for + `volt`. The range value sent to the instrument depends on the unit set on the input range value. `auto` selects auto ranging. - :type: `~quantities.Quantity` + :type: `~pint.Quantity` """ raise NotImplementedError @@ -431,29 +435,35 @@ def input_range(self, value): if value.lower() == "auto": self.sendcmd("R1W") else: - raise ValueError("Only 'auto' is acceptable when specifying " - "the input range as a string.") + raise ValueError( + "Only 'auto' is acceptable when specifying " + "the input range as a string." + ) - elif isinstance(value, pq.quantity.Quantity): - if value.units == pq.volt: + elif isinstance(value, u.Quantity): + if value.units == u.volt: valid = HP3456a.ValidRange.voltage.value - value = value.rescale(pq.volt) - elif value.units == pq.ohm: + value = value.to(u.volt) + elif value.units == u.ohm: valid = HP3456a.ValidRange.resistance.value - value = value.rescale(pq.ohm) + value = value.to(u.ohm) else: - raise ValueError("Value {} not quantity.volt or quantity.ohm" - "".format(value)) + raise ValueError( + "Value {} not quantity.volt or quantity.ohm" "".format(value) + ) - value = float(value) + value = float(value.magnitude) if value not in valid: - raise ValueError("Value {} outside valid ranges " - "{}".format(value, valid)) + raise ValueError( + "Value {} outside valid ranges " "{}".format(value, valid) + ) value = valid.index(value) + 2 - self.sendcmd("R{}W".format(value)) + self.sendcmd(f"R{value}W") else: - raise TypeError("Range setting must be specified as a float, int, " - "or the string 'auto', got {}".format(type(value))) + raise TypeError( + "Range setting must be specified as a float, int, " + "or the string 'auto', got {}".format(type(value)) + ) @property def relative(self): @@ -468,13 +478,15 @@ def relative(self): def relative(self, value): if value is True: self._null = True - self.sendcmd("M{}".format(HP3456a.MathMode.null.value)) + self.sendcmd(f"M{HP3456a.MathMode.null.value}") elif value is False: self._null = False - self.sendcmd("M{}".format(HP3456a.MathMode.off.value)) + self.sendcmd(f"M{HP3456a.MathMode.off.value}") else: - raise TypeError("Relative setting must be specified as a bool, " - "got {}".format(type(value))) + raise TypeError( + "Relative setting must be specified as a bool, " + "got {}".format(type(value)) + ) # METHODS ## @@ -513,7 +525,7 @@ def fetch(self, mode=None): :type mode: `HP3456a.Mode` :return: A series of measurements from the multimeter. - :rtype: `~quantities.quantity.Quantity` + :rtype: `~pint.Quantity` """ if mode is not None: units = UNITS[mode] @@ -547,7 +559,7 @@ def measure(self, mode=None): :type mode: `HP3456a.Mode` :return: A measurement from the multimeter. - :rtype: `~quantities.quantity.Quantity` + :rtype: `~pint.Quantity` """ if mode is not None: @@ -557,7 +569,7 @@ def measure(self, mode=None): modevalue = "" units = 1 - self.sendcmd("{}W1STNT3".format(modevalue)) + self.sendcmd(f"{modevalue}W1STNT3") value = self.query("", size=-1) return float(value) * units @@ -575,12 +587,13 @@ def _register_read(self, name): except KeyError: pass if not isinstance(name, HP3456a.Register): - raise TypeError("register must be specified as a " - "HP3456a.Register, got {} " - "instead.".format(name)) - self.sendcmd("RE{}".format(name.value)) - if not self._testing: # pragma: no cover - time.sleep(.1) + raise TypeError( + "register must be specified as a " + "HP3456a.Register, got {} " + "instead.".format(name) + ) + self.sendcmd(f"RE{name.value}") + time.sleep(0.1) return float(self.query("", size=-1)) def _register_write(self, name, value): @@ -596,18 +609,19 @@ def _register_write(self, name, value): except KeyError: pass if not isinstance(name, HP3456a.Register): - raise TypeError("register must be specified as a " - "HP3456a.Register, got {} " - "instead.".format(name)) + raise TypeError( + "register must be specified as a " + "HP3456a.Register, got {} " + "instead.".format(name) + ) if name in [ - HP3456a.Register.mean, - HP3456a.Register.variance, - HP3456a.Register.count + HP3456a.Register.mean, + HP3456a.Register.variance, + HP3456a.Register.count, ]: - raise ValueError("register {} is read only".format(name)) - self.sendcmd("W{}ST{}".format(value, name.value)) - if not self._testing: # pragma: no cover - time.sleep(.1) + raise ValueError(f"register {name} is read only") + self.sendcmd(f"W{value}ST{name.value}") + time.sleep(0.1) def trigger(self): """ @@ -615,18 +629,19 @@ def trigger(self): """ self.sendcmd("T3") + # UNITS ####################################################################### UNITS = { None: 1, - HP3456a.Mode.dcv: pq.volt, - HP3456a.Mode.acv: pq.volt, - HP3456a.Mode.acvdcv: pq.volt, - HP3456a.Mode.resistance_2wire: pq.ohm, - HP3456a.Mode.resistance_4wire: pq.ohm, + HP3456a.Mode.dcv: u.volt, + HP3456a.Mode.acv: u.volt, + HP3456a.Mode.acvdcv: u.volt, + HP3456a.Mode.resistance_2wire: u.ohm, + HP3456a.Mode.resistance_4wire: u.ohm, HP3456a.Mode.ratio_dcv_dcv: 1, HP3456a.Mode.ratio_acv_dcv: 1, HP3456a.Mode.ratio_acvdcv_dcv: 1, - HP3456a.Mode.oc_resistence_2wire: pq.ohm, - HP3456a.Mode.oc_resistence_4wire: pq.ohm, + HP3456a.Mode.oc_resistence_2wire: u.ohm, + HP3456a.Mode.oc_resistence_4wire: u.ohm, } diff --git a/instruments/hp/hp6624a.py b/instruments/hp/hp6624a.py index d2c16bfd6..6762ade5d 100644 --- a/instruments/hp/hp6624a.py +++ b/instruments/hp/hp6624a.py @@ -1,23 +1,14 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides support for the HP6624a power supply """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division - -from builtins import range from enum import Enum -import quantities as pq - -from instruments.abstract_instruments import ( - PowerSupply, - PowerSupplyChannel -) +from instruments.abstract_instruments import PowerSupply +from instruments.units import ureg as u from instruments.util_fns import ProxyList, unitful_property, bool_property # CLASSES ##################################################################### @@ -41,12 +32,12 @@ class HP6624a(PowerSupply): """ def __init__(self, filelike): - super(HP6624a, self).__init__(filelike) + super().__init__(filelike) self._channel_count = 4 # INNER CLASSES # - class Channel(PowerSupplyChannel): + class Channel(PowerSupply.Channel): """ Class representing a power output channel on the HP6624a. @@ -63,12 +54,10 @@ def __init__(self, hp, idx): def _format_cmd(self, cmd): cmd = cmd.split(" ") if len(cmd) == 1: - cmd = "{cmd} {idx}".format(cmd=cmd[0], idx=self._idx) + cmd = f"{cmd[0]} {self._idx}" else: cmd = "{cmd} {idx},{value}".format( - cmd=cmd[0], - idx=self._idx, - value=cmd[1] + cmd=cmd[0], idx=self._idx, value=cmd[1] ) return cmd @@ -111,7 +100,7 @@ def mode(self, newval): voltage = unitful_property( "VSET", - pq.volt, + u.volt, set_fmt="{} {:.1f}", output_decoration=float, doc=""" @@ -121,13 +110,13 @@ def mode(self, newval): Note there is no bounds checking on the value specified. :units: As specified, or assumed to be :math:`\\text{V}` otherwise. - :type: `float` or `~quantities.quantity.Quantity` - """ + :type: `float` or `~pint.Quantity` + """, ) current = unitful_property( "ISET", - pq.amp, + u.amp, set_fmt="{} {:.1f}", output_decoration=float, doc=""" @@ -137,39 +126,39 @@ def mode(self, newval): Note there is no bounds checking on the value specified. :units: As specified, or assumed to be :math:`\\text{A}` otherwise. - :type: `float` or `~quantities.quantity.Quantity` - """ + :type: `float` or `~pint.Quantity` + """, ) voltage_sense = unitful_property( "VOUT", - pq.volt, + u.volt, readonly=True, doc=""" Gets the actual voltage as measured by the sense wires for the specified channel. :units: :math:`\\text{V}` (volts) - :rtype: `~quantities.quantity.Quantity` - """ + :rtype: `~pint.Quantity` + """, ) current_sense = unitful_property( "IOUT", - pq.amp, + u.amp, readonly=True, doc=""" Gets the actual output current as measured by the instrument for the specified channel. :units: :math:`\\text{A}` (amps) - :rtype: `~quantities.quantity.Quantity` - """ + :rtype: `~pint.Quantity` + """, ) overvoltage = unitful_property( "OVSET", - pq.volt, + u.volt, set_fmt="{} {:.1f}", output_decoration=float, doc=""" @@ -178,8 +167,8 @@ def mode(self, newval): Note there is no bounds checking on the value specified. :units: As specified, or assumed to be :math:`\\text{V}` otherwise. - :type: `float` or `~quantities.quantity.Quantity` - """ + :type: `float` or `~pint.Quantity` + """, ) overcurrent = bool_property( @@ -192,7 +181,7 @@ def mode(self, newval): This is a toggle setting. It is either on or off. :type: `bool` - """ + """, ) output = bool_property( @@ -206,7 +195,7 @@ def mode(self, newval): while False will turn it off. :type: `bool` - """ + """, ) # METHODS ## @@ -215,8 +204,8 @@ def reset(self): """ Reset overvoltage and overcurrent errors to resume operation. """ - self.sendcmd('OVRST') - self.sendcmd('OCRST') + self.sendcmd("OVRST") + self.sendcmd("OCRST") # ENUMS # @@ -228,6 +217,7 @@ class Mode(Enum): constant-voltage output, so this class current does not do anything and is just a placeholder. """ + voltage = 0 current = 0 @@ -251,21 +241,21 @@ def voltage(self): """ Gets/sets the voltage for all four channels. - :units: As specified (if a `~quantities.Quantity`) or assumed to be + :units: As specified (if a `~pint.Quantity`) or assumed to be of units Volts. - :type: `list` of `~quantities.quantity.Quantity` with units Volt + :type: `tuple`[`~pint.Quantity`, ...] with units Volt """ - return [ - self.channel[i].voltage for i in range(self.channel_count) - ] + return tuple(self.channel[i].voltage for i in range(self.channel_count)) @voltage.setter def voltage(self, newval): if isinstance(newval, (list, tuple)): if len(newval) is not self.channel_count: - raise ValueError('When specifying the voltage for all channels ' - 'as a list or tuple, it must be of ' - 'length {}.'.format(self.channel_count)) + raise ValueError( + "When specifying the voltage for all channels " + "as a list or tuple, it must be of " + "length {}.".format(self.channel_count) + ) for i in range(self.channel_count): self.channel[i].voltage = newval[i] else: @@ -277,21 +267,21 @@ def current(self): """ Gets/sets the current for all four channels. - :units: As specified (if a `~quantities.Quantity`) or assumed to be + :units: As specified (if a `~pint.Quantity`) or assumed to be of units Amps. - :type: `list` of `~quantities.quantity.Quantity` with units Amp + :type: `tuple`[`~pint.Quantity`, ...] with units Amp """ - return [ - self.channel[i].current for i in range(self.channel_count) - ] + return tuple(self.channel[i].current for i in range(self.channel_count)) @current.setter def current(self, newval): if isinstance(newval, (list, tuple)): if len(newval) is not self.channel_count: - raise ValueError('When specifying the current for all channels ' - 'as a list or tuple, it must be of ' - 'length {}.'.format(self.channel_count)) + raise ValueError( + "When specifying the current for all channels " + "as a list or tuple, it must be of " + "length {}.".format(self.channel_count) + ) for i in range(self.channel_count): self.channel[i].current = newval[i] else: @@ -304,11 +294,9 @@ def voltage_sense(self): Gets the actual voltage as measured by the sense wires for all channels. :units: :math:`\\text{V}` (volts) - :rtype: `tuple` of `~quantities.quantity.Quantity` + :rtype: `tuple`[`~pint.Quantity`, ...] """ - return ( - self.channel[i].voltage_sense for i in range(self.channel_count) - ) + return tuple(self.channel[i].voltage_sense for i in range(self.channel_count)) @property def current_sense(self): @@ -316,11 +304,9 @@ def current_sense(self): Gets the actual current as measured by the instrument for all channels. :units: :math:`\\text{A}` (amps) - :rtype: `tuple` of `~quantities.quantity.Quantity` + :rtype: `tuple`[`~pint.Quantity`, ...] """ - return ( - self.channel[i].current_sense for i in range(self.channel_count) - ) + return tuple(self.channel[i].current_sense for i in range(self.channel_count)) @property def channel_count(self): @@ -335,9 +321,9 @@ def channel_count(self): @channel_count.setter def channel_count(self, newval): if not isinstance(newval, int): - raise TypeError('Channel count must be specified as an integer.') + raise TypeError("Channel count must be specified as an integer.") if newval < 1: - raise ValueError('Channel count must be >=1') + raise ValueError("Channel count must be >=1") self._channel_count = newval # METHODS ## @@ -353,4 +339,4 @@ def clear(self): #) The power supply remains addressed to listen. #) The PON bit in the serial poll register is cleared. """ - self.sendcmd('CLR') + self.sendcmd("CLR") diff --git a/instruments/hp/hp6632b.py b/instruments/hp/hp6632b.py index 635ca53cd..9d13aac6d 100644 --- a/instruments/hp/hp6632b.py +++ b/instruments/hp/hp6632b.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # # hp6632b.py: Python class for the HP6632b power supply # @@ -32,17 +31,18 @@ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division -from builtins import range - from enum import Enum, IntEnum -import quantities as pq from instruments.generic_scpi.scpi_instrument import SCPIInstrument from instruments.hp.hp6652a import HP6652a -from instruments.util_fns import (unitful_property, unitless_property, - bool_property, enum_property, int_property) +from instruments.units import ureg as u +from instruments.util_fns import ( + unitful_property, + unitless_property, + bool_property, + enum_property, + int_property, +) # CLASSES ##################################################################### @@ -77,15 +77,13 @@ class HP6632b(SCPIInstrument, HP6652a): array(10.0) * V """ - def __init__(self, filelike): - super(HP6632b, self).__init__(filelike) - # ENUMS ## class ALCBandwidth(IntEnum): """ Enum containing valid ALC bandwidth modes for the hp6632b """ + normal = 1.5e4 fast = 6e4 @@ -93,24 +91,27 @@ class DigitalFunction(Enum): """ Enum containing valid digital function modes for the hp6632b """ - remote_inhibit = 'RIDF' - data = 'DIG' + + remote_inhibit = "RIDF" + data = "DIG" class DFISource(Enum): """ Enum containing valid DFI sources for the hp6632b """ - questionable = 'QUES' - operation = 'OPER' - event_status_bit = 'ESB' - request_service_bit = 'RQS' - off = 'OFF' + + questionable = "QUES" + operation = "OPER" + event_status_bit = "ESB" + request_service_bit = "RQS" + off = "OFF" class ErrorCodes(IntEnum): """ Enum containing generic-SCPI error codes along with codes specific to the HP6632b. """ + no_error = 0 # -100 BLOCK: COMMAND ERRORS ## @@ -233,16 +234,18 @@ class RemoteInhibit(Enum): """ Enum containing vlaid remote inhibit modes for the hp6632b. """ - latching = 'LATC' - live = 'LIVE' - off = 'OFF' + + latching = "LATC" + live = "LIVE" + off = "OFF" class SenseWindow(Enum): """ Enum containing valid sense window modes for the hp6632b. """ - hanning = 'HANN' - rectangular = 'RECT' + + hanning = "HANN" + rectangular = "RECT" # PROPERTIES ## @@ -257,33 +260,33 @@ class SenseWindow(Enum): denotes that it is, and `Fast` denotes that it is not. :type: `~HP6632b.ALCBandwidth` - """ + """, ) voltage_trigger = unitful_property( "VOLT:TRIG", - pq.volt, + u.volt, doc=""" Gets/sets the pending triggered output voltage. Note there is no bounds checking on the value specified. :units: As specified, or assumed to be :math:`\\text{V}` otherwise. - :type: `float` or `~quantities.Quantity` - """ + :type: `float` or `~pint.Quantity` + """, ) current_trigger = unitful_property( "CURR:TRIG", - pq.amp, + u.amp, doc=""" Gets/sets the pending triggered output current. Note there is no bounds checking on the value specified. :units: As specified, or assumed to be :math:`\\text{A}` otherwise. - :type: `float` or `~quantities.Quantity` - """ + :type: `float` or `~pint.Quantity` + """, ) init_output_continuous = bool_property( @@ -297,12 +300,12 @@ class SenseWindow(Enum): levels. :type: `bool` - """ + """, ) current_sense_range = unitful_property( - 'SENS:CURR:RANGE', - pq.ampere, + "SENS:CURR:RANGE", + u.ampere, doc=""" Get/set the sense current range by the current max value. @@ -311,14 +314,14 @@ class SenseWindow(Enum): range increases the low current measurement sensitivity and accuracy. :units: As specified, or assumed to be :math:`\\text{A}` otherwise. - :type: `float` or `~quantities.quantity.Quantity` - """ + :type: `float` or `~pint.Quantity` + """, ) output_dfi = bool_property( - 'OUTP:DFI', - '1', - '0', + "OUTP:DFI", + "1", + "0", doc=""" Get/set the discrete fault indicator (DFI) output from the dc source. The DFI is an open-collector logic signal connected to the read @@ -326,7 +329,7 @@ class SenseWindow(Enum): a fault is detected. :type: `bool` - """ + """, ) output_dfi_source = enum_property( @@ -336,7 +339,7 @@ class SenseWindow(Enum): Get/set the source for discrete fault indicator (DFI) events. :type: `~HP6632b.DFISource` - """ + """, ) output_remote_inhibit = enum_property( @@ -348,7 +351,7 @@ class SenseWindow(Enum): connection, which allows an external device to signal a fault. :type: `~HP6632b.RemoteInhibit` - """ + """, ) digital_function = enum_property( @@ -358,7 +361,7 @@ class SenseWindow(Enum): Get/set the inhibit+fault port to digital in+out or vice-versa. :type: `~HP6632b.DigitalFunction` - """ + """, ) digital_data = int_property( @@ -368,7 +371,7 @@ class SenseWindow(Enum): Get/set digital in+out port to data. Data can be an integer from 0-7. :type: `int` - """ + """, ) sense_sweep_points = unitless_property( @@ -377,19 +380,19 @@ class SenseWindow(Enum): Get/set the number of points in a measurement sweep. :type: `int` - """ + """, ) sense_sweep_interval = unitful_property( "SENS:SWE:TINT", - pq.second, + u.second, doc=""" Get/set the digitizer sample spacing. Can be set from 15.6 us to 31200 seconds, the interval will be rounded to the nearest 15.6 us increment. :units: As specified, or assumed to be :math:`\\text{s}` otherwise. - :type: `float` or `~quantities.Quantity` - """ + :type: `float` or `~pint.Quantity` + """, ) sense_window = enum_property( @@ -399,12 +402,12 @@ class SenseWindow(Enum): Get/set the measurement window function. :type: `~HP6632b.SenseWindow` - """ + """, ) output_protection_delay = unitful_property( "OUTP:PROT:DEL", - pq.second, + u.second, doc=""" Get/set the time between programming of an output change that produces a constant current condition and the recording of that condigition in @@ -412,8 +415,8 @@ class SenseWindow(Enum): current protection, but not overvoltage protection. :units: As specified, or assumed to be :math:`\\text{s}` otherwise. - :type: `float` or `~quantities.Quantity` - """ + :type: `float` or `~pint.Quantity` + """, ) # FUNCTIONS ## @@ -423,13 +426,13 @@ def init_output_trigger(self): Set the output trigger system to the initiated state. In this state, the power supply will respond to the next output trigger command. """ - self.sendcmd('INIT:NAME TRAN') + self.sendcmd("INIT:NAME TRAN") def abort_output_trigger(self): """ Set the output trigger system to the idle state. """ - self.sendcmd('ABORT') + self.sendcmd("ABORT") # SCPIInstrument commands that need local overrides @@ -466,11 +469,14 @@ def check_error_queue(self): done = False result = [] while not done: - err = int(self.query('SYST:ERR?').split(',')[0]) + err = int(self.query("SYST:ERR?").split(",")[0]) if err == self.ErrorCodes.no_error: done = True else: result.append( - self.ErrorCodes(err) if err in self.ErrorCodes else err) + self.ErrorCodes(err) + if any(err == item.value for item in self.ErrorCodes) + else err + ) return result diff --git a/instruments/hp/hp6652a.py b/instruments/hp/hp6652a.py index 38e6bac40..31bbae8e8 100644 --- a/instruments/hp/hp6652a.py +++ b/instruments/hp/hp6652a.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Driver for the HP6652a single output power supply @@ -8,25 +7,23 @@ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division -import quantities as pq +from instruments.units import ureg as u -from instruments.abstract_instruments import (PowerSupply, PowerSupplyChannel) +from instruments.abstract_instruments import PowerSupply from instruments.util_fns import unitful_property, bool_property # CLASSES ##################################################################### -class HP6652a(PowerSupply, PowerSupplyChannel): +class HP6652a(PowerSupply, PowerSupply.Channel): """ The HP6652a is a single output power supply. Because it is a single channel output, this object inherits from both - PowerSupply and PowerSupplyChannel. + PowerSupply and PowerSupply.Channel. According to the manual, this class MIGHT be usable for any HP power supply with a model number HP66XYA, where X is in {4,5,7,8,9} and Y is a digit(?). @@ -55,9 +52,6 @@ class HP6652a(PowerSupply, PowerSupplyChannel): >>> psu.display_textmode=False """ - def __init__(self, filelike): - super(HP6652a, self).__init__(filelike) - # ENUMS ## # I don't know of any possible enumerations supported @@ -67,65 +61,65 @@ def __init__(self, filelike): voltage = unitful_property( "VOLT", - pq.volt, + u.volt, doc=""" Gets/sets the output voltage. Note there is no bounds checking on the value specified. :units: As specified, or assumed to be :math:`\\text{V}` otherwise. - :type: `float` or `~quantities.Quantity` - """ + :type: `float` or `~pint.Quantity` + """, ) current = unitful_property( "CURR", - pq.amp, + u.amp, doc=""" Gets/sets the output current. Note there is no bounds checking on the value specified. :units: As specified, or assumed to be :math:`\\text{A}` otherwise. - :type: `float` or `~quantities.Quantity` - """ + :type: `float` or `~pint.Quantity` + """, ) voltage_sense = unitful_property( "MEAS:VOLT", - pq.volt, + u.volt, readonly=True, doc=""" Gets the actual output voltage as measured by the sense wires. :units: :math:`\\text{V}` (volts) - :rtype: `~quantities.Quantity` - """ + :rtype: `~pint.Quantity` + """, ) current_sense = unitful_property( "MEAS:CURR", - pq.amp, + u.amp, readonly=True, doc=""" Gets the actual output current as measured by the sense wires. :units: :math:`\\text{A}` (amps) - :rtype: `~quantities.Quantity` - """ + :rtype: `~pint.Quantity` + """, ) overvoltage = unitful_property( "VOLT:PROT", - pq.volt, + u.volt, doc=""" Gets/sets the overvoltage protection setting in volts. Note there is no bounds checking on the value specified. :units: As specified, or assumed to be :math:`\\text{V}` otherwise. - :type: `float` or `~quantities.Quantity` - """ + :type: `float` or `~pint.Quantity` + """, ) overcurrent = bool_property( @@ -138,7 +132,7 @@ def __init__(self, filelike): This is a toggle setting. It is either on or off. :type: `bool` - """ + """, ) output = bool_property( @@ -152,7 +146,7 @@ def __init__(self, filelike): while False will turn it off. :type: `bool` - """ + """, ) display_textmode = bool_property( @@ -169,7 +163,7 @@ def __init__(self, filelike): .. seealso:: `~HP6652a.display_text()` :type: `bool` - """ + """, ) @property @@ -181,8 +175,8 @@ def name(self): :rtype: `str` """ idn_string = self.query("*IDN?") - idn_list = idn_string.split(',') - return ' '.join(idn_list[:2]) + idn_list = idn_string.split(",") + return " ".join(idn_list[:2]) @property def mode(self): @@ -204,7 +198,7 @@ def reset(self): """ Reset overvoltage and overcurrent errors to resume operation. """ - self.sendcmd('OUTP:PROT:CLE') + self.sendcmd("OUTP:PROT:CLE") def display_text(self, text_to_display): """ @@ -240,7 +234,7 @@ def display_text(self, text_to_display): text_to_display = text_to_display[:15] text_to_display = text_to_display.upper() - self.sendcmd('DISP:TEXT "{}"'.format(text_to_display)) + self.sendcmd(f'DISP:TEXT "{text_to_display}"') return text_to_display @@ -253,4 +247,4 @@ def channel(self): :rtype: 'tuple' of length 1 containing a reference back to the parent HP6652a object. """ - return self, + return (self,) diff --git a/instruments/hp/hpe3631a.py b/instruments/hp/hpe3631a.py new file mode 100644 index 000000000..de1dc3186 --- /dev/null +++ b/instruments/hp/hpe3631a.py @@ -0,0 +1,276 @@ +#!/usr/bin/env python +# +# hpe3631a.py: Driver for the HP E3631A Power Supply +# +# © 2019 Francois Drielsma (francois.drielsma@gmail.com). +# +# This file is a part of the InstrumentKit project. +# Licensed under the AGPL version 3. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +""" +Driver for the HP E3631A Power Supply + +Originally contributed and copyright held by Francois Drielsma +(francois.drielsma@gmail.com) + +An unrestricted license has been provided to the maintainers of the Instrument +Kit project. +""" + +# IMPORTS ##################################################################### + +import time + +from instruments.units import ureg as u + +from instruments.abstract_instruments import PowerSupply +from instruments.generic_scpi import SCPIInstrument +from instruments.util_fns import ( + int_property, + unitful_property, + bounded_unitful_property, + bool_property, + split_unit_str, + assume_units, +) + +# CLASSES ##################################################################### + + +class HPe3631a(PowerSupply, PowerSupply.Channel, SCPIInstrument): + + """ + The HPe3631a is a three channels voltage/current supply. + - Channel 1 is a positive +6V/5A channel (P6V) + - Channel 2 is a positive +25V/1A channel (P25V) + - Channel 3 is a negative -25V/1A channel (N25V) + + This module is designed for the power supply to be set to + a specific channel and remain set afterwards as this device + does not offer commands to set or read multiple channels + without calling the channel set command each time (0.5s). It is + possible to call a specific channel through psu.channel[idx], + which will automatically reset the channel id, when necessary. + + This module is likely to work as is for the Agilent E3631 and + Keysight E3631 which seem to be rebranded but identical devices. + + Example usage: + + >>> import instruments as ik + >>> psu = ik.hp.HPe3631a.open_gpibusb("/dev/ttyUSB0", 10) + >>> psu.channelid = 2 # Sets channel to P25V + >>> psu.voltage = 12.5 # Sets voltage to 12.5V + >>> psu.voltage # Reads back set voltage + array(12.5) * V + >>> psu.voltage_sense # Reads back sensed voltage + array(12.501) * V + """ + + def __init__(self, filelike): + super().__init__(filelike) + self.sendcmd("SYST:REM") # Puts the device in remote operation + time.sleep(0.1) + + # INNER CLASSES # + + class Channel: + """ + Class representing a power output channel on the HPe3631a. + + .. warning:: This class should NOT be manually created by the user. It is + designed to be initialized by the `HPe3631a` class. + """ + + def __init__(self, parent, valid_set): + self._parent = parent + self._valid_set = valid_set + + def __getitem__(self, idx): + # Check that the channel is available. If it is, set the + # channelid of the device and return the device object. + if self._parent.channelid != idx: + self._parent.channelid = idx + time.sleep(0.5) + return self._parent + + def __len__(self): + return len(self._valid_set) + + # PROPERTIES ## + + @property + def channel(self): + """ + Gets a specific channel object. The desired channel is specified like + one would access a list. + + :rtype: `HPe3631a.Channel` + + .. seealso:: + `HPe3631a` for example using this property. + """ + return self.Channel(self, [1, 2, 3]) + + @property + def mode(self): + """ + Gets/sets the mode for the specified channel. + + The constant-voltage/constant-current modes of the power supply + are selected automatically depending on the load (resistance) + connected to the power supply. If the load greater than the set + V/I is connected, a voltage V is applied and the current flowing + is lower than I. If the load is smaller than V/I, the set current + I acts as a current limiter and the voltage is lower than V. + """ + raise AttributeError("The `HPe3631a` sets its mode automatically") + + channelid = int_property( + "INST:NSEL", + valid_set=[1, 2, 3], + doc=""" + Gets/Sets the active channel ID. + + :type: `HPe3631a.ChannelType` + """, + ) + + @property + def voltage(self): + """ + Gets/sets the output voltage of the source. + + :units: As specified, or assumed to be :math:`\\text{V}` otherwise. + :type: `float` or `~pint.Quantity` + """ + raw = self.query("SOUR:VOLT?") + return u.Quantity(*split_unit_str(raw, u.volt)).to(u.volt) + + @voltage.setter + def voltage(self, newval): + """ + Gets/sets the output voltage of the source. + + :units: As specified, or assumed to be :math:`\\text{V}` otherwise. + :type: `float` or `~pint.Quantity` + """ + min_value, max_value = self.voltage_range + if newval < min_value: + raise ValueError( + "Voltage quantity is too low. Got {}, minimum " + "value is {}".format(newval, min_value) + ) + + if newval > max_value: + raise ValueError( + "Voltage quantity is too high. Got {}, maximum " + "value is {}".format(newval, max_value) + ) + + # Rescale to the correct unit before printing. This will also + # catch bad units. + strval = f"{assume_units(newval, u.volt).to(u.volt).magnitude:e}" + self.sendcmd(f"SOUR:VOLT {strval}") + + @property + def voltage_min(self): + """ + Gets the minimum voltage for the current channel. + + :units: :math:`\\text{V}`. + :type: `~pint.Quantity` + """ + return self.voltage_range[0] + + @property + def voltage_max(self): + """ + Gets the maximum voltage for the current channel. + + :units: :math:`\\text{V}`. + :type: `~pint.Quantity` + """ + return self.voltage_range[1] + + @property + def voltage_range(self): + """ + Gets the voltage range for the current channel. + + The MAX function SCPI command is designed in such a way + on this device that it always returns the largest absolute value. + There is no need to query MIN, as it is always 0., but one has to + order the values as MAX can be negative. + + :units: :math:`\\text{V}`. + :type: array of `~pint.Quantity` + """ + value = u.Quantity(*split_unit_str(self.query("SOUR:VOLT? MAX"), u.volt)) + if value < 0.0: + return value, 0.0 + return 0.0, value + + current, current_min, current_max = bounded_unitful_property( + "SOUR:CURR", + u.amp, + min_fmt_str="{}? MIN", + max_fmt_str="{}? MAX", + doc=""" + Gets/sets the output current of the source. + + :units: As specified, or assumed to be :math:`\\text{A}` otherwise. + :type: `float` or `~pint.Quantity` + """, + ) + + voltage_sense = unitful_property( + "MEAS:VOLT", + u.volt, + readonly=True, + doc=""" + Gets the actual output voltage as measured by the sense wires. + + :units: As specified, or assumed to be :math:`\\text{V}` otherwise. + :type: `~pint.Quantity` + """, + ) + + current_sense = unitful_property( + "MEAS:CURR", + u.amp, + readonly=True, + doc=""" + Gets the actual output current as measured by the sense wires. + + :units: As specified, or assumed to be :math:`\\text{A}` otherwise. + :type: `~pint.Quantity` + """, + ) + + output = bool_property( + "OUTP", + inst_true="1", + inst_false="0", + doc=""" + Gets/sets the outputting status of the specified channel. + + This is a toggle setting. ON will turn on the channel output + while OFF will turn it off. + + :type: `bool` + """, + ) diff --git a/instruments/keithley/__init__.py b/instruments/keithley/__init__.py index 0c8c34e4f..56f8ef371 100644 --- a/instruments/keithley/__init__.py +++ b/instruments/keithley/__init__.py @@ -1,12 +1,11 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing Keithley instruments """ -from __future__ import absolute_import from .keithley195 import Keithley195 +from .keithley485 import Keithley485 from .keithley580 import Keithley580 from .keithley2182 import Keithley2182 from .keithley6220 import Keithley6220 diff --git a/instruments/keithley/keithley195.py b/instruments/keithley/keithley195.py index 5bf00b06f..0c79931dc 100644 --- a/instruments/keithley/keithley195.py +++ b/instruments/keithley/keithley195.py @@ -1,21 +1,17 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Driver for the Keithley 195 digital multimeter """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division import time import struct from enum import Enum, IntEnum -import quantities as pq - from instruments.abstract_instruments import Multimeter +from instruments.units import ureg as u # CLASSES ##################################################################### @@ -29,7 +25,7 @@ class Keithley195(Multimeter): Example usage: >>> import instruments as ik - >>> import quantities as pq + >>> import instruments.units as u >>> dmm = ik.keithley.Keithley195.open_gpibusb('/dev/ttyUSB0', 12) >>> print dmm.measure(dmm.Mode.resistance) @@ -37,9 +33,9 @@ class Keithley195(Multimeter): """ def __init__(self, filelike): - super(Keithley195, self).__init__(filelike) - self.sendcmd('YX') # Removes the termination CRLF - self.sendcmd('G1DX') # Disable returning prefix and suffix + super().__init__(filelike) + self.sendcmd("YX") # Removes the termination CRLF + self.sendcmd("G1DX") # Disable returning prefix and suffix # ENUMS ## @@ -47,6 +43,7 @@ class Mode(IntEnum): """ Enum containing valid measurement modes for the Keithley 195 """ + voltage_dc = 0 voltage_ac = 1 resistance = 2 @@ -57,6 +54,7 @@ class TriggerMode(IntEnum): """ Enum containing valid trigger modes for the Keithley 195 """ + talk_continuous = 0 talk_one_shot = 1 get_continuous = 2 @@ -70,6 +68,7 @@ class ValidRange(Enum): """ Enum containing valid range settings for the Keithley 195 """ + voltage_dc = (20e-3, 200e-3, 2, 20, 200, 1000) voltage_ac = (20e-3, 200e-3, 2, 20, 200, 700) current_dc = (20e-6, 200e-6, 2e-3, 20e-3, 200e-3, 2) @@ -94,16 +93,18 @@ def mode(self): :type: `Keithley195.Mode` """ - return self.parse_status_word(self.get_status_word())['mode'] + return self.parse_status_word(self.get_status_word())["mode"] @mode.setter def mode(self, newval): if isinstance(newval, str): newval = self.Mode[newval] if not isinstance(newval, Keithley195.Mode): - raise TypeError("Mode must be specified as a Keithley195.Mode " - "value, got {} instead.".format(newval)) - self.sendcmd('F{}DX'.format(newval.value)) + raise TypeError( + "Mode must be specified as a Keithley195.Mode " + "value, got {} instead.".format(newval) + ) + self.sendcmd(f"F{newval.value}DX") @property def trigger_mode(self): @@ -130,17 +131,19 @@ def trigger_mode(self): :type: `Keithley195.TriggerMode` """ - return self.parse_status_word(self.get_status_word())['trigger'] + return self.parse_status_word(self.get_status_word())["trigger"] @trigger_mode.setter def trigger_mode(self, newval): if isinstance(newval, str): newval = Keithley195.TriggerMode[newval] if not isinstance(newval, Keithley195.TriggerMode): - raise TypeError('Drive must be specified as a ' - 'Keithley195.TriggerMode, got {} ' - 'instead.'.format(newval)) - self.sendcmd('T{}X'.format(newval.value)) + raise TypeError( + "Drive must be specified as a " + "Keithley195.TriggerMode, got {} " + "instead.".format(newval) + ) + self.sendcmd(f"T{newval.value}X") @property def relative(self): @@ -162,13 +165,13 @@ def relative(self): :type: `bool` """ - return self.parse_status_word(self.get_status_word())['relative'] + return self.parse_status_word(self.get_status_word())["relative"] @relative.setter def relative(self, newval): if not isinstance(newval, bool): - raise TypeError('Relative mode must be a boolean.') - self.sendcmd('Z{}DX'.format(int(newval))) + raise TypeError("Relative mode must be a boolean.") + self.sendcmd(f"Z{int(newval)}DX") @property def input_range(self): @@ -190,28 +193,30 @@ def input_range(self): All modes will also accept the string ``auto`` which will set the 195 into auto ranging mode. - :rtype: `~quantities.quantity.Quantity` or `str` + :rtype: `~pint.Quantity` or `str` """ - index = self.parse_status_word(self.get_status_word())['range'] + index = self.parse_status_word(self.get_status_word())["range"] if index == 0: - return 'auto' - else: - mode = self.mode - value = Keithley195.ValidRange[mode.name].value[index - 1] - units = UNITS2[mode] - return value * units + return "auto" + + mode = self.mode + value = Keithley195.ValidRange[mode.name].value[index - 1] + units = UNITS2[mode] + return value * units @input_range.setter def input_range(self, newval): if isinstance(newval, str): - if newval.lower() == 'auto': - self.sendcmd('R0DX') + if newval.lower() == "auto": + self.sendcmd("R0DX") return else: - raise ValueError('Only "auto" is acceptable when specifying ' - 'the input range as a string.') - if isinstance(newval, pq.quantity.Quantity): - newval = float(newval) + raise ValueError( + 'Only "auto" is acceptable when specifying ' + "the input range as a string." + ) + if isinstance(newval, u.Quantity): + newval = float(newval.magnitude) mode = self.mode valid = Keithley195.ValidRange[mode.name].value @@ -219,12 +224,15 @@ def input_range(self, newval): if newval in valid: newval = valid.index(newval) + 1 else: - raise ValueError('Valid range settings for mode {} ' - 'are: {}'.format(mode, valid)) + raise ValueError( + "Valid range settings for mode {} " "are: {}".format(mode, valid) + ) else: - raise TypeError('Range setting must be specified as a float, int, ' - 'or the string "auto", got {}'.format(type(newval))) - self.sendcmd('R{}DX'.format(newval)) + raise TypeError( + "Range setting must be specified as a float, int, " + 'or the string "auto", got {}'.format(type(newval)) + ) + self.sendcmd(f"R{newval}DX") # METHODS # @@ -246,7 +254,7 @@ def measure(self, mode=None): Example usage: >>> import instruments as ik - >>> import quantities as pq + >>> import instruments.units as u >>> dmm = ik.keithley.Keithley195.open_gpibusb('/dev/ttyUSB0', 12) >>> print(dmm.measure(dmm.Mode.resistance)) @@ -255,17 +263,16 @@ def measure(self, mode=None): :type mode: `Keithley195.Mode` :return: A measurement from the multimeter. - :rtype: `~quantities.quantity.Quantity` + :rtype: `~pint.Quantity` """ if mode is not None: current_mode = self.mode if mode != current_mode: self.mode = mode - if not self._testing: - time.sleep(2) # Gives the instrument a moment to settle + time.sleep(2) # Gives the instrument a moment to settle else: mode = self.mode - value = self.query('') + value = self.query("") return float(value) * UNITS2[mode] def get_status_word(self): @@ -279,7 +286,8 @@ def get_status_word(self): :return: String containing setting information of the instrument :rtype: `str` """ - return self.query('U0DX') + self.sendcmd("U0DX") + return self._file.read_raw() @staticmethod def parse_status_word(statusword): # pylint: disable=too-many-locals @@ -297,29 +305,47 @@ def parse_status_word(statusword): # pylint: disable=too-many-locals :return: A parsed version of the status word as a Python dictionary :rtype: `dict` """ - if statusword[:3] != '195': - raise ValueError('Status word starts with wrong prefix, expected ' - '195, got {}'.format(statusword)) - - (trigger, function, input_range, eoi, buf, rate, srqmode, relative, - delay, multiplex, selftest, data_fmt, data_ctrl, filter_mode, - terminator) = struct.unpack('@4c2s3c2s5c2s', statusword[4:]) - - return {'trigger': Keithley195.TriggerMode(int(trigger)), - 'mode': Keithley195.Mode(int(function)), - 'range': int(input_range), - 'eoi': (eoi == '1'), - 'buffer': buf, - 'rate': rate, - 'srqmode': srqmode, - 'relative': (relative == '1'), - 'delay': delay, - 'multiplex': (multiplex == '1'), - 'selftest': selftest, - 'dataformat': data_fmt, - 'datacontrol': data_ctrl, - 'filter': filter_mode, - 'terminator': terminator} + if statusword[:3] != b"195": + raise ValueError( + "Status word starts with wrong prefix, expected " + "195, got {}".format(statusword) + ) + + ( + trigger, + function, + input_range, + eoi, + buf, + rate, + srqmode, + relative, + delay, + multiplex, + selftest, + data_fmt, + data_ctrl, + filter_mode, + terminator, + ) = struct.unpack("@4c2s3c2s5c2s", statusword[4:]) + + return { + "trigger": Keithley195.TriggerMode(int(trigger)), + "mode": Keithley195.Mode(int(function)), + "range": int(input_range), + "eoi": (eoi == b"1"), + "buffer": buf, + "rate": rate, + "srqmode": srqmode, + "relative": (relative == b"1"), + "delay": delay, + "multiplex": (multiplex == b"1"), + "selftest": selftest, + "dataformat": data_fmt, + "datacontrol": data_ctrl, + "filter": filter_mode, + "terminator": terminator, + } def trigger(self): """ @@ -328,7 +354,7 @@ def trigger(self): Do note that this is different from the standard SCPI ``*TRG`` command (which is not supported by the 195 anyways). """ - self.sendcmd('X') + self.sendcmd("X") def auto_range(self): """ @@ -336,22 +362,23 @@ def auto_range(self): This is the same as calling ``Keithley195.input_range = 'auto'`` """ - self.input_range = 'auto' + self.input_range = "auto" + # UNITS ####################################################################### UNITS = { - 'DCV': pq.volt, - 'ACV': pq.volt, - 'ACA': pq.amp, - 'DCA': pq.amp, - 'OHM': pq.ohm, + "DCV": u.volt, + "ACV": u.volt, + "ACA": u.amp, + "DCA": u.amp, + "OHM": u.ohm, } UNITS2 = { - Keithley195.Mode.voltage_dc: pq.volt, - Keithley195.Mode.voltage_ac: pq.volt, - Keithley195.Mode.current_dc: pq.amp, - Keithley195.Mode.current_ac: pq.amp, - Keithley195.Mode.resistance: pq.ohm, + Keithley195.Mode.voltage_dc: u.volt, + Keithley195.Mode.voltage_ac: u.volt, + Keithley195.Mode.current_dc: u.amp, + Keithley195.Mode.current_ac: u.amp, + Keithley195.Mode.resistance: u.ohm, } diff --git a/instruments/keithley/keithley2182.py b/instruments/keithley/keithley2182.py index 6a4873b06..3436df564 100644 --- a/instruments/keithley/keithley2182.py +++ b/instruments/keithley/keithley2182.py @@ -1,20 +1,16 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Driver for the Keithley 2182 nano-voltmeter """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division -from builtins import range, map - from enum import Enum -import quantities as pq -from instruments.generic_scpi import SCPIMultimeter from instruments.abstract_instruments import Multimeter +from instruments.generic_scpi import SCPIMultimeter +from instruments.optional_dep_finder import numpy +from instruments.units import ureg as u from instruments.util_fns import ProxyList # CLASSES ##################################################################### @@ -30,14 +26,11 @@ class Keithley2182(SCPIMultimeter): >>> import instruments as ik >>> meter = ik.keithley.Keithley2182.open_gpibusb("/dev/ttyUSB0", 10) - >>> print meter.measure(meter.Mode.voltage_dc) + >>> print(meter.measure(meter.Mode.voltage_dc)) """ - def __init__(self, filelike): - super(Keithley2182, self).__init__(filelike) - # INNER CLASSES # class Channel(Multimeter): @@ -57,7 +50,7 @@ def __init__(self, parent, idx): @property def mode(self): - return Keithley2182.Mode(self._parent.query('SENS:FUNC?')) + return Keithley2182.Mode(self._parent.query("SENS:FUNC?")) @mode.setter def mode(self, newval): @@ -97,15 +90,15 @@ def measure(self, mode=None): :param mode: Mode that the measurement will be performed in :type mode: Keithley2182.Mode :return: The value of the measurement - :rtype: `~quantities.quantity.Quantity` + :rtype: `~pint.Quantity` """ if mode is not None: # self.mode = mode raise NotImplementedError - self._parent.sendcmd('SENS:CHAN {}'.format(self._idx)) - value = float(self._parent.query('SENS:DATA:FRES?')) + self._parent.sendcmd(f"SENS:CHAN {self._idx}") + value = float(self._parent.query("SENS:DATA:FRES?")) unit = self._parent.units - return value * unit + return u.Quantity(value, unit) # ENUMS # @@ -113,6 +106,7 @@ class Mode(Enum): """ Enum containing valid measurement modes for the Keithley 2182 """ + voltage_dc = "VOLT" temperature = "TEMP" @@ -120,11 +114,12 @@ class TriggerMode(Enum): """ Enum containing valid trigger modes for the Keithley 2182 """ - immediate = 'IMM' - external = 'EXT' - bus = 'BUS' - timer = 'TIM' - manual = 'MAN' + + immediate = "IMM" + external = "EXT" + bus = "BUS" + timer = "TIM" + manual = "MAN" # PROPERTIES # @@ -162,7 +157,7 @@ def relative(self): :type: `bool` """ mode = self.channel[0].mode - return self.query("SENS:{}:CHAN1:REF:STAT?".format(mode.value)) == "ON" + return self.query(f"SENS:{mode.value}:CHAN1:REF:STAT?") == "ON" @relative.setter def relative(self, newval): @@ -170,11 +165,10 @@ def relative(self, newval): raise TypeError("Relative mode must be a boolean.") mode = self.channel[0].mode if self.relative: - self.sendcmd("SENS:{}:CHAN1:REF:ACQ".format(mode.value)) + self.sendcmd(f"SENS:{mode.value}:CHAN1:REF:ACQ") else: - newval = ("ON" if newval is True else "OFF") - self.sendcmd( - "SENS:{}:CHAN1:REF:STAT {}".format(mode.value, newval)) + newval = "ON" if newval is True else "OFF" + self.sendcmd(f"SENS:{mode.value}:CHAN1:REF:STAT {newval}") @property def input_range(self): @@ -189,18 +183,18 @@ def units(self): """ Gets the current measurement units of the instrument. - :rtype: `~quantities.unitquantity.UnitQuantity` + :rtype: `~pint.Unit` """ mode = self.channel[0].mode if mode == Keithley2182.Mode.voltage_dc: - return pq.volt + return u.volt unit = self.query("UNIT:TEMP?") if unit == "C": - unit = pq.celsius + unit = u.celsius elif unit == "K": - unit = pq.kelvin + unit = u.kelvin elif unit == "F": - unit = pq.fahrenheit + unit = u.fahrenheit else: raise ValueError("Unknown temperature units.") return unit @@ -219,9 +213,14 @@ def fetch(self): recommended to transfer a large number of data points using GPIB. :return: Measurement readings from the instrument output buffer. - :rtype: `list` of `~quantities.quantity.Quantity` elements + :rtype: `tuple`[`~pint.Quantity`, ...] + or if numpy is installed, `~pint.Quantity` with `numpy.array` data """ - return list(map(float, self.query("FETC?").split(","))) * self.units + data = list(map(float, self.query("FETC?").split(","))) + unit = self.units + if numpy: + return data * unit + return tuple(d * unit for d in data) def measure(self, mode=None): """ @@ -232,14 +231,16 @@ def measure(self, mode=None): :type: `Keithley2182.Mode` :return: Returns a single shot measurement of the specified mode. - :rtype: `~quantities.quantity.Quantity` + :rtype: `~pint.Quantity` :units: Volts, Celsius, Kelvin, or Fahrenheit """ if mode is None: mode = self.channel[0].mode if not isinstance(mode, Keithley2182.Mode): - raise TypeError("Mode must be specified as a Keithley2182.Mode " - "value, got {} instead.".format(mode)) - value = float(self.query("MEAS:{}?".format(mode.value))) + raise TypeError( + "Mode must be specified as a Keithley2182.Mode " + "value, got {} instead.".format(mode) + ) + value = float(self.query(f"MEAS:{mode.value}?")) unit = self.units return value * unit diff --git a/instruments/keithley/keithley485.py b/instruments/keithley/keithley485.py new file mode 100644 index 000000000..38cb5ee99 --- /dev/null +++ b/instruments/keithley/keithley485.py @@ -0,0 +1,455 @@ +#!/usr/bin/env python +# +# keithley485.py: Driver for the Keithley 485 picoammeter. +# +# © 2019 Francois Drielsma (francois.drielsma@gmail.com). +# +# This file is a part of the InstrumentKit project. +# Licensed under the AGPL version 3. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +""" +Driver for the Keithley 485 picoammeter. + +Originally contributed and copyright held by Francois Drielsma +(francois.drielsma@gmail.com). + +An unrestricted license has been provided to the maintainers of the Instrument +Kit project. +""" + +# IMPORTS ##################################################################### + +from struct import unpack +from enum import Enum + +from instruments.abstract_instruments import Instrument +from instruments.units import ureg as u + +# CLASSES ##################################################################### + + +class Keithley485(Instrument): + + """ + The Keithley Model 485 is a 4 1/2 digit resolution autoranging + picoammeter with a +- 20000 count LCD. It is designed for low + current measurement requirements from 0.1pA to 2mA. + + The device needs some processing time (manual reports 300-500ms) after a + command has been transmitted. + + Example usage: + + >>> import instruments as ik + >>> inst = ik.keithley.Keithley485.open_gpibusb("/dev/ttyUSB0", 22) + >>> inst.measure() # Measures the current + array(-1.278e-10) * A + """ + + # ENUMS # + + class TriggerMode(Enum): + """ + Enum containing valid trigger modes for the Keithley 485 + """ + + #: Continuously measures current, returns on talk + continuous_ontalk = 0 + #: Measures current once and returns on talk + oneshot_ontalk = 1 + #: Continuously measures current, returns on `GET` + continuous_onget = 2 + #: Measures current once and returns on `GET` + oneshot_onget = 3 + #: Continuously measures current, returns on `X` + continuous_onx = 4 + #: Measures current once and returns on `X` + oneshot_onx = 5 + + class SRQDataMask(Enum): + """ + Enum containing valid SRQ data masks for the Keithley 485 + """ + + #: Service request (SRQ) disabled + srq_disabled = 0 + #: Read overflow + read_ovf = 1 + #: Read done + read_done = 8 + #: Read done or read overflow + read_done_ovf = 9 + #: Device busy + busy = 16 + #: Device busy or read overflow + busy_read_ovf = 17 + #: Device busy or read overflow + busy_read_done = 24 + #: Device busy, read done or read overflow + busy_read_done_ovf = 25 + + class SRQErrorMask(Enum): + """ + Enum containing valid SRQ error masks for the Keithley 485 + """ + + #: Service request (SRQ) disabled + srq_disabled = 0 + #: Illegal Device-Dependent Command Option (IDDCO) + idcco = 1 + #: Illegal Device-Dependent Command (IDDC) + idcc = 2 + #: IDDCO or IDDC + idcco_idcc = 3 + #: Device not in remote + not_remote = 4 + #: Device not in remote or IDDCO + not_remote_idcco = 5 + #: Device not in remote or IDDC + not_remote_idcc = 6 + #: Device not in remote, IDDCO or IDDC + not_remote_idcco_idcc = 7 + + class Status(Enum): + """ + Enum containing valid status keys in the measurement string + """ + + #: Measurement normal + normal = b"N" + #: Measurement zero-check + zerocheck = b"C" + #: Measurement overflow + overflow = b"O" + #: Measurement relative + relative = b"Z" + + # PROPERTIES # + + @property + def zero_check(self): + """ + Gets/sets the 'zero check' mode (C) of the Keithley 485. + + Once zero check is enabled (C1 sent), the display can be + zeroed with the REL feature or the front panel pot. + + See the Keithley 485 manual for more information. + + :type: `bool` + """ + return self.get_status()["zerocheck"] + + @zero_check.setter + def zero_check(self, newval): + if not isinstance(newval, bool): + raise TypeError("Zero Check mode must be a boolean.") + self.sendcmd(f"C{int(newval)}X") + + @property + def log(self): + """ + Gets/sets the 'log' mode (D) of the Keithley 485. + + Once log is enabled (D1 sent), the device will return + the logarithm of the current readings. + + See the Keithley 485 manual for more information. + + :type: `bool` + """ + return self.get_status()["log"] + + @log.setter + def log(self, newval): + if not isinstance(newval, bool): + raise TypeError("Log mode must be a boolean.") + self.sendcmd(f"D{int(newval)}X") + + @property + def input_range(self): + """ + Gets/sets the range (R) of the Keithley 485 input terminals. The valid + ranges are one of ``{auto|2e-9|2e-8|2e-7|2e-6|2e-5|2e-4|2e-3}`` + + :type: `~pint.Quantity` or `str` + """ + value = self.get_status()["range"] + if isinstance(value, str): + return value + return value * u.amp + + @input_range.setter + def input_range(self, newval): + valid = ("auto", 2e-9, 2e-8, 2e-7, 2e-6, 2e-5, 2e-4, 2e-3) + if isinstance(newval, str): + newval = newval.lower() + if newval == "auto": + self.sendcmd("R0X") + return + else: + raise ValueError( + "Only `auto` is acceptable when specifying " + "the range as a string." + ) + if isinstance(newval, u.Quantity): + newval = float(newval.magnitude) + + if isinstance(newval, (float, int)): + if newval in valid: + newval = valid.index(newval) + else: + raise ValueError(f"Valid range settings are: {valid}") + else: + raise TypeError( + "Range setting must be specified as a float, int, " + "or the string `auto`, got {}".format(type(newval)) + ) + self.sendcmd(f"R{newval}X") + + @property + def relative(self): + """ + Gets/sets the relative measurement mode (Z) of the Keithley 485. + + As stated in the manual: The relative function is used to establish a + baseline reading. This reading is subtracted from all subsequent + readings. The purpose of making relative measurements is to cancel test + lead and offset currents or to store an input as a reference level. + + Once a relative level is established, it remains in effect until another + relative level is set. The relative value is only good for the range the + value was taken on and higher ranges. If a lower range is selected than + that on which the relative was taken, inaccurate results may occur. + Relative cannot be activated when "OL" is displayed. + + See the manual for more information. + + :type: `bool` + """ + return self.get_status()["relative"] + + @relative.setter + def relative(self, newval): + if not isinstance(newval, bool): + raise TypeError("Relative mode must be a boolean.") + self.sendcmd(f"Z{int(newval)}X") + + @property + def eoi_mode(self): + """ + Gets/sets the 'eoi' mode (K) of the Keithley 485. + + The model 485 will normally send an end of interrupt (EOI) + during the last byte of its data string or status word. + The EOI reponse of the instrument may be included or omitted. + Warning: the default setting (K0) includes it. + + See the Keithley 485 manual for more information. + + :type: `bool` + """ + return self.get_status()["eoi_mode"] + + @eoi_mode.setter + def eoi_mode(self, newval): + if not isinstance(newval, bool): + raise TypeError("EOI mode must be a boolean.") + self.sendcmd(f"K{1 - int(newval)}X") + + @property + def trigger_mode(self): + """ + Gets/sets the trigger mode (T) of the Keithley 485. + + There are two different trigger settings for three different sources. + This means there are six different settings for the trigger mode. + + The two types are continuous and one-shot. Continuous has the instrument + continuously sample the current. One-shot performs a single + current measurement when requested to do so. + + The three trigger sources are on talk, on GET, and on "X". On talk + refers to addressing the instrument to talk over GPIB. On GET is when + the instrument receives the GPIB command byte for "group execute + trigger". Last, on "X" is when one sends the ASCII character "X" to the + instrument. + + It is recommended to leave it in the default mode (T0, continuous on talk), + and simply ignore the output when other commands are called. + + :type: `Keithley485.TriggerMode` + """ + return self.get_status()["trigger"] + + @trigger_mode.setter + def trigger_mode(self, newval): + if isinstance(newval, str): + newval = Keithley485.TriggerMode[newval] + if not isinstance(newval, Keithley485.TriggerMode): + raise TypeError( + "Drive must be specified as a " + "Keithley485.TriggerMode, got {} " + "instead.".format(newval) + ) + self.sendcmd(f"T{newval.value}X") + + # METHODS # + + def auto_range(self): + """ + Turn on auto range for the Keithley 485. + + This is the same as calling the `Keithley485.set_current_range` + method and setting the parameter to "AUTO". + """ + self.sendcmd("R0X") + + def get_status(self): + """ + Gets and parses the status word. + + Returns a `dict` with the following keys: + ``{zerocheck,log,range,relative,eoi,relative, + trigger,datamask,errormask,terminator}`` + + :rtype: `dict` + """ + return self._parse_status_word(self._get_status_word()) + + def _get_status_word(self): + """ + The device will not always respond with the statusword when asked. We + use a simple heuristic here: request it up to 5 times. + + :rtype: `str` + """ + tries = 5 + statusword = "" + while statusword[:3] != "485" and tries != 0: + statusword = self.query("U0X") + tries -= 1 + + if tries == 0: + raise OSError("Could not retrieve status word") + + return statusword[:-1] + + def _parse_status_word(self, statusword): + """ + Parse the status word returned by the function + `~Keithley485.get_status_word`. + + Returns a `dict` with the following keys: + ``{zerocheck,log,range,relative,eoi,relative, + trigger,datamask,errormask,terminator}`` + + :param statusword: Byte string to be unpacked and parsed + :type: `str` + + :rtype: `dict` + """ + if statusword[:3] != "485": + raise ValueError( + "Status word starts with wrong " "prefix: {}".format(statusword) + ) + + ( + zerocheck, + log, + device_range, + relative, + eoi_mode, + trigger, + datamask, + errormask, + ) = unpack("@6c2s2s", bytes(statusword[3:], "utf-8")) + + valid_range = { + b"0": "auto", + b"1": 2e-9, + b"2": 2e-8, + b"3": 2e-7, + b"4": 2e-6, + b"5": 2e-5, + b"6": 2e-4, + b"7": 2e-3, + } + + try: + device_range = valid_range[device_range] + trigger = self.TriggerMode(int(trigger)).name + datamask = self.SRQDataMask(int(datamask)).name + errormask = self.SRQErrorMask(int(errormask)).name + except: + raise RuntimeError("Cannot parse status " "word: {}".format(statusword)) + + return { + "zerocheck": zerocheck == b"1", + "log": log == b"1", + "range": device_range, + "relative": relative == b"1", + "eoi_mode": eoi_mode == b"0", + "trigger": trigger, + "datamask": datamask, + "errormask": errormask, + "terminator": self.terminator, + } + + def measure(self): + """ + Perform a current measurement with the Keithley 485. + + :rtype: `~pint.Quantity` + """ + return self._parse_measurement(self.query("X")) + + def _parse_measurement(self, measurement): + """ + Parse the measurement string returned by the instrument. + + Returns the current formatted as a Quantity. + + :param measurement: String to be unpacked and parsed + :type: `str` + + :rtype: `~pint.Quantity` + """ + (status, function, base, current) = unpack( + "@1c2s1c10s", bytes(measurement, "utf-8") + ) + + try: + status = self.Status(status) + except ValueError: + raise ValueError(f"Invalid status word in measurement: {status}") + + if status != self.Status.normal: + raise ValueError(f"Instrument not in normal mode: {status.name}") + + if function != b"DC": + raise ValueError(f"Instrument not returning DC function: {function}") + + try: + current = ( + float(current) * u.amp + if base == b"A" + else 10 ** (float(current)) * u.amp + ) + except: + raise Exception(f"Cannot parse measurement: {measurement}") + + return current diff --git a/instruments/keithley/keithley580.py b/instruments/keithley/keithley580.py index d8336ebbd..6d726c56a 100644 --- a/instruments/keithley/keithley580.py +++ b/instruments/keithley/keithley580.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # # keithley580.py: Driver for the Keithley 580 micro-ohmmeter. # @@ -33,14 +32,12 @@ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division import time import struct from enum import IntEnum -import quantities as pq +from instruments.units import ureg as u from instruments.abstract_instruments import Instrument @@ -62,8 +59,8 @@ def __init__(self, filelike): """ Initialise the instrument and remove CRLF line termination """ - super(Keithley580, self).__init__(filelike) - self.sendcmd('Y:X') # Removes the termination CRLF characters + super().__init__(filelike) + self.sendcmd("Y:X") # Removes the termination CRLF characters # ENUMS # @@ -71,6 +68,7 @@ class Polarity(IntEnum): """ Enum containing valid polarity modes for the Keithley 580 """ + positive = 0 negative = 1 @@ -78,6 +76,7 @@ class Drive(IntEnum): """ Enum containing valid drive modes for the Keithley 580 """ + pulsed = 0 dc = 1 @@ -85,6 +84,7 @@ class TriggerMode(IntEnum): """ Enum containing valid trigger modes for the Keithley 580 """ + talk_continuous = 0 talk_one_shot = 1 get_continuous = 2 @@ -107,25 +107,24 @@ def polarity(self): :type: `Keithley580.Polarity` """ - value = self.parse_status_word(self.get_status_word())['polarity'] - if value == '+': + value = self.parse_status_word(self.get_status_word())["polarity"] + if value == "+": return Keithley580.Polarity.positive - elif value == '-': - return Keithley580.Polarity.negative else: - raise ValueError('Not a valid polarity returned from ' - 'instrument, got {}'.format(value)) + return Keithley580.Polarity.negative @polarity.setter def polarity(self, newval): if isinstance(newval, str): newval = Keithley580.Polarity[newval] if not isinstance(newval, Keithley580.Polarity): - raise TypeError('Polarity must be specified as a ' - 'Keithley580.Polarity, got {} ' - 'instead.'.format(newval)) + raise TypeError( + "Polarity must be specified as a " + "Keithley580.Polarity, got {} " + "instead.".format(newval) + ) - self.sendcmd('P{}X'.format(newval.value)) + self.sendcmd(f"P{newval.value}X") @property def drive(self): @@ -140,7 +139,7 @@ def drive(self): :type: `Keithley580.Drive` """ - value = self.parse_status_word(self.get_status_word())['drive'] + value = self.parse_status_word(self.get_status_word())["drive"] return Keithley580.Drive[value] @drive.setter @@ -148,11 +147,13 @@ def drive(self, newval): if isinstance(newval, str): newval = Keithley580.Drive[newval] if not isinstance(newval, Keithley580.Drive): - raise TypeError('Drive must be specified as a ' - 'Keithley580.Drive, got {} ' - 'instead.'.format(newval)) + raise TypeError( + "Drive must be specified as a " + "Keithley580.Drive, got {} " + "instead.".format(newval) + ) - self.sendcmd('D{}X'.format(newval.value)) + self.sendcmd(f"D{newval.value}X") @property def dry_circuit_test(self): @@ -169,13 +170,13 @@ def dry_circuit_test(self): :type: `bool` """ - return self.parse_status_word(self.get_status_word())['drycircuit'] + return self.parse_status_word(self.get_status_word())["drycircuit"] @dry_circuit_test.setter def dry_circuit_test(self, newval): if not isinstance(newval, bool): - raise TypeError('DryCircuitTest mode must be a boolean.') - self.sendcmd('C{}X'.format(int(newval))) + raise TypeError("DryCircuitTest mode must be a boolean.") + self.sendcmd(f"C{int(newval)}X") @property def operate(self): @@ -186,13 +187,13 @@ def operate(self): :type: `bool` """ - return self.parse_status_word(self.get_status_word())['operate'] + return self.parse_status_word(self.get_status_word())["operate"] @operate.setter def operate(self, newval): if not isinstance(newval, bool): - raise TypeError('Operate mode must be a boolean.') - self.sendcmd('O{}X'.format(int(newval))) + raise TypeError("Operate mode must be a boolean.") + self.sendcmd(f"O{int(newval)}X") @property def relative(self): @@ -214,13 +215,13 @@ def relative(self): :type: `bool` """ - return self.parse_status_word(self.get_status_word())['relative'] + return self.parse_status_word(self.get_status_word())["relative"] @relative.setter def relative(self, newval): if not isinstance(newval, bool): - raise TypeError('Relative mode must be a boolean.') - self.sendcmd('Z{}X'.format(int(newval))) + raise TypeError("Relative mode must be a boolean.") + self.sendcmd(f"Z{int(newval)}X") @property def trigger_mode(self): @@ -250,11 +251,13 @@ def trigger_mode(self): def trigger_mode(self, newval): if isinstance(newval, str): newval = Keithley580.TriggerMode[newval] - if newval not in Keithley580.TriggerMode: - raise TypeError('Drive must be specified as a ' - 'Keithley580.TriggerMode, got {} ' - 'instead.'.format(newval)) - self.sendcmd('T{}X'.format(newval.value)) + if not isinstance(newval, Keithley580.TriggerMode): + raise TypeError( + "Drive must be specified as a " + "Keithley580.TriggerMode, got {} " + "instead.".format(newval) + ) + self.sendcmd(f"T{newval.value}X") @property def input_range(self): @@ -262,34 +265,41 @@ def input_range(self): Gets/sets the range of the Keithley 580 input terminals. The valid ranges are one of ``{AUTO|2e-1|2|20|200|2000|2e4|2e5}`` - :type: `~quantities.quantity.Quantity` or `str` + :type: `~pint.Quantity` or `str` """ - value = float(self.parse_status_word(self.get_status_word())['range']) - return value * pq.ohm + value = self.parse_status_word(self.get_status_word())["range"] + if isinstance(value, str): # if range is 'auto' + return value + else: + return float(value) * u.ohm @input_range.setter def input_range(self, newval): - valid = ('auto', 2e-1, 2e0, 2e1, 2e2, 2e3, 2e4, 2e5) + valid = ("auto", 2e-1, 2e0, 2e1, 2e2, 2e3, 2e4, 2e5) if isinstance(newval, str): newval = newval.lower() - if newval == 'auto': - self.sendcmd('R0X') + if newval == "auto": + self.sendcmd("R0X") return else: - raise ValueError('Only "auto" is acceptable when specifying ' - 'the input range as a string.') - if isinstance(newval, pq.quantity.Quantity): - newval = float(newval) + raise ValueError( + 'Only "auto" is acceptable when specifying ' + "the input range as a string." + ) + if isinstance(newval, u.Quantity): + newval = float(newval.magnitude) if isinstance(newval, (float, int)): if newval in valid: newval = valid.index(newval) else: - raise ValueError('Valid range settings are: {}'.format(valid)) + raise ValueError(f"Valid range settings are: {valid}") else: - raise TypeError('Range setting must be specified as a float, int, ' - 'or the string "auto", got {}'.format(type(newval))) - self.sendcmd('R{}X'.format(newval)) + raise TypeError( + "Range setting must be specified as a float, int, " + 'or the string "auto", got {}'.format(type(newval)) + ) + self.sendcmd(f"R{newval}X") # METHODS # @@ -300,7 +310,7 @@ def trigger(self): Do note that this is different from the standard SCPI ``*TRG`` command (which is not supported by the 580 anyways). """ - self.sendcmd('X') + self.sendcmd("X") def auto_range(self): """ @@ -309,7 +319,7 @@ def auto_range(self): This is the same as calling the `Keithley580.set_resistance_range` method and setting the parameter to "AUTO". """ - self.sendcmd('R0X') + self.sendcmd("R0X") def set_calibration_value(self, value): """ @@ -318,7 +328,7 @@ def set_calibration_value(self, value): :param value: Calibration value to write """ # self.write('V+n.nnnnE+nn') - raise NotImplementedError('setCalibrationValue not implemented') + raise NotImplementedError("setCalibrationValue not implemented") def store_calibration_constants(self): """ @@ -326,7 +336,7 @@ def store_calibration_constants(self): not currently implemented. """ # self.write('L0X') - raise NotImplementedError('setCalibrationConstants not implemented') + raise NotImplementedError("storeCalibrationConstants not implemented") def get_status_word(self): """ @@ -337,15 +347,16 @@ def get_status_word(self): :rtype: `str` """ tries = 5 - statusword = '' - while statusword[:3] != '580' and tries != 0: + statusword = "" + while statusword[:3] != b"580" and tries != 0: tries -= 1 - self.sendcmd('U0X') + self.sendcmd("U0X") time.sleep(1) - statusword = self.query('') + self.sendcmd("") + statusword = self._file.read_raw() - if statusword is None: - raise IOError('could not retrieve status word') + if tries == 0: + raise OSError("could not retrieve status word") return statusword[:-1] @@ -363,50 +374,63 @@ def parse_status_word(self, statusword): :rtype: `dict` """ - if statusword[:3] != '580': - raise ValueError('Status word starts with wrong ' - 'prefix: {}'.format(statusword)) - - (drive, polarity, drycircuit, operate, rng, - relative, eoi, trigger, sqrondata, sqronerror, - linefreq) = struct.unpack('@8c2s2s2', statusword[3:]) - - valid = {'drive': {'0': 'pulsed', - '1': 'dc'}, - 'polarity': {'0': '+', - '1': '-'}, - 'range': {'0': 'auto', - '1': 0.2, - '2': 2, - '3': 20, - '4': 2e2, - '5': 2e3, - '6': 2e4, - '7': 2e5}, - 'linefreq': {'0': '60Hz', - '1': '50Hz'}} + if statusword[:3] != b"580": + raise ValueError( + "Status word starts with wrong " "prefix: {}".format(statusword) + ) + + ( + drive, + polarity, + drycircuit, + operate, + rng, + relative, + eoi, + trigger, + sqrondata, + sqronerror, + linefreq, + ) = struct.unpack("@8c2s2sc", statusword[3:16]) + + valid = { + "drive": {b"0": "pulsed", b"1": "dc"}, + "polarity": {b"0": "+", b"1": "-"}, + "range": { + b"0": "auto", + b"1": 0.2, + b"2": 2, + b"3": 20, + b"4": 2e2, + b"5": 2e3, + b"6": 2e4, + b"7": 2e5, + }, + "linefreq": {b"0": "60Hz", b"1": "50Hz"}, + } try: - drive = valid['drive'][drive] - polarity = valid['polarity'][polarity] - rng = valid['range'][rng] - linefreq = valid['linefreq'][linefreq] + drive = valid["drive"][drive] + polarity = valid["polarity"][polarity] + rng = valid["range"][rng] + linefreq = valid["linefreq"][linefreq] except: - raise RuntimeError('Cannot parse status ' - 'word: {}'.format(statusword)) - - return {'drive': drive, - 'polarity': polarity, - 'drycircuit': (drycircuit == '1'), - 'operate': (operate == '1'), - 'range': rng, - 'relative': (relative == '1'), - 'eoi': eoi, - 'trigger': (trigger == '1'), - 'sqrondata': sqrondata, - 'sqronerror': sqronerror, - 'linefreq': linefreq, - 'terminator': self.terminator} + raise RuntimeError("Cannot parse status " "word: {}".format(statusword)) + + return { + "drive": drive, + "polarity": polarity, + "drycircuit": (drycircuit == b"1"), + "operate": (operate == b"1"), + "range": rng, + "relative": (relative == b"1"), + "eoi": eoi, + "trigger": (trigger == b"1"), + "sqrondata": sqrondata, + "sqronerror": sqronerror, + "linefreq": linefreq, + "terminator": self.terminator, + } def measure(self): """ @@ -415,10 +439,11 @@ def measure(self): The usual mode parameter is ignored for the Keithley 580 as the only valid mode is resistance. - :rtype: `~quantities.quantity.Quantity` + :rtype: `~pint.Quantity` """ self.trigger() - return self.parse_measurement(self.query(''))['resistance'] + self.sendcmd("") + return self.parse_measurement(self._file.read_raw()[:-1])["resistance"] @staticmethod def parse_measurement(measurement): @@ -433,38 +458,42 @@ def parse_measurement(measurement): :rtype: `dict` """ - (status, polarity, drycircuit, drive, resistance) = \ - struct.unpack('@4c11s', measurement) - - valid = {'status': {'S': 'standby', - 'N': 'normal', - 'O': 'overflow', - 'Z': 'relative'}, - 'polarity': {'+': '+', - '-': '-'}, - 'drycircuit': {'N': False, - 'D': True}, - 'drive': {'P': 'pulsed', - 'D': 'dc'}} + (status, polarity, drycircuit, drive, resistance) = struct.unpack( + "@4c11s", measurement + ) + + valid = { + "status": { + b"S": "standby", + b"N": "normal", + b"O": "overflow", + b"Z": "relative", + }, + "polarity": {b"+": "+", b"-": "-"}, + "drycircuit": {b"N": False, b"D": True}, + "drive": {b"P": "pulsed", b"D": "dc"}, + } try: - status = valid['status'][status] - polarity = valid['polarity'][polarity] - drycircuit = valid['drycircuit'][drycircuit] - drive = valid['drive'][drive] - resistance = float(resistance) * pq.ohm + status = valid["status"][status] + polarity = valid["polarity"][polarity] + drycircuit = valid["drycircuit"][drycircuit] + drive = valid["drive"][drive] + resistance = float(resistance) * u.ohm except: - raise Exception('Cannot parse measurement: {}'.format(measurement)) + raise Exception(f"Cannot parse measurement: {measurement}") - return {'status': status, - 'polarity': polarity, - 'drycircuit': drycircuit, - 'drive': drive, - 'resistance': resistance} + return { + "status": status, + "polarity": polarity, + "drycircuit": drycircuit, + "drive": drive, + "resistance": resistance, + } # COMMUNICATOR METHODS # - def sendcmd(self, msg): - super(Keithley580, self).sendcmd(msg + ':') + def sendcmd(self, cmd): + super().sendcmd(cmd + ":") - def query(self, msg, size=-1): - return super(Keithley580, self).query(msg + ':', size)[:-1] + def query(self, cmd, size=-1): + return super().query(cmd + ":", size)[:-1] diff --git a/instruments/keithley/keithley6220.py b/instruments/keithley/keithley6220.py index fae1dff99..0ff380670 100644 --- a/instruments/keithley/keithley6220.py +++ b/instruments/keithley/keithley6220.py @@ -1,15 +1,12 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides support for the Keithley 6220 constant current supply """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division -import quantities as pq +from instruments.units import ureg as u from instruments.abstract_instruments import PowerSupply from instruments.generic_scpi import SCPIInstrument @@ -28,10 +25,10 @@ class Keithley6220(SCPIInstrument, PowerSupply): Example usage: - >>> import quantities as pq + >>> import instruments.units as u >>> import instruments as ik >>> ccs = ik.keithley.Keithley6220.open_gpibusb("/dev/ttyUSB0", 10) - >>> ccs.current = 10 * pq.milliamp # Sets current to 10mA + >>> ccs.current = 10 * u.milliamp # Sets current to 10mA >>> ccs.disable() # Turns off the output and sets the current to 0A """ @@ -51,32 +48,34 @@ def channel(self): >>> ccs.channel[0].current = 0.01 >>> ccs.current = 0.01 """ - return self, + return (self,) @property def voltage(self): """ This property is not supported by the Keithley 6220. """ - raise NotImplementedError("The Keithley 6220 does not support voltage " - "settings.") + raise NotImplementedError( + "The Keithley 6220 does not support voltage " "settings." + ) @voltage.setter def voltage(self, newval): - raise NotImplementedError("The Keithley 6220 does not support voltage " - "settings.") + raise NotImplementedError( + "The Keithley 6220 does not support voltage " "settings." + ) current, current_min, current_max = bounded_unitful_property( "SOUR:CURR", - pq.amp, - valid_range=(-105 * pq.milliamp, +105 * pq.milliamp), + u.amp, + valid_range=(-105 * u.milliamp, +105 * u.milliamp), doc=""" Gets/sets the output current of the source. Value must be between -105mA and +105mA. :units: As specified, or assumed to be :math:`\\text{A}` otherwise. - :type: `float` or `~quantities.Quantity` - """ + :type: `float` or `~pint.Quantity` + """, ) # METHODS # diff --git a/instruments/keithley/keithley6514.py b/instruments/keithley/keithley6514.py index 80eaecda5..8581f1fc0 100644 --- a/instruments/keithley/keithley6514.py +++ b/instruments/keithley/keithley6514.py @@ -1,21 +1,15 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides support for the Keithley 6514 electrometer """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division -from builtins import map - from enum import Enum -import quantities as pq - from instruments.abstract_instruments import Electrometer from instruments.generic_scpi import SCPIInstrument +from instruments.units import ureg as u from instruments.util_fns import bool_property, enum_property # CLASSES ##################################################################### @@ -30,7 +24,7 @@ class Keithley6514(SCPIInstrument, Electrometer): Example usage: >>> import instruments as ik - >>> import quantities as pq + >>> import instruments.units as u >>> dmm = ik.keithley.Keithley6514.open_gpibusb('/dev/ttyUSB0', 12) """ @@ -40,48 +34,62 @@ class Mode(Enum): """ Enum containing valid measurement modes for the Keithley 6514 """ - voltage = 'VOLT:DC' - current = 'CURR:DC' - resistance = 'RES' - charge = 'CHAR' + + voltage = "VOLT:DC" + current = "CURR:DC" + resistance = "RES" + charge = "CHAR" class TriggerMode(Enum): """ Enum containing valid trigger modes for the Keithley 6514 """ - immediate = 'IMM' - tlink = 'TLINK' + + immediate = "IMM" + tlink = "TLINK" class ArmSource(Enum): """ Enum containing valid trigger arming sources for the Keithley 6514 """ - immediate = 'IMM' - timer = 'TIM' - bus = 'BUS' - tlink = 'TLIN' - stest = 'STES' - pstest = 'PST' - nstest = 'NST' - manual = 'MAN' + + immediate = "IMM" + timer = "TIM" + bus = "BUS" + tlink = "TLIN" + stest = "STES" + pstest = "PST" + nstest = "NST" + manual = "MAN" class ValidRange(Enum): """ Enum containing valid measurement ranges for the Keithley 6514 """ + voltage = (2, 20, 200) - current = (20e-12, 200e-12, 2e-9, 20e-9, - 200e-9, 2e-6, 20e-6, 200e-6, 2e-3, 20e-3) + current = ( + 20e-12, + 200e-12, + 2e-9, + 20e-9, + 200e-9, + 2e-6, + 20e-6, + 200e-6, + 2e-3, + 20e-3, + ) resistance = (2e3, 20e3, 200e3, 2e6, 20e6, 200e6, 2e9, 20e9, 200e9) charge = (20e-9, 200e-9, 2e-6, 20e-6) # CONSTANTS # _MODE_UNITS = { - Mode.voltage: pq.volt, - Mode.current: pq.amp, - Mode.resistance: pq.ohm, - Mode.charge: pq.coulomb + Mode.voltage: u.volt, + Mode.current: u.amp, + Mode.resistance: u.ohm, + Mode.charge: u.coulomb, } # PRIVATE METHODS # @@ -96,11 +104,11 @@ def _valid_range(self, mode): elif mode == self.Mode.charge: return self.ValidRange.charge else: - raise ValueError('Invalid mode.') + raise ValueError("Invalid mode.") def _parse_measurement(self, ascii): # TODO: don't assume ASCII data format # pylint: disable=fixme - vals = list(map(float, ascii.split(','))) + vals = list(map(float, ascii.split(","))) reading = vals[0] * self.unit timestamp = vals[1] status = vals[2] @@ -110,48 +118,48 @@ def _parse_measurement(self, ascii): # The mode values have quotes around them for some annoying reason. mode = enum_property( - 'FUNCTION', + "FUNCTION", Mode, input_decoration=lambda val: val[1:-1], # output_decoration=lambda val: '"{}"'.format(val), set_fmt='{} "{}"', doc=""" Gets/sets the measurement mode of the Keithley 6514. - """ + """, ) trigger_mode = enum_property( - 'TRIGGER:SOURCE', + "TRIGGER:SOURCE", TriggerMode, doc=""" Gets/sets the trigger mode of the Keithley 6514. - """ + """, ) arm_source = enum_property( - 'ARM:SOURCE', + "ARM:SOURCE", ArmSource, doc=""" Gets/sets the arm source of the Keithley 6514. - """ + """, ) zero_check = bool_property( - 'SYST:ZCH', - inst_true='ON', - inst_false='OFF', + "SYST:ZCH", + inst_true="ON", + inst_false="OFF", doc=""" Gets/sets the zero checking status of the Keithley 6514. - """ + """, ) zero_correct = bool_property( - 'SYST:ZCOR', - inst_true='ON', - inst_false='OFF', + "SYST:ZCOR", + inst_true="ON", + inst_false="OFF", doc=""" Gets/sets the zero correcting status of the Keithley 6514. - """ + """, ) @property @@ -166,36 +174,34 @@ def auto_range(self): :type: `bool` """ # pylint: disable=no-member - out = self.query('{}:RANGE:AUTO?'.format(self.mode.value)) - return True if out == '1' else False + out = self.query(f"{self.mode.value}:RANGE:AUTO?") + return True if out == "1" else False @auto_range.setter def auto_range(self, newval): # pylint: disable=no-member - self.sendcmd('{}:RANGE:AUTO {}'.format( - self.mode.value, '1' if newval else '0')) + self.sendcmd("{}:RANGE:AUTO {}".format(self.mode.value, "1" if newval else "0")) @property def input_range(self): """ Gets/sets the upper limit of the current range. - :type: `~quantities.Quantity` + :type: `~pint.Quantity` """ # pylint: disable=no-member mode = self.mode - out = self.query('{}:RANGE:UPPER?'.format(mode.value)) + out = self.query(f"{mode.value}:RANGE:UPPER?") return float(out) * self._MODE_UNITS[mode] @input_range.setter def input_range(self, newval): # pylint: disable=no-member mode = self.mode - val = newval.rescale(self._MODE_UNITS[mode]).item() + val = newval.to(self._MODE_UNITS[mode]).magnitude if val not in self._valid_range(mode).value: - raise ValueError( - 'Unexpected range limit for currently selected mode.') - self.sendcmd('{}:RANGE:UPPER {:e}'.format(mode.value, val)) + raise ValueError("Unexpected range limit for currently selected mode.") + self.sendcmd(f"{mode.value}:RANGE:UPPER {val:e}") # METHODS ## @@ -212,7 +218,7 @@ def auto_config(self, mode): - Disable buffer operation - Enable autozero """ - self.sendcmd('CONF:{}'.format(mode.value)) + self.sendcmd(f"CONF:{mode.value}") def fetch(self): """ @@ -220,7 +226,7 @@ def fetch(self): (So does not issue a trigger) Returns a tuple of the form (reading, timestamp) """ - raw = self.query('FETC?') + raw = self.query("FETC?") reading, timestamp, _ = self._parse_measurement(raw) return reading, timestamp @@ -229,6 +235,6 @@ def read_measurements(self): Trigger and acquire readings using the current mode. Returns a tuple of the form (reading, timestamp) """ - raw = self.query('READ?') + raw = self.query("READ?") reading, timestamp, _ = self._parse_measurement(raw) return reading, timestamp diff --git a/instruments/lakeshore/__init__.py b/instruments/lakeshore/__init__.py index e9343a49e..ac32a5d2e 100644 --- a/instruments/lakeshore/__init__.py +++ b/instruments/lakeshore/__init__.py @@ -1,10 +1,8 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing Lakeshore instruments """ -from __future__ import absolute_import from instruments.lakeshore.lakeshore340 import Lakeshore340 from instruments.lakeshore.lakeshore370 import Lakeshore370 diff --git a/instruments/lakeshore/lakeshore340.py b/instruments/lakeshore/lakeshore340.py index 051d32d82..697e3ac04 100644 --- a/instruments/lakeshore/lakeshore340.py +++ b/instruments/lakeshore/lakeshore340.py @@ -1,18 +1,12 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides support for the Lakeshore 340 cryogenic temperature controller. """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division -from builtins import range - -import quantities as pq - from instruments.generic_scpi import SCPIInstrument +from instruments.units import ureg as u from instruments.util_fns import ProxyList # CLASSES ##################################################################### @@ -26,7 +20,7 @@ class Lakeshore340(SCPIInstrument): Example usage: >>> import instruments as ik - >>> import quantities as pq + >>> import instruments.units as u >>> inst = ik.lakeshore.Lakeshore340.open_gpibusb('/dev/ttyUSB0', 1) >>> print(inst.sensor[0].temperature) >>> print(inst.sensor[1].temperature) @@ -34,7 +28,7 @@ class Lakeshore340(SCPIInstrument): # INNER CLASSES ## - class Sensor(object): + class Sensor: """ Class representing a sensor attached to the Lakeshore 340. @@ -55,10 +49,10 @@ def temperature(self): Gets the temperature of the specified sensor. :units: Kelvin - :type: `~quantities.quantity.Quantity` + :type: `~pint.Quantity` """ - value = self._parent.query('KRDG?{}'.format(self._idx)) - return pq.Quantity(float(value), pq.Kelvin) + value = self._parent.query(f"KRDG?{self._idx}") + return u.Quantity(float(value), u.kelvin) # PROPERTIES ## diff --git a/instruments/lakeshore/lakeshore370.py b/instruments/lakeshore/lakeshore370.py index 8293b8de4..005b1e7ca 100644 --- a/instruments/lakeshore/lakeshore370.py +++ b/instruments/lakeshore/lakeshore370.py @@ -1,18 +1,12 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides support for the Lakeshore 370 AC resistance bridge. """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division -from builtins import range - -import quantities as pq - from instruments.generic_scpi import SCPIInstrument +from instruments.units import ureg as u from instruments.util_fns import ProxyList # CLASSES ##################################################################### @@ -32,13 +26,13 @@ class Lakeshore370(SCPIInstrument): """ def __init__(self, filelike): - super(Lakeshore370, self).__init__(filelike) + super().__init__(filelike) # Disable termination characters and enable EOI - self.sendcmd('IEEE 3,0') + self.sendcmd("IEEE 3,0") # INNER CLASSES ## - class Channel(object): + class Channel: """ Class representing a sensor attached to the Lakeshore 370. @@ -59,10 +53,10 @@ def resistance(self): Gets the resistance of the specified sensor. :units: Ohm - :rtype: `~quantities.quantity.Quantity` + :rtype: `~pint.Quantity` """ - value = self._parent.query('RDGR? {}'.format(self._idx)) - return pq.Quantity(float(value), pq.ohm) + value = self._parent.query(f"RDGR? {self._idx}") + return u.Quantity(float(value), u.ohm) # PROPERTIES ## diff --git a/instruments/lakeshore/lakeshore475.py b/instruments/lakeshore/lakeshore475.py index 86d02fb59..cee029f1e 100644 --- a/instruments/lakeshore/lakeshore475.py +++ b/instruments/lakeshore/lakeshore475.py @@ -1,40 +1,24 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides support for the Lakeshore 475 Gaussmeter. """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division - -from builtins import range from enum import IntEnum -import quantities as pq - from instruments.generic_scpi import SCPIInstrument +from instruments.units import ureg as u from instruments.util_fns import assume_units, bool_property # CONSTANTS ################################################################### -LAKESHORE_FIELD_UNITS = { - 1: pq.gauss, - 2: pq.tesla, - 3: pq.oersted, - 4: pq.CompoundUnit('A/m') -} +LAKESHORE_FIELD_UNITS = {1: u.gauss, 2: u.tesla, 3: u.oersted, 4: u.amp / u.meter} -LAKESHORE_TEMP_UNITS = { - 1: pq.celsius, - 2: pq.kelvin -} +LAKESHORE_TEMP_UNITS = {1: u.celsius, 2: u.kelvin} -LAKESHORE_FIELD_UNITS_INV = dict((v, k) for k, v in - LAKESHORE_FIELD_UNITS.items()) -LAKESHORE_TEMP_UNITS_INV = dict((v, k) for k, v in - LAKESHORE_TEMP_UNITS.items()) +LAKESHORE_FIELD_UNITS_INV = {v: k for k, v in LAKESHORE_FIELD_UNITS.items()} +LAKESHORE_TEMP_UNITS_INV = {v: k for k, v in LAKESHORE_TEMP_UNITS.items()} # CLASSES ##################################################################### @@ -47,11 +31,11 @@ class Lakeshore475(SCPIInstrument): Example usage: >>> import instruments as ik - >>> import quantities as pq + >>> import instruments.units as u >>> gm = ik.lakeshore.Lakeshore475.open_gpibusb('/dev/ttyUSB0', 1) >>> print(gm.field) - >>> gm.field_units = pq.tesla - >>> gm.field_setpoint = 0.05 * pq.tesla + >>> gm.field_units = u.tesla + >>> gm.field_setpoint = 0.05 * u.tesla """ # ENUMS ## @@ -60,6 +44,7 @@ class Mode(IntEnum): """ Enum containing valid measurement modes for the Lakeshore 475 """ + dc = 1 rms = 2 peak = 3 @@ -68,6 +53,7 @@ class Filter(IntEnum): """ Enum containing valid filter modes for the Lakeshore 475 """ + wide = 1 narrow = 2 lowpass = 3 @@ -76,6 +62,7 @@ class PeakMode(IntEnum): """ Enum containing valid peak modes for the Lakeshore 475 """ + periodic = 1 pulse = 2 @@ -83,6 +70,7 @@ class PeakDisplay(IntEnum): """ Enum containing valid peak displays for the Lakeshore 475 """ + positive = 1 negative = 2 both = 3 @@ -94,9 +82,9 @@ def field(self): """ Read field from connected probe. - :type: `~quantities.quantity.Quantity` + :type: `~pint.Quantity` """ - return float(self.query('RDGFIELD?')) * self.field_units + return float(self.query("RDGFIELD?")) * self.field_units @property def field_units(self): @@ -105,20 +93,20 @@ def field_units(self): Acceptable units are Gauss, Tesla, Oersted, and Amp/meter. - :type: `~quantities.unitquantity.UnitQuantity` + :type: `~pint.Unit` """ - value = int(self.query('UNIT?')) + value = int(self.query("UNIT?")) return LAKESHORE_FIELD_UNITS[value] @field_units.setter def field_units(self, newval): - if isinstance(newval, pq.unitquantity.UnitQuantity): + if isinstance(newval, u.Unit): if newval in LAKESHORE_FIELD_UNITS_INV: - self.sendcmd('UNIT ' + LAKESHORE_FIELD_UNITS_INV[newval]) + self.sendcmd(f"UNIT {LAKESHORE_FIELD_UNITS_INV[newval]}") else: - raise ValueError('Not an acceptable Python quantities object') + raise ValueError("Not an acceptable Python quantities object") else: - raise TypeError('Field units must be a Python quantity') + raise TypeError("Field units must be a Python quantity") @property def temp_units(self): @@ -127,39 +115,47 @@ def temp_units(self): Acceptable units are celcius and kelvin. - :type: `~quantities.unitquantity.UnitQuantity` + :type: `~pint.Unit` """ - value = int(self.query('TUNIT?')) + value = int(self.query("TUNIT?")) return LAKESHORE_TEMP_UNITS[value] @temp_units.setter def temp_units(self, newval): - if isinstance(newval, pq.unitquantity.UnitQuantity): + if isinstance(newval, u.Unit): if newval in LAKESHORE_TEMP_UNITS_INV: - self.sendcmd('TUNIT ' + LAKESHORE_TEMP_UNITS_INV[newval]) + self.sendcmd(f"TUNIT {LAKESHORE_TEMP_UNITS_INV[newval]}") else: - raise TypeError('Not an acceptable Python quantities object') + raise TypeError("Not an acceptable Python quantities object") else: - raise TypeError('Temperature units must be a Python quantity') + raise TypeError("Temperature units must be a Python quantity") @property def field_setpoint(self): """ Gets/sets the final setpoint of the field control ramp. - :units: As specified (if a `~quantities.Quantity`) or assumed to be + :units: As specified (if a `~pint.Quantity`) or assumed to be of units Gauss. - :type: `~quantities.quantity.Quantity` with units Gauss + :type: `~pint.Quantity` with units Gauss """ - value = self.query('CSETP?').strip() + value = self.query("CSETP?").strip() units = self.field_units return float(value) * units @field_setpoint.setter def field_setpoint(self, newval): - units = self.field_units - newval = float(assume_units(newval, pq.gauss).rescale(units).magnitude) - self.sendcmd('CSETP {}'.format(newval)) + expected_units = self.field_units + newval = assume_units(newval, u.gauss) + + if newval.units != expected_units: + raise ValueError( + f"Field setpoint must be specified in the same units " + f"that the field units are currently set to. Attempts units of " + f"{newval.units}, currently expecting {expected_units}." + ) + + self.sendcmd(f"CSETP {newval.magnitude}") @property def field_control_params(self): @@ -167,36 +163,37 @@ def field_control_params(self): Gets/sets the parameters associated with the field control ramp. These are (in this order) the P, I, ramp rate, and control slope limit. - :type: `tuple` of 2 `float` and 2 `~quantities.quantity.Quantity` + :type: `tuple` of 2 `float` and 2 `~pint.Quantity` """ - params = self.query('CPARAM?').strip().split(',') + params = self.query("CPARAM?").strip().split(",") params = [float(x) for x in params] - params[2] = params[2] * self.field_units / pq.minute - params[3] = params[3] * pq.volt / pq.minute + params[2] = params[2] * self.field_units / u.minute + params[3] = params[3] * u.volt / u.minute return tuple(params) @field_control_params.setter def field_control_params(self, newval): if not isinstance(newval, tuple): - raise TypeError('Field control parameters must be specified as ' - ' a tuple') - newval = list(newval) - newval[0] = float(newval[0]) - newval[1] = float(newval[1]) - - unit = self.field_units / pq.minute - newval[2] = float( - assume_units(newval[2], unit).rescale(unit).magnitude) - unit = pq.volt / pq.minute - newval[3] = float( - assume_units(newval[3], unit).rescale(unit).magnitude) - - self.sendcmd('CPARAM {},{},{},{}'.format( - newval[0], - newval[1], - newval[2], - newval[3], - )) + raise TypeError("Field control parameters must be specified as " " a tuple") + p, i, ramp_rate, control_slope_lim = newval + + expected_units = self.field_units / u.minute + + ramp_rate = assume_units(ramp_rate, expected_units) + if ramp_rate.units != expected_units: + raise ValueError( + f"Field control params ramp rate must be specified in the same units " + f"that the field units are currently set to, per minute. Attempts units of " + f"{ramp_rate.units}, currently expecting {expected_units}." + ) + ramp_rate = float(ramp_rate.magnitude) + + unit = u.volt / u.minute + control_slope_lim = float( + assume_units(control_slope_lim, unit).to(unit).magnitude + ) + + self.sendcmd(f"CPARAM {p},{i},{ramp_rate},{control_slope_lim}") @property def p_value(self): @@ -235,16 +232,16 @@ def ramp_rate(self): """ Gets/sets the ramp rate value for the field control ramp. - :units: As specified (if a `~quantities.Quantity`) or assumed to be + :units: As specified (if a `~pint.Quantity`) or assumed to be of current field units / minute. - :type: `~quantities.quantity.Quantity` + :type: `~pint.Quantity` """ return self.field_control_params[2] @ramp_rate.setter def ramp_rate(self, newval): - unit = self.field_units / pq.minute - newval = float(assume_units(newval, unit).rescale(unit).magnitude) + unit = self.field_units / u.minute + newval = float(assume_units(newval, unit).to(unit).magnitude) values = list(self.field_control_params) values[2] = newval self.field_control_params = tuple(values) @@ -254,22 +251,22 @@ def control_slope_limit(self): """ Gets/sets the I value for the field control ramp. - :units: As specified (if a `~quantities.Quantity`) or assumed to be + :units: As specified (if a `~pint.Quantity`) or assumed to be of units volt / minute. - :type: `~quantities.quantity.Quantity` + :type: `~pint.Quantity` """ return self.field_control_params[3] @control_slope_limit.setter def control_slope_limit(self, newval): - unit = pq.volt / pq.minute - newval = float(assume_units(newval, unit).rescale(unit).magnitude) + unit = u.volt / u.minute + newval = float(assume_units(newval, unit).to(unit).magnitude) values = list(self.field_control_params) values[3] = newval self.field_control_params = tuple(values) control_mode = bool_property( - name="CMODE", + command="CMODE", inst_true="1", inst_false="0", doc=""" @@ -278,14 +275,15 @@ def control_slope_limit(self, newval): field control. :type: `bool` - """ + """, ) # METHODS ## # pylint: disable=too-many-arguments - def change_measurement_mode(self, mode, resolution, filter_type, - peak_mode, peak_disp): + def change_measurement_mode( + self, mode, resolution, filter_type, peak_mode, peak_disp + ): """ Change the measurement mode of the Gaussmeter. @@ -309,23 +307,31 @@ def change_measurement_mode(self, mode, resolution, filter_type, :type peak_disp: `Lakeshore475.PeakDisplay` """ if not isinstance(mode, Lakeshore475.Mode): - raise TypeError("Mode setting must be a " - "`Lakeshore475.Mode` value, got {} " - "instead.".format(type(mode))) + raise TypeError( + "Mode setting must be a " + "`Lakeshore475.Mode` value, got {} " + "instead.".format(type(mode)) + ) if not isinstance(resolution, int): raise TypeError('Parameter "resolution" must be an integer.') if not isinstance(filter_type, Lakeshore475.Filter): - raise TypeError("Filter type setting must be a " - "`Lakeshore475.Filter` value, got {} " - "instead.".format(type(filter_type))) + raise TypeError( + "Filter type setting must be a " + "`Lakeshore475.Filter` value, got {} " + "instead.".format(type(filter_type)) + ) if not isinstance(peak_mode, Lakeshore475.PeakMode): - raise TypeError("Filter type setting must be a " - "`Lakeshore475.PeakMode` value, got {} " - "instead.".format(type(peak_mode))) + raise TypeError( + "Peak measurement type setting must be a " + "`Lakeshore475.PeakMode` value, got {} " + "instead.".format(type(peak_mode)) + ) if not isinstance(peak_disp, Lakeshore475.PeakDisplay): - raise TypeError("Filter type setting must be a " - "`Lakeshore475.PeakDisplay` value, got {} " - "instead.".format(type(peak_disp))) + raise TypeError( + "Peak display type setting must be a " + "`Lakeshore475.PeakDisplay` value, got {} " + "instead.".format(type(peak_disp)) + ) mode = mode.value filter_type = filter_type.value @@ -336,12 +342,10 @@ def change_measurement_mode(self, mode, resolution, filter_type, if resolution in range(3, 6): resolution -= 2 else: - raise ValueError('Only 3,4,5 are valid resolutions.') - - self.sendcmd('RDGMODE {},{},{},{},{}'.format( - mode, - resolution, - filter_type, - peak_mode, - peak_disp - )) + raise ValueError("Only 3,4,5 are valid resolutions.") + + self.sendcmd( + "RDGMODE {},{},{},{},{}".format( + mode, resolution, filter_type, peak_mode, peak_disp + ) + ) diff --git a/instruments/minghe/__init__.py b/instruments/minghe/__init__.py new file mode 100644 index 000000000..4c9ec9535 --- /dev/null +++ b/instruments/minghe/__init__.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python +""" +Module containing MingHe instruments +""" +from .mhs5200a import MHS5200 diff --git a/instruments/minghe/mhs5200a.py b/instruments/minghe/mhs5200a.py new file mode 100644 index 000000000..f0e0314f0 --- /dev/null +++ b/instruments/minghe/mhs5200a.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python +""" +Provides the support for the MingHe low-cost function generator. + +Class originally contributed by Catherine Holloway. +""" + +# IMPORTS ##################################################################### + +from enum import Enum + +from instruments.abstract_instruments import FunctionGenerator +from instruments.units import ureg as u +from instruments.util_fns import ProxyList, assume_units + +# CLASSES ##################################################################### + + +class MHS5200(FunctionGenerator): + """ + The MHS5200 is a low-cost, 2 channel function generator. + + There is no user manual, but Al Williams has reverse-engineered the + communications protocol: + https://github.com/wd5gnr/mhs5200a/blob/master/MHS5200AProtocol.pdf + """ + + def __init__(self, filelike): + super().__init__(filelike) + self._channel_count = 2 + self.terminator = "\r\n" + + def _ack_expected(self, msg=""): + if msg.find(":r") == 0: + return None + # most commands res + return "ok" + + # INNER CLASSES # + + class Channel(FunctionGenerator.Channel): + """ + Class representing a channel on the MHS52000. + """ + + # pylint: disable=protected-access + + __CHANNEL_NAMES = {1: "1", 2: "2"} + + def __init__(self, mhs, idx): + self._mhs = mhs + super(MHS5200.Channel, self).__init__(parent=mhs, name=idx) + # Use zero-based indexing for the external API, but one-based + # for talking to the instrument. + self._idx = idx + 1 + self._chan = self.__CHANNEL_NAMES[self._idx] + self._count = 0 + + def _get_amplitude_(self): + query = f":r{self._chan}a" + response = self._mhs.query(query) + return float(response.replace(query, "")) / 100.0, self._mhs.VoltageMode.rms + + def _set_amplitude_(self, magnitude, units): + if ( + units == self._mhs.VoltageMode.peak_to_peak + or units == self._mhs.VoltageMode.rms + ): + magnitude = assume_units(magnitude, "V").to(u.V).magnitude + elif units == self._mhs.VoltageMode.dBm: + raise NotImplementedError("Decibel units are not supported.") + magnitude *= 100 + query = f":s{self._chan}a{int(magnitude)}" + self._mhs.sendcmd(query) + + @property + def duty_cycle(self): + """ + Gets/Sets the duty cycle of this channel. + + :units: A fraction + :type: `float` + """ + query = f":r{self._chan}d" + response = self._mhs.query(query) + duty = float(response.replace(query, "")) / 10.0 + return duty + + @duty_cycle.setter + def duty_cycle(self, new_val): + query = f":s{self._chan}d{int(100.0 * new_val)}" + self._mhs.sendcmd(query) + + @property + def enable(self): + """ + Gets/Sets the enable state of this channel. + + :type: `bool` + """ + query = f":r{self._chan}b" + return int(self._mhs.query(query).replace(query, "").replace("\r", "")) + + @enable.setter + def enable(self, newval): + query = f":s{self._chan}b{int(newval)}" + self._mhs.sendcmd(query) + + @property + def frequency(self): + """ + Gets/Sets the frequency of this channel. + + :units: As specified (if a `~pint.Quantity`) or assumed to be + of units hertz. + :type: `~pint.Quantity` + """ + query = f":r{self._chan}f" + response = self._mhs.query(query) + freq = float(response.replace(query, "")) * u.Hz + return freq / 100.0 + + @frequency.setter + def frequency(self, new_val): + new_val = assume_units(new_val, u.Hz).to(u.Hz).magnitude * 100.0 + query = f":s{self._chan}f{int(new_val)}" + self._mhs.sendcmd(query) + + @property + def offset(self): + """ + Gets/Sets the offset of this channel. + + The fraction of the duty cycle to offset the function by. + + :type: `float` + """ + # need to convert + query = f":r{self._chan}o" + response = self._mhs.query(query) + return int(response.replace(query, "")) / 100.0 - 1.20 + + @offset.setter + def offset(self, new_val): + new_val = int(new_val * 100) + 120 + query = f":s{self._chan}o{new_val}" + self._mhs.sendcmd(query) + + @property + def phase(self): + """ + Gets/Sets the phase of this channel. + + :units: As specified (if a `~pint.Quantity`) or assumed to be + of degrees. + :type: `~pint.Quantity` + """ + # need to convert + query = f":r{self._chan}p" + response = self._mhs.query(query) + return int(response.replace(query, "")) * u.deg + + @phase.setter + def phase(self, new_val): + new_val = assume_units(new_val, u.deg).to("deg").magnitude + query = f":s{self._chan}p{int(new_val)}" + self._mhs.sendcmd(query) + + @property + def function(self): + """ + Gets/Sets the wave type of this channel. + + :type: `MHS5200.Function` + """ + query = f":r{self._chan}w" + response = self._mhs.query(query).replace(query, "") + return self._mhs.Function(int(response)) + + @function.setter + def function(self, new_val): + query = f":s{self._chan}w{self._mhs.Function(new_val).value}" + self._mhs.sendcmd(query) + + class Function(Enum): + """ + Enum containing valid wave modes for + """ + + sine = 0 + square = 1 + triangular = 2 + sawtooth_up = 3 + sawtooth_down = 4 + + @property + def channel(self): + """ + Gets a specific channel object. The desired channel is specified like + one would access a list. + + For instance, this would print the counts of the first channel:: + + >>> import instruments as ik + >>> mhs = ik.minghe.MHS5200.open_serial(vid=1027, pid=24577, + baud=19200, timeout=1) + >>> print(mhs.channel[0].frequency) + + :rtype: `list`[`MHS5200.Channel`] + """ + return ProxyList(self, MHS5200.Channel, range(self._channel_count)) + + @property + def serial_number(self): + """ + Get the serial number, as an int + + :rtype: int + """ + query = ":r0c" + response = self.query(query) + response = response.replace(query, "").replace("\r", "") + return response + + def _get_amplitude_(self): + raise NotImplementedError() + + def _set_amplitude_(self, magnitude, units): + raise NotImplementedError() diff --git a/instruments/named_struct.py b/instruments/named_struct.py new file mode 100644 index 000000000..5e2871224 --- /dev/null +++ b/instruments/named_struct.py @@ -0,0 +1,320 @@ +#!/usr/bin/env python +""" +Class for quickly defining C-like structures with named fields. +""" + +# IMPORTS ##################################################################### + + +import struct +from collections import OrderedDict + +# DESIGN NOTES ################################################################ + +# This class uses the Django-like strategy described at +# http://stackoverflow.com/a/3288988/267841 +# to assign a "birthday" to each Field as it's instantiated. We can thus sort +# each Field in a NamedStruct by its birthday. + +# Notably, this hack is not at all required on Python 3.6: +# https://www.python.org/dev/peps/pep-0520/ + +# TODO: arrays other than string arrays do not currently work. + +# PYLINT CONFIGURATION ######################################################## + +# All of the classes in this module need to interact with each other rather +# deeply, so we disable the protected-access check within this module. + +# pylint:disable=protected-access + +# CLASSES ##################################################################### + + +class Field: + """ + A named field within a C-style structure. + + :param str fmt: Format for the field, corresponding to the + documentation of the :mod:`struct` standard library package. + """ + + __n_fields_created = 0 + _field_birthday = None + + _fmt = "" + _name = None + _owner_type = object + + def __init__(self, fmt, strip_null=False): + super().__init__() + + # Record our birthday so that we can sort fields later. + self._field_birthday = Field.__n_fields_created + Field.__n_fields_created += 1 + + self._fmt = fmt.strip() + self._strip_null = strip_null + + # If we're given a length, check that it + # makes sense. + if self._fmt[:-1] and int(self._fmt[:-1]) < 0: + raise TypeError("Field is specified with negative length.") + + def is_significant(self): + return not self._fmt.endswith("x") + + @property + def fmt_char(self): + """ + Gets the format character + """ + return self._fmt[-1] + + def __len__(self): + if self._fmt[:-1]: + # Although we know that length > 0, this abs ensures that static + # code checks are happy with __len__ always returning a positive number + return abs(int(self._fmt[:-1])) + + raise TypeError("Field is scalar and has no len().") + + def __repr__(self): + if self._owner_type: # pylint: disable=using-constant-test + return "".format( + self._name, self._owner_type, self._fmt + ) + + return f"" + + def __str__(self): + n, fmt_char = len(self), self.fmt_char + c_type = { + "x": "char", + "c": "char", + "b": "char", + "B": "unsigned char", + "?": "bool", + "h": "short", + "H": "unsigned short", + "i": "int", + "I": "unsigned int", + "l": "long", + "L": "unsigned long", + "q": "long long", + "Q": "unsigned long long", + "f": "float", + "d": "double", + # NB: no [], since that will be implied by n. + "s": "char", + "p": "char", + "P": "void *", + }[fmt_char] + + if n: + c_type = f"{c_type}[{n}]" + return f"{c_type} {self._name}" if self.is_significant() else c_type + + # DESCRIPTOR PROTOCOL # + + def __get__(self, obj, type=None): + return obj._values[self._name] + + def __set__(self, obj, value): + obj._values[self._name] = value + + +class StringField(Field): + """ + Represents a field that is interpreted as a Python string. + + :param int length: Maximum allowed length of the field, as + measured in the number of bytes used by its encoding. + Note that if a shorter string is provided, it will + be padded by null bytes. + :param str encoding: Name of an encoding to use in serialization + and deserialization to Python strings. + :param bool strip_null: If `True`, null bytes (``'\x00'``) will + be removed from the right upon deserialization. + """ + + _strip_null = False + _encoding = "ascii" + + def __init__(self, length, encoding="ascii", strip_null=False): + super().__init__(f"{length}s") + self._strip_null = strip_null + self._encoding = encoding + + def __set__(self, obj, value): + if isinstance(value, bytes): + value = value.decode(self._encoding) + if self._strip_null: + value = value.rstrip("\x00") + value = value.encode(self._encoding) + + super().__set__(obj, value) + + def __get__(self, obj, type=None): + return super().__get__(obj, type=type).decode(self._encoding) + + +class Padding(Field): + """ + Represents a field whose value is insignificant, and will not + be kept in serialization and deserialization. + + :param int n_bytes: Number of padding bytes occupied by this field. + """ + + def __init__(self, n_bytes=1): + super().__init__(f"{n_bytes}x") + + +class HasFields(type): + """ + Metaclass used for NamedStruct + """ + + def __new__(mcs, name, bases, attrs): + # Since this is a metaclass, the __new__ method observes + # creation of new *classes* and not new instances. + # We call the superclass of HasFields, which is another + # metaclass, to do most of the heavy lifting of creating + # the new class. + cls = super().__new__(mcs, name, bases, attrs) + + # We now sort the fields by their birthdays and store them in an + # ordered dict for easier look up later. + cls._fields = OrderedDict( + [ + (field_name, field) + for field_name, field in sorted( + ( + (field_name, field) + for field_name, field in attrs.items() + if isinstance(field, Field) + ), + key=lambda item: item[1]._field_birthday, + ) + ] + ) + + # Assign names and owner types to each field so that they can follow + # the descriptor protocol. + for field_name, field in cls._fields.items(): + field._name = field_name + field._owner_type = cls + + # Associate a struct.Struct instance with the new class + # that defines how to pack/unpack the new type. + cls._struct = struct.Struct( + # TODO: support alignment char at start. + " ".join([field._fmt for field in cls._fields.values()]) + ) + + return cls + + +class NamedStruct(metaclass=HasFields): + """ + Represents a C-style struct with one or more named fields, + useful for packing and unpacking serialized data documented + in terms of C examples. For instance, consider a struct of the + form:: + + typedef struct { + unsigned long a = 0x1234; + char[12] dummy; + unsigned char b = 0xab; + } Foo; + + This struct can be represented as the following NamedStruct:: + + class Foo(NamedStruct): + a = Field('L') + dummy = Padding(12) + b = Field('B') + + foo = Foo(a=0x1234, b=0xab) + """ + + # Provide reasonable defaults for the lowercase-f-fields + # created by HasFields. This will prevent a few edge cases, + # allow type inference and will prevent pylint false positives. + _fields = {} + _struct = None + + def __init__(self, **kwargs): + super().__init__() + self._values = OrderedDict( + [ + (field._name, None) + for field in filter(Field.is_significant, self._fields.values()) + ] + ) + + for field_name, value in kwargs.items(): + setattr(self, field_name, value) + + def _to_seq(self): + return tuple(self._values.values()) + + @classmethod + def _from_seq(cls, new_values): + return cls( + **{ + field._name: new_value + for field, new_value in zip( + list(filter(Field.is_significant, cls._fields.values())), new_values + ) + } + ) + + def pack(self): + """ + Packs this instance into bytes, suitable for transmitting over + a network or recording to disc. See :func:`struct.pack` for details. + + :return bytes packed_data: A serialized representation of this + instance. + """ + return self._struct.pack(*self._to_seq()) + + @classmethod + def unpack(cls, buffer): + """ + Given a buffer, unpacks it into an instance of this NamedStruct. + See :func:`struct.unpack` for details. + + :param bytes buffer: Data to use in creating a new instance. + :return: The new instance represented by `buffer`. + """ + return cls._from_seq(cls._struct.unpack(buffer)) + + def __eq__(self, other): + if not isinstance(other, NamedStruct): + return False + + return self._values == other._values + + def __hash__(self): + return hash(self._values) + + def __str__(self): + return "{name} {{\n{fields}\n}}".format( + name=type(self).__name__, + fields="\n".join( + [ + " {field}{value};".format( + field=field, + value=( + f" = {repr(self._values[field._name])}" + if field.is_significant() + else "" + ), + ) + for field in self._fields.values() + ] + ), + ) diff --git a/instruments/newport/__init__.py b/instruments/newport/__init__.py index d55e1d62f..200052102 100644 --- a/instruments/newport/__init__.py +++ b/instruments/newport/__init__.py @@ -1,12 +1,11 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing Newport instruments """ -from __future__ import absolute_import +from .agilis import AGUC2 from .errors import NewportError -from .newportesp301 import ( - NewportESP301, NewportESP301Axis, NewportESP301HomeSearchMode -) +from .newportesp301 import NewportESP301 + +from .newport_pmc8742 import PicoMotorController8742 diff --git a/instruments/newport/agilis.py b/instruments/newport/agilis.py new file mode 100644 index 000000000..9761b1585 --- /dev/null +++ b/instruments/newport/agilis.py @@ -0,0 +1,553 @@ +#!/usr/bin/env python +""" +Provides support for the Newport Agilis Controller AG-UC2 only (currently). + +Agilis controllers are piezo driven motors that do not have support for units. +All units used in this document are given as steps. + +Currently I only have a AG-PR100 rotation stage available for testing. This +device does not contain a limit switch and certain routines are therefore +completely untested! These are labeled in their respective docstring with: + `UNTESTED: SEE COMMENT ON TOP` + +The governing document for the commands and implementation is: + +Agilis Series, Piezo Motor Driven Components, User's Manual, v2.2.x, +by Newport, especially chapter 4.7: "ASCII Command Set" +Document number from footer: EDH0224En5022 — 10/12 + +Routines not implemented at all: +- Measure current position (MA command): + This routine interrupts the communication and + restarts it afterwards. It can, according to the documentation, take up to + 2 minutes to complete. It is furthermore only available on stages with limit + switches. I currently do not have the capability to implement this therefore. +- Absolute Move (PA command): + Exactly the same reason as for MA command. +""" + +# IMPORTS ##################################################################### + +import time + +from enum import IntEnum + +from instruments.abstract_instruments import Instrument +from instruments.util_fns import ProxyList + +# CLASSES ##################################################################### + + +class AGUC2(Instrument): + + """ + Handles the communication with the AGUC2 controller using the serial + connection. + + Example usage: + + >>> import instruments as ik + >>> agl = ik.newport.AGUC2.open_serial(port='COM5', baud=921600) + + This loads a controller into the instance `agl`. The two axis are + called 'X' (axis 1) and 'Y' (axis 2). Controller commands and settings + can be executed as following, as examples: + + Reset the controller: + + >>> agl.reset_controller() + + Print the firmware version: + + >>> print(agl.firmware_version) + + Individual axes can be controlled and queried as following: + + Relative move by 1000 steps: + + >>> agl.axis["X"].move_relative(1000) + + Activate jogging in mode 3: + + >>> agl.axis["X"].jog(3) + + Jogging will continue until the axis is stopped + + >>> agl.axis["X"].stop() + + Query the step amplitude, then set the postive one to +10 and the + negative one to -20 + + >>> print(agl.axis["X"].step_amplitude) + >>> agl.axis["X"].step_amplitude = 10 + >>> agl.axis["X"].step_amplitude = -20 + """ + + def __init__(self, filelike): + super().__init__(filelike) + + # Instrument requires '\r\n' line termination + self.terminator = "\r\n" + + # Some local variables + self._remote_mode = False + self._sleep_time = 0.25 + + class Axis: + + """ + Class representing one axis attached to a Controller. This will likely + work with the AG-UC8 controller as well. + + .. warning:: This class should NOT be manually created by the user. It is + designed to be initialized by a Controller class + """ + + def __init__(self, cont, ax): + if not isinstance(cont, AGUC2): + raise TypeError("Don't do that.") + + # set axis integer + if isinstance(ax, AGUC2.Axes): + self._ax = ax.value + else: + self._ax = ax + + # set controller + self._cont = cont + + # PROPERTIES # + + @property + def axis_status(self): + """ + Returns the status of the current axis. + """ + resp = self._cont.ag_query(f"{int(self._ax)} TS") + if resp.find("TS") == -1: + return "Status code query failed." + + resp = int(resp.replace(str(int(self._ax)) + "TS", "")) + status_message = agilis_status_message(resp) + return status_message + + @property + def jog(self): + """ + Start jog motion / get jog mode + Defined jog steps are defined with `step_amplitude` function (default + 16). If a jog mode is supplied, the jog motion is started. Otherwise + the current jog mode is queried. Valid jog modes are: + + -4 — Negative direction, 666 steps/s at defined step amplitude. + -3 — Negative direction, 1700 steps/s at max. step amplitude. + -2 — Negative direction, 100 step/s at max. step amplitude. + -1 — Negative direction, 5 steps/s at defined step amplitude. + 0 — No move, go to READY state. + 1 — Positive direction, 5 steps/s at defined step amplitude. + 2 — Positive direction, 100 steps/s at max. step amplitude. + 3 — Positive direction, 1700 steps/s at max. step amplitude. + 4 — Positive direction, 666 steps/s at defined step amplitude. + + :return: Jog motion set + :rtype: `int` + """ + resp = self._cont.ag_query(f"{int(self._ax)} JA?") + return int(resp.split("JA")[1]) + + @jog.setter + def jog(self, mode): + mode = int(mode) + if mode < -4 or mode > 4: + raise ValueError("Jog mode out of range. Must be between -4 and " "4.") + + self._cont.ag_sendcmd(f"{int(self._ax)} JA {mode}") + + @property + def number_of_steps(self): + """ + Returns the number of accumulated steps in forward direction minus + the number of steps in backward direction since powering the + controller or since the last ZP (zero position) command, whatever + was last. + + Note: + The step size of the Agilis devices are not 100% repeatable and + vary between forward and backward direction. Furthermore, the step + size can be modified using the SU command. Consequently, the TP + command provides only limited information about the actual position + of the device. In particular, an Agilis device can be at very + different positions even though a TP command may return the same + result. + + :return: Number of steps + :rtype: int + """ + resp = self._cont.ag_query(f"{int(self._ax)} TP") + return int(resp.split("TP")[1]) + + @property + def move_relative(self): + """ + Moves the axis by nn steps / Queries the status of the axis. + Steps must be given a number that can be converted to a signed integer + between -2,147,483,648 and 2,147,483,647. + If queried, command returns the current target position. At least this + is the expected behaviour, never worked with the rotation stage. + """ + resp = self._cont.ag_query(f"{int(self._ax)} PR?") + return int(resp.split("PR")[1]) + + @move_relative.setter + def move_relative(self, steps): + steps = int(steps) + if steps < -2147483648 or steps > 2147483647: + raise ValueError( + "Number of steps are out of range. They must be " + "between -2,147,483,648 and 2,147,483,647" + ) + + self._cont.ag_sendcmd(f"{int(self._ax)} PR {steps}") + + @property + def move_to_limit(self): + """ + UNTESTED: SEE COMMENT ON TOP + + The command functions properly only with devices that feature a + limit switch like models AG-LS25, AG-M050L and AG-M100L. + + Starts a jog motion at a defined speed to the limit and stops + automatically when the limit is activated. See `jog` command for + details on available modes. + + Returns the distance of the current position to the limit in + 1/1000th of the total travel. + """ + resp = self._cont.ag_query(f"{int(self._ax)} MA?") + return int(resp.split("MA")[1]) + + @move_to_limit.setter + def move_to_limit(self, mode): + mode = int(mode) + if mode < -4 or mode > 4: + raise ValueError("Jog mode out of range. Must be between -4 and " "4.") + + self._cont.ag_sendcmd(f"{int(self._ax)} MA {mode}") + + @property + def step_amplitude(self): + """ + Sets / Gets the step_amplitude. + + Sets the step amplitude (step size) in positive and / or negative + direction. If the parameter is positive, it will set the step + amplitude in the forward direction. If the parameter is negative, + it will set the step amplitude in the backward direction. You can also + provide a tuple or list of two values (one positive, one negative), + which will set both values. + Valid values are between -50 and 50, except for 0. + + :return: Tuple of first negative, then positive step amplitude + response. + :rtype: (`int`, `int`) + """ + resp_neg = self._cont.ag_query(f"{int(self._ax)} SU-?") + resp_pos = self._cont.ag_query(f"{int(self._ax)} SU+?") + return int(resp_neg.split("SU")[1]), int(resp_pos.split("SU")[1]) + + @step_amplitude.setter + def step_amplitude(self, nns): + if not isinstance(nns, tuple) and not isinstance(nns, list): + nns = [nns] + + # check all values for validity + for nn in nns: + nn = int(nn) + if nn < -50 or nn > 50 or nn == 0: + raise ValueError( + "Step amplitude {} outside the valid range. " + "It must be between -50 and -1 or between " + "1 and 50.".format(nn) + ) + + for nn in nns: + self._cont.ag_sendcmd(f"{int(self._ax)} SU {int(nn)}") + + @property + def step_delay(self): + """ + Sets/gets the step delay of stepping mode. The delay applies for both + positive and negative directions. The delay is programmed as multiple + of 10µs. For example, a delay of 40 is equivalent to + 40 x 10 µs = 400 µs. The maximum value of the parameter is equal to a + delay of 2 seconds between pulses. By default, after reset, the value + is 0. + Setter: value must be integer between 0 and 200000 included + + :return: Step delay + :rtype: `int` + """ + resp = self._cont.ag_query(f"{int(self._ax)} DL?") + return int(resp.split("DL")[1]) + + @step_delay.setter + def step_delay(self, nn): + nn = int(nn) + if nn < 0 or nn > 200000: + raise ValueError( + "Step delay is out of range. It must be between " "0 and 200000." + ) + + self._cont.ag_sendcmd(f"{int(self._ax)} DL {nn}") + + # MODES # + + def am_i_still(self, max_retries=5): + """ + Function to test if an axis stands still. It queries the status of + the given axis and returns True (if axis is still) or False if it is + moving. + The reason this routine is implemented is because the status messages + can time out. If a timeout occurs, this routine will retry the query + until `max_retries` is reached. If query is still not successful, an + IOError will be raised. + + :param int max_retries: Maximum number of retries + + :return: True if the axis is still, False if the axis is moving + :rtype: bool + """ + retries = 0 + + while retries < max_retries: + status = self.axis_status + if status == agilis_status_message(0): + return True + elif ( + status == agilis_status_message(1) + or status == agilis_status_message(2) + or status == agilis_status_message(3) + ): + return False + else: + retries += 1 + + raise OSError( + "The function `am_i_still` ran out of maximum retries. " + "Could not query the status of the axis." + ) + + def stop(self): + """ + Stops the axis. This is useful to interrupt a jogging motion. + """ + self._cont.ag_sendcmd(f"{int(self._ax)} ST") + + def zero_position(self): + """ + Resets the step counter to zero. See `number_of_steps` for details. + """ + self._cont.ag_sendcmd(f"{int(self._ax)} ZP") + + # ENUMS # + + class Axes(IntEnum): + """ + Enumeration of valid delay channels for the AG-UC2 controller. + """ + + X = 1 + Y = 2 + + # INNER CLASSES # + + # PROPERTIES # + + @property + def axis(self): + """ + Gets a specific axis object. + + The desired axis is accessed by passing an EnumValue from + `~AGUC2.Channels`. For example, to access the X axis (axis 1): + + >>> import instruments as ik + >>> agl = ik.newport.AGUC2.open_serial(port='COM5', baud=921600) + >>> agl.axis["X"].move_relative(1000) + + See example in `AGUC2` for a more details + + :rtype: `AGUC2.Axis` + """ + self.enable_remote_mode = True + return ProxyList(self, self.Axis, AGUC2.Axes) + + @property + def enable_remote_mode(self): + """ + Gets / sets the status of remote mode. + """ + return self._remote_mode + + @enable_remote_mode.setter + def enable_remote_mode(self, newval): + if newval and not self._remote_mode: + self._remote_mode = True + self.ag_sendcmd("MR") + elif not newval and self._remote_mode: + self._remote_mode = False + self.ag_sendcmd("ML") + + @property + def error_previous_command(self): + """ + Retrieves the error of the previous command and translates it into a + string. The string is returned + """ + resp = self.ag_query("TE") + + if resp.find("TE") == -1: + return "Error code query failed." + + resp = int(resp.replace("TE", "")) + error_message = agilis_error_message(resp) + return error_message + + @property + def firmware_version(self): + """ + Returns the firmware version of the controller + """ + resp = self.ag_query("VE") + return resp + + @property + def limit_status(self): + """ + PARTLY UNTESTED: SEE COMMENT ABOVE + + Returns the limit switch status of the controller. Possible returns + are: + - PH0: No limit switch is active + - PH1: Limit switch of axis #1 (X) is active, + limit switch of axis #2 (Y) is not active + - PH2: Limit switch of axis #2 (Y) is active, + limit switch of axis #1 (X) is not active + - PH3: Limit switches of axis #1 (X) and axis #2 (Y) are active + + If device has no limit switch, this routine always returns PH0 + """ + self.enable_remote_mode = True + resp = self.ag_query("PH") + return resp + + @property + def sleep_time(self): + """ + The device often times out. Therefore a sleep time can be set. The + routine will wait for this amount (in seconds) every time after a + command or a query are sent. + Setting the sleep time: Give time in seconds + If queried: Returns the sleep time in seconds as a float + """ + return self._sleep_time + + @sleep_time.setter + def sleep_time(self, t): + if t < 0: + raise ValueError("Sleep time must be >= 0.") + + self._sleep_time = float(t) + + # MODES # + + def reset_controller(self): + """ + Resets the controller. All temporary settings are reset to the default + value. Controller is put into local model. + """ + self._remote_mode = False + self.ag_sendcmd("RS") + + # SEND COMMAND AND QUERY ROUTINES AGILIS STYLE # + + def ag_sendcmd(self, cmd): + """ + Sends the command, then sleeps + """ + self.sendcmd(cmd) + time.sleep(self._sleep_time) + + def ag_query(self, cmd, size=-1): + """ + This runs the query command. However, the query command often times + out for this device. The response of all queries are always strings. + If timeout occurs, the response will be: + "Query timed out." + """ + try: + resp = self.query(cmd, size=size) + except OSError: + resp = "Query timed out." + + # sleep + time.sleep(self._sleep_time) + + return resp + + +def agilis_error_message(error_code): + """ + Returns a string with th error message for a given Agilis error code. + + :param int error_code: error code as an integer + + :return: error message + :rtype: string + """ + if not isinstance(error_code, int): + return "Error code is not an integer." + + error_dict = { + 0: "No error", + -1: "Unknown command", + -2: "Axis out of range (must be 1 or 2, or must not be specified)", + -3: "Wrong format for parameter nn (or must not be specified)", + -4: "Parameter nn out of range", + -5: "Not allowed in local mode", + -6: "Not allowed in current state", + } + + if error_code in error_dict.keys(): + return error_dict[error_code] + else: + return "An unknown error occurred." + + +def agilis_status_message(status_code): + """ + Returns a string with the status message for a given Agilis status + code. + + :param int status_code: status code as returned + + :return: status message + :rtype: string + """ + if not isinstance(status_code, int): + return "Status code is not an integer." + + status_dict = { + 0: "Ready (not moving).", + 1: "Stepping (currently executing a `move_relative` command).", + 2: "Jogging (currently executing a `jog` command with command" + "parameter different than 0).", + 3: "Moving to limit (currently executing `measure_current_position`, " + "`move_to_limit`, or `move_absolute` command).", + } + + if status_code in status_dict.keys(): + return status_dict[status_code] + else: + return "An unknown status occurred." diff --git a/instruments/newport/errors.py b/instruments/newport/errors.py index 6659ae70c..f4d9baea1 100644 --- a/instruments/newport/errors.py +++ b/instruments/newport/errors.py @@ -1,13 +1,10 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides common error handling for Newport devices. """ # IMPORTS #################################################################### -from __future__ import absolute_import -from __future__ import division import datetime @@ -18,86 +15,87 @@ class NewportError(IOError): """ Raised in response to an error with a Newport-brand instrument. """ + start_time = datetime.datetime.now() # Dict Containing all possible errors. # Uses strings for keys in order to handle axis messageDict = { - '0': "NO ERROR DETECTED", - '1': "PCI COMMUNICATION TIME-OUT", - '2': "Reserved for future use", - '3': "Reserved for future use", - '4': "EMERGENCY SOP ACTIVATED", - '5': "Reserved for future use", - '6': "COMMAND DOES NOT EXIST", - '7': "PARAMETER OUT OF RANGE", - '8': "CABLE INTERLOCK ERROR", - '9': "AXIS NUMBER OUT OF RANGE", - '10': "Reserved for future use", - '11': "Reserved for future use", - '12': "Reserved for future use", - '13': "GROUP NUMBER MISSING", - '14': "GROUP NUMBER OUT OF RANGE", - '15': "GROUP NUMBER NOT ASSIGNED", - '16': "GROUP NUMBER ALREADY ASSIGNED", - '17': "GROUP AXIS OUT OF RANGE", - '18': "GROUP AXIS ALREADY ASSIGNED", - '19': "GROUP AXIS DUPLICATED", - '20': "DATA ACQUISITION IS BUSY", - '21': "DATA ACQUISITION SETUP ERROR", - '22': "DATA ACQUISITION NOT ENABLED", - '23': "SERVO CYCLE (400 µS) TICK FAILURE", - '24': "Reserved for future use", - '25': "DOWNLOAD IN PROGRESS", - '26': "STORED PROGRAM NOT STARTEDL", - '27': "COMMAND NOT ALLOWEDL", - '28': "STORED PROGRAM FLASH AREA FULL", - '29': "GROUP PARAMETER MISSING", - '30': "GROUP PARAMETER OUT OF RANGE", - '31': "GROUP MAXIMUM VELOCITY EXCEEDED", - '32': "GROUP MAXIMUM ACCELERATION EXCEEDED", - '33': "GROUP MAXIMUM DECELERATION EXCEEDED", - '34': " GROUP MOVE NOT ALLOWED DURING MOTION", - '35': "PROGRAM NOT FOUND", - '36': "Reserved for future use", - '37': "AXIS NUMBER MISSING", - '38': "COMMAND PARAMETER MISSING", - '39': "PROGRAM LABEL NOT FOUND", - '40': "LAST COMMAND CANNOT BE REPEATED", - '41': "MAX NUMBER OF LABELS PER PROGRAM EXCEEDED", - 'x00': "MOTOR TYPE NOT DEFINED", - 'x01': "PARAMETER OUT OF RANGE", - 'x02': "AMPLIFIER FAULT DETECTED", - 'x03': "FOLLOWING ERROR THRESHOLD EXCEEDED", - 'x04': "POSITIVE HARDWARE LIMIT DETECTED", - 'x05': "NEGATIVE HARDWARE LIMIT DETECTED", - 'x06': "POSITIVE SOFTWARE LIMIT DETECTED", - 'x07': "NEGATIVE SOFTWARE LIMIT DETECTED", - 'x08': "MOTOR / STAGE NOT CONNECTED", - 'x09': "FEEDBACK SIGNAL FAULT DETECTED", - 'x10': "MAXIMUM VELOCITY EXCEEDED", - 'x11': "MAXIMUM ACCELERATION EXCEEDED", - 'x12': "Reserved for future use", - 'x13': "MOTOR NOT ENABLED", - 'x14': "Reserved for future use", - 'x15': "MAXIMUM JERK EXCEEDED", - 'x16': "MAXIMUM DAC OFFSET EXCEEDED", - 'x17': "ESP CRITICAL SETTINGS ARE PROTECTED", - 'x18': "ESP STAGE DEVICE ERROR", - 'x19': "ESP STAGE DATA INVALID", - 'x20': "HOMING ABORTED", - 'x21': "MOTOR CURRENT NOT DEFINED", - 'x22': "UNIDRIVE COMMUNICATIONS ERROR", - 'x23': "UNIDRIVE NOT DETECTED", - 'x24': "SPEED OUT OF RANGE", - 'x25': "INVALID TRAJECTORY MASTER AXIS", - 'x26': "PARAMETER CHARGE NOT ALLOWED", - 'x27': "INVALID TRAJECTORY MODE FOR HOMING", - 'x28': "INVALID ENCODER STEP RATIO", - 'x29': "DIGITAL I/O INTERLOCK DETECTED", - 'x30': "COMMAND NOT ALLOWED DURING HOMING", - 'x31': "COMMAND NOT ALLOWED DUE TO GROUP", - 'x32': "INVALID TRAJECTORY MODE FOR MOVING" + "0": "NO ERROR DETECTED", + "1": "PCI COMMUNICATION TIME-OUT", + "2": "Reserved for future use", + "3": "Reserved for future use", + "4": "EMERGENCY SOP ACTIVATED", + "5": "Reserved for future use", + "6": "COMMAND DOES NOT EXIST", + "7": "PARAMETER OUT OF RANGE", + "8": "CABLE INTERLOCK ERROR", + "9": "AXIS NUMBER OUT OF RANGE", + "10": "Reserved for future use", + "11": "Reserved for future use", + "12": "Reserved for future use", + "13": "GROUP NUMBER MISSING", + "14": "GROUP NUMBER OUT OF RANGE", + "15": "GROUP NUMBER NOT ASSIGNED", + "16": "GROUP NUMBER ALREADY ASSIGNED", + "17": "GROUP AXIS OUT OF RANGE", + "18": "GROUP AXIS ALREADY ASSIGNED", + "19": "GROUP AXIS DUPLICATED", + "20": "DATA ACQUISITION IS BUSY", + "21": "DATA ACQUISITION SETUP ERROR", + "22": "DATA ACQUISITION NOT ENABLED", + "23": "SERVO CYCLE (400 µS) TICK FAILURE", + "24": "Reserved for future use", + "25": "DOWNLOAD IN PROGRESS", + "26": "STORED PROGRAM NOT STARTEDL", + "27": "COMMAND NOT ALLOWEDL", + "28": "STORED PROGRAM FLASH AREA FULL", + "29": "GROUP PARAMETER MISSING", + "30": "GROUP PARAMETER OUT OF RANGE", + "31": "GROUP MAXIMUM VELOCITY EXCEEDED", + "32": "GROUP MAXIMUM ACCELERATION EXCEEDED", + "33": "GROUP MAXIMUM DECELERATION EXCEEDED", + "34": " GROUP MOVE NOT ALLOWED DURING MOTION", + "35": "PROGRAM NOT FOUND", + "36": "Reserved for future use", + "37": "AXIS NUMBER MISSING", + "38": "COMMAND PARAMETER MISSING", + "39": "PROGRAM LABEL NOT FOUND", + "40": "LAST COMMAND CANNOT BE REPEATED", + "41": "MAX NUMBER OF LABELS PER PROGRAM EXCEEDED", + "x00": "MOTOR TYPE NOT DEFINED", + "x01": "PARAMETER OUT OF RANGE", + "x02": "AMPLIFIER FAULT DETECTED", + "x03": "FOLLOWING ERROR THRESHOLD EXCEEDED", + "x04": "POSITIVE HARDWARE LIMIT DETECTED", + "x05": "NEGATIVE HARDWARE LIMIT DETECTED", + "x06": "POSITIVE SOFTWARE LIMIT DETECTED", + "x07": "NEGATIVE SOFTWARE LIMIT DETECTED", + "x08": "MOTOR / STAGE NOT CONNECTED", + "x09": "FEEDBACK SIGNAL FAULT DETECTED", + "x10": "MAXIMUM VELOCITY EXCEEDED", + "x11": "MAXIMUM ACCELERATION EXCEEDED", + "x12": "Reserved for future use", + "x13": "MOTOR NOT ENABLED", + "x14": "Reserved for future use", + "x15": "MAXIMUM JERK EXCEEDED", + "x16": "MAXIMUM DAC OFFSET EXCEEDED", + "x17": "ESP CRITICAL SETTINGS ARE PROTECTED", + "x18": "ESP STAGE DEVICE ERROR", + "x19": "ESP STAGE DATA INVALID", + "x20": "HOMING ABORTED", + "x21": "MOTOR CURRENT NOT DEFINED", + "x22": "UNIDRIVE COMMUNICATIONS ERROR", + "x23": "UNIDRIVE NOT DETECTED", + "x24": "SPEED OUT OF RANGE", + "x25": "INVALID TRAJECTORY MASTER AXIS", + "x26": "PARAMETER CHARGE NOT ALLOWED", + "x27": "INVALID TRAJECTORY MODE FOR HOMING", + "x28": "INVALID ENCODER STEP RATIO", + "x29": "DIGITAL I/O INTERLOCK DETECTED", + "x30": "COMMAND NOT ALLOWED DURING HOMING", + "x31": "COMMAND NOT ALLOWED DUE TO GROUP", + "x32": "INVALID TRAJECTORY MODE FOR MOVING", } def __init__(self, errcode=None, timestamp=None): @@ -105,8 +103,7 @@ def __init__(self, errcode=None, timestamp=None): if timestamp is None: self._timestamp = datetime.datetime.now() - NewportError.start_time else: - self._timestamp = datetime.timedelta( - seconds=(timestamp * 400E-6)) + self._timestamp = datetime.datetime.now() - timestamp if errcode is not None: # Break the error code into an axis number @@ -116,25 +113,25 @@ def __init__(self, errcode=None, timestamp=None): if self._axis == 0: self._axis = None error_message = self.get_message(str(errcode)) - error = "Newport Error: {0}. Error Message: {1}. " \ - "At time : {2}".format(str(errcode), - error_message, - self._timestamp) - super(NewportError, self).__init__(error) + error = "Newport Error: {}. Error Message: {}. " "At time : {}".format( + str(errcode), error_message, self._timestamp + ) + super().__init__(error) else: - error_message = self.get_message('x{0}'.format(self._errcode)) - error = "Newport Error: {0}. Axis: {1}. " \ - "Error Message: {2}. " \ - "At time : {3}".format(str(self._errcode), - self._axis, - error_message, - self._timestamp) - super(NewportError, self).__init__(error) + error_message = self.get_message(f"x{self._errcode:02d}") + error = ( + "Newport Error: {}. Axis: {}. " + "Error Message: {}. " + "At time : {}".format( + str(self._errcode), self._axis, error_message, self._timestamp + ) + ) + super().__init__(error) else: self._errcode = None self._axis = None - super(NewportError, self).__init__("") + super().__init__("") # PRIVATE METHODS ## diff --git a/instruments/newport/newport_pmc8742.py b/instruments/newport/newport_pmc8742.py new file mode 100644 index 000000000..71240b831 --- /dev/null +++ b/instruments/newport/newport_pmc8742.py @@ -0,0 +1,1178 @@ +#!/usr/bin/env python +""" +Provides support for the Newport Pico Motor Controller 8742 + +Note that the class is currently only tested with one controller connected, +however, a main controller / secondary controller setup has also been +implemented already. Commands are as described in the Picomotor manual. + +If a connection via TCP/IP is opened, the standard port that these devices +listen to is 23. + +If you have only one controller connected, everything should work out of +the box. Please only use axiss 0 through 3. + +If you have multiple controllers connected (up to 31), you need to set the +addresses of each controller. This can be done with this this class. See, +e.g., routines for `controller_address`, `scan_controller`, and `scan`. +Also make sure that you set `multiple_controllers` to `True`. This is +used for internal handling of the class only and does not communicate with +the instruments. +If you run with multiple controllers, the axiss are as following: +Ch 0 - 3 -> Motors 1 - 4 on controller with address 1 +Ch 4 - 7 -> Motors 1 - 4 on controller with address 2 +Ch i - i+4 -> Motors 1 - 4 on controller with address i / 4 + 1 (with i%4 = 0) + +All network commands only work with the main controller (this should make +sense). + +If in multiple controller mode, you can always send controller specific +commands by sending them to one individual axis of that controller. +Any axis works! +""" + +# IMPORTS # + +from enum import IntEnum + +from instruments.abstract_instruments import Instrument +from instruments.units import ureg as u +from instruments.util_fns import assume_units, ProxyList + +# pylint: disable=too-many-lines + + +class PicoMotorController8742(Instrument): + """Newport Picomotor Controller 8742 Communications Class + + Use this class to communicate with the picomotor controller 8742. + Single-controller and multi-controller setup can be used. + + Device can be talked to via TCP/IP or over USB. + FixMe: InstrumentKit currently does not communicate correctly via USB! + + Example for TCP/IP controller in single controller mode: + >>> import instruments as ik + >>> ip = "192.168.1.2" + >>> port = 23 # this is the default port + >>> inst = ik.newport.PicoMotorController8742.open_tcpip(ip, port) + >>> motor1 = inst.axis[0] + >>> motor1.move_relative = 100 + + Example for communications via USB: + >>> import instruments as ik + >>> pid = 0x4000 + >>> vid = 0x104d + >>> ik.newport.PicoMotorController8742.open_usb(pid=pid, vid=vid) + >>> motor3 = inst.axis[2] + >>> motor3.move_absolute = -200 + + Example for multicontrollers with controller addresses 1 and 2: + >>> import instruments as ik + >>> ip = "192.168.1.2" + >>> port = 23 # this is the default port + >>> inst = ik.newport.PicoMotorController8742.open_tcpip(ip, port) + >>> inst.multiple_controllers = True + >>> contr1mot1 = inst.axis[0] + >>> contr2mot1 = inst.axis[4] + >>> contr1mot1.move_absolute = 200 + >>> contr2mot1.move_relative = -212 + """ + + def __init__(self, filelike): + """Initialize the PicoMotorController class.""" + super().__init__(filelike) + + # terminator + self.terminator = "\r\n" + + # setup + self._multiple_controllers = False + + # INNER CLASSES # + + class Axis: + """PicoMotorController8742 Axis class for individual motors.""" + + def __init__(self, parent, idx): + """Initialize the axis with the parent and the number. + + :raises IndexError: Axis accessed looks like a main / secondary + setup, but the flag for `multiple_controllers` is not set + appropriately. See introduction. + """ + if not isinstance(parent, PicoMotorController8742): + raise TypeError("Don't do that.") + + if idx > 3 and not parent.multiple_controllers: + raise IndexError( + "You requested an axis that is only " + "available in multi controller mode, " + "however, have not enabled it. See " + "`multi_controllers` routine." + ) + + # set controller + self._parent = parent + self._idx = idx % 4 + 1 + + # set _address: + if self._parent.multiple_controllers: + self._address = f"{idx // 4 + 1}>" + else: + self._address = "" + + # ENUMS # + + class MotorType(IntEnum): + """IntEnum Class containing valid MotorTypes + + Use this enum to set the motor type. You can select that no or an + unkown motor are connected. See also `motor_check` command to set + these values per controller automatically. + """ + + none = 0 + unknown = 1 + tiny = 2 + standard = 3 + + # PROPERTIES # + + @property + def acceleration(self): + """Get / set acceleration of axis in steps / sec^2. + + Valid values are between 1 and 200,000 (steps) 1 / sec^2 with the + default as 100,000 (steps) 1 / sec^2. If quantity is not unitful, + it is assumed that 1 / sec^2 is chosen. + + :return: Acceleration in 1 / sec^2 + :rtype: u.Quantity(int) + + :raises ValueError: Limit is out of bound. + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> ip = "192.168.1.2" + >>> port = 23 # this is the default port + >>> inst = ik.newport.PicoMotorController8742.open_tcpip(ip, port) + >>> ax = inst.axis[0] + >>> ax.acceleration = u.Quantity(500, 1/u.s**-2) + """ + return assume_units(int(self.query("AC?")), u.s ** -2) + + @acceleration.setter + def acceleration(self, value): + value = int(assume_units(value, u.s ** -2).to(u.s ** -2).magnitude) + if not 1 <= value <= 200000: + raise ValueError( + f"Acceleration must be between 1 and " + f"200,000 s^-2 but is {value}." + ) + self.sendcmd(f"AC{value}") + + @property + def home_position(self): + """Get / set home position + + The home position of the device is used, e.g., when moving + to a specific position instead of a relative move. Valid values + are between -2147483648 and 2147483647. + + :return: Home position. + :rtype: int + + :raises ValueError: Set value is out of range. + + Example: + >>> import instruments as ik + >>> ip = "192.168.1.2" + >>> port = 23 # this is the default port + >>> inst = ik.newport.PicoMotorController8742.open_tcpip(ip, port) + >>> ax = inst.axis[0] + >>> ax.home_position = 444 + """ + return int(self.query("DH?")) + + @home_position.setter + def home_position(self, value): + if not -2147483648 <= value <= 2147483647: + raise ValueError( + f"Home position must be between -2147483648 " + f"and 2147483647, but is {value}." + ) + self.sendcmd(f"DH{int(value)}") + + @property + def is_stopped(self): + """Get if an axis is stopped (not moving). + + :return: Is the axis stopped? + :rtype: bool + + Example: + >>> import instruments as ik + >>> ip = "192.168.1.2" + >>> port = 23 # this is the default port + >>> inst = ik.newport.PicoMotorController8742.open_tcpip(ip, port) + >>> ax = inst.axis[0] + >>> ax.is_stopped + True + """ + return bool(int(self.query("MD?"))) + + @property + def motor_type(self): + """Set / get the type of motor connected to the axis. + + Use a `MotorType` IntEnum to set this motor type. + + :return: Motor type set. + :rtype: MotorType + + :raises TypeError: Set motor type is not of type `MotorType`. + + Example: + >>> import instruments as ik + >>> ip = "192.168.1.2" + >>> port = 23 # this is the default port + >>> inst = ik.newport.PicoMotorController8742.open_tcpip(ip, port) + >>> ax = inst.axis[0] + >>> ax.motor_type = ax.MotorType.tiny + """ + retval = int(self.query("QM?")) + return self.MotorType(retval) + + @motor_type.setter + def motor_type(self, value): + if not isinstance(value, self.MotorType): + raise TypeError( + f"Set motor type must be of type `MotorType` " + f"but is of type {type(value)}." + ) + self.sendcmd(f"QM{value.value}") + + @property + def move_absolute(self): + """Get / set the absolute target position of a motor. + + Set with absolute position in steps. Valid values between + -2147483648 and +2147483647. + See also: `home_position`. + + :return: Absolute motion target position. + :rtype: int + + :raises ValueError: Requested position out of range. + + Example: + >>> import instruments as ik + >>> ip = "192.168.1.2" + >>> port = 23 # this is the default port + >>> inst = ik.newport.PicoMotorController8742.open_tcpip(ip, port) + >>> ax = inst.axis[0] + >>> ax.move_absolute = 100 + """ + return int(self.query("PA?")) + + @move_absolute.setter + def move_absolute(self, value): + if not -2147483648 <= value <= 2147483647: + raise ValueError( + f"Set position must be between -2147483648 " + f"and 2147483647, but is {value}." + ) + self.sendcmd(f"PA{int(value)}") + + @property + def move_relative(self): + """Get / set the relative target position of a motor. + + Set with relative motion in steps. Valid values between + -2147483648 and +2147483647. + See also: `home_position`. + + :return: Relative motion target position. + :rtype: int + + :raises ValueError: Requested motion out of range. + + Example: + >>> import instruments as ik + >>> ip = "192.168.1.2" + >>> port = 23 # this is the default port + >>> inst = ik.newport.PicoMotorController8742.open_tcpip(ip, port) + >>> ax = inst.axis[0] + >>> ax.move_relative = 100 + """ + return int(self.query("PR?")) + + @move_relative.setter + def move_relative(self, value): + if not -2147483648 <= value <= 2147483647: + raise ValueError( + f"Set motion must be between -2147483648 " + f"and 2147483647, but is {value}." + ) + self.sendcmd(f"PR{int(value)}") + + @property + def position(self): + """Queries current, actual position of motor. + + Positions are with respect to the home position. + + :return: Current position in steps. + :rtype: int + + Example: + >>> import instruments as ik + >>> ip = "192.168.1.2" + >>> port = 23 # this is the default port + >>> inst = ik.newport.PicoMotorController8742.open_tcpip(ip, port) + >>> ax = inst.axis[0] + >>> ax.position + 123 + """ + return int(self.query("TP?")) + + @property + def velocity(self): + """Get / set velocty of the connected motor (unitful). + + Velocity is given in (steps) per second (1/s). + If a `MotorType.tiny` motor is connected, the maximum velocity + allowed is 1750 /s, otherwise 2000 /s. + If no units are given, 1/s are assumed. + + :return: Velocity in 1/s + :rtype: u.Quantity(int) + + :raises ValueError: Set value is out of the allowed range. + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> ip = "192.168.1.2" + >>> port = 23 # this is the default port + >>> inst = ik.newport.PicoMotorController8742.open_tcpip(ip, port) + >>> ax = inst.axis[0] + >>> ax.velocity = u.Quantity(500, 1/u.s) + """ + retval = int(self.query("VA?")) + return u.Quantity(retval, 1 / u.s) + + @velocity.setter + def velocity(self, value): + if self.motor_type == self.MotorType.tiny: + max_velocity = 1750 + else: + max_velocity = 2000 + + value = int(assume_units(value, 1 / u.s).to(1 / u.s).magnitude) + if not 0 < value <= max_velocity: + raise ValueError( + f"The maximum allowed velocity for the set " + f"motor is {max_velocity}. The requested " + f"velocity of {value} is out of range." + ) + self.sendcmd(f"VA{value}") + + # METHODS # + + def move_indefinite(self, direction): + """Move the motor indefinitely in the specific direction. + + To stop motion, issue `stop_motion` or `abort_motion` command. + Direction is defined as a string of either "+" or "-". + + :param direction: Direction in which to move the motor, "+" or "-" + :type direction: str + + Example: + >>> from time import sleep + >>> import instruments as ik + >>> ip = "192.168.1.2" + >>> port = 23 # this is the default port + >>> inst = ik.newport.PicoMotorController8742.open_tcpip(ip, port) + >>> ax = inst.axis[0] + >>> ax.move_indefinite("+") + >>> sleep(1) # wait a second + >>> ax.stop() + """ + if direction in ["+", "-"]: + self.sendcmd(f"MV{direction}") + + def stop(self): + """Stops the specific axis if in motion. + + Example: + >>> from time import sleep + >>> import instruments as ik + >>> ip = "192.168.1.2" + >>> port = 23 # this is the default port + >>> inst = ik.newport.PicoMotorController8742.open_tcpip(ip, port) + >>> ax = inst.axis[0] + >>> ax.move_indefinite("+") + >>> sleep(1) # wait a second + >>> ax.stop() + """ + self.sendcmd("ST") + + # CONTROLLER SPECIFIC PROPERTIES # + + @property + def controller_address(self): + """Get / set the controller address. + + Valid address values are between 1 and 31. For setting up multiple + instruments, see `multiple_controllers`. + + :return: Address of this device if secondary, otherwise `None` + :rtype: int + """ + retval = int(self.query("SA?", axs=False)) + return retval + + @controller_address.setter + def controller_address(self, newval): + self.sendcmd(f"SA{int(newval)}", axs=False) + + @property + def controller_configuration(self): + """Get / set configuration of some of the controller’s features. + + Configuration is given as a bit mask. If changed, please save + the settings afterwards if you would like to do so. See + `save_settings`. + + The bitmask to be set can be either given as a number, or as a + string of the mask itself. The following values are equivalent: + 3, 0b11, "11" + + Bit 0: + Value 0: Perform auto motor detection. Check and set motor + type automatically when commanded to move. + Value 1: Do not perform auto motor detection on move. + Bit 1: + Value 0: Do not scan for motors connected to controllers upon + reboot (Performs ‘MC’ command upon power-up, reset or + reboot). + Value 1: Scan for motors connected to controller upon power-up + or reset. + + :return: Bitmask of the controller configuration. + :rtype: str, binary configuration + """ + return self.query("ZZ?", axs=False) + + @controller_configuration.setter + def controller_configuration(self, value): + if isinstance(value, str): + self.sendcmd(f"ZZ{value}", axs=False) + else: + self.sendcmd(f"ZZ{str(bin(value))[2:]}", axs=False) + + @property + def error_code(self): + """Get error code only. + + Error code0 means no error detected. + + :return: Error code. + :rtype: int + """ + return int(self.query("TE?", axs=False)) + + @property + def error_code_and_message(self): + """Get error code and message. + + :return: Error code, error message + :rtype: int, str + """ + retval = self.query("TB?", axs=False) + err_code, err_msg = retval.split(",") + err_code = int(err_code) + err_msg = err_msg.strip() + return err_code, err_msg + + @property + def firmware_version(self): + """Get the controller firmware version.""" + return self.query("VE?", axs=False) + + @property + def name(self): + """Get the name of the controller.""" + return self.query("*IDN?", axs=False) + + # CONTROLLER SPECIFIC METHODS # + + def abort_motion(self): + """Instantaneously stops any motion in progress.""" + self.sendcmd("AB", axs=False) + + def motor_check(self): + """Check what motors are connected and set parameters. + + Use the save command `save_settings` if you want to save the + configuration to the non-volatile memory. + """ + self.sendcmd("MC", axs=False) + + def purge(self): + """Purge the non-volatile memory of the controller. + + Perform a hard reset and reset all the saved variables. The + following variables are reset to factory settings: + 1. Hostname + 2. IP Mode + 3. IP Address + 4. Subnet mask address + 5. Gateway address + 6. Configuration register + 7. Motor type + 8. Desired Velocity + 9. Desired Acceleration + """ + self.sendcmd("XX", axs=False) + + def recall_parameters(self, value=0): + """Recall parameter set. + + This command restores the controller working parameters from values + saved in its non-volatile memory. It is useful when, for example, + the user has been exploring and changing parameters (e.g., velocity) + but then chooses to reload from previously stored, qualified + settings. Note that “*RCL 0” command just restores the working + parameters to factory default settings. It does not change the + settings saved in EEPROM. + + :param value: 0 -> Recall factory default, + 1 -> Recall last saved settings + :type int: + """ + self.sendcmd(f"*RCL{1 if value else 0}", axs=False) + + def reset(self): + """Reset the controller. + + Perform a soft reset. Saved variables are not deleted! For a + hard reset, see the `purge` command. + + ..note:: It might take up to 30 seconds to re-establish + communications via TCP/IP + """ + self.sendcmd("*RST", axs=False) + + def save_settings(self): + """Save user settings. + + This command saves the controller settings in its non-volatile memory. + The controller restores or reloads these settings to working registers + automatically after system reset or it reboots. The purge + command is used to clear non-volatile memory and restore to factory + settings. Note that the SM saves parameters for all motors. + + Saves the following variables: + 1. Controller address + 2. Hostname + 3. IP Mode + 4. IP Address + 5. Subnet mask address + 6. Gateway address + 7. Configuration register + 8. Motor type + 9. Desired Velocity + 10. Desired Acceleration + """ + self.sendcmd("SM", axs=False) + + # SEND AND QUERY # + + def sendcmd(self, cmd, axs=True): + """Send a command to an axis object. + + :param cmd: Command + :type cmd: str + :param axs: Send axis address along? Not used for controller + commands. Defaults to `True` + :type axs: bool + """ + if axs: + command = f"{self._address}{self._idx}{cmd}" + else: + command = f"{self._address}{cmd}" + self._parent.sendcmd(command) + + def query(self, cmd, size=-1, axs=True): + """Query for an axis object. + + :param cmd: Command + :type cmd: str + :param size: bytes to read, defaults to "until terminator" (-1) + :type size: int + :param axs: Send axis address along? Not used for controller + commands. Defaults to `True` + :type axs: bool + + :raises IOError: The wrong axis answered. + """ + if axs: + command = f"{self._address}{self._idx}{cmd}" + else: + command = f"{self._address}{cmd}" + + retval = self._parent.query(command, size=size) + + if retval[: len(self._address)] != self._address: + raise OSError( + f"Expected to hear back from secondary " + f"controller {self._address}, instead " + f"controller {retval[:len(self._address)]} " + f"answered." + ) + + return retval[len(self._address) :] + + @property + def axis(self): + """Return an axis object. + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> ip = "192.168.1.2" + >>> port = 23 # this is the default port + >>> inst = ik.newport.PicoMotorController8742.open_tcpip(ip, port) + >>> ax = inst.axis[0] + """ + return ProxyList(self, self.Axis, range(31 * 4)) + + @property + def controller_address(self): + """Get / set the controller address. + + Valid address values are between 1 and 31. For setting up multiple + instruments, see `multiple_controllers`. + + :return: Address of this device if secondary, otherwise `None` + :rtype: int + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> ip = "192.168.1.2" + >>> port = 23 # this is the default port + >>> inst = ik.newport.PicoMotorController8742.open_tcpip(ip, port) + >>> inst.controller_address = 13 + """ + return self.axis[0].controller_address + + @controller_address.setter + def controller_address(self, newval): + self.axis[0].controller_address = newval + + @property + def controller_configuration(self): + """Get / set configuration of some of the controller’s features. + + Configuration is given as a bit mask. If changed, please save + the settings afterwards if you would like to do so. See + `save_settings`. + + Bit 0: + Value 0: Perform auto motor detection. Check and set motor + type automatically when commanded to move. + Value 1: Do not perform auto motor detection on move. + Bit 1: + Value 0: Do not scan for motors connected to controllers upon + reboot (Performs ‘MC’ command upon power-up, reset or + reboot). + Value 1: Scan for motors connected to controller upon power-up + or reset. + + :return: Bitmask of the controller configuration. + :rtype: str + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> ip = "192.168.1.2" + >>> port = 23 # this is the default port + >>> inst = ik.newport.PicoMotorController8742.open_tcpip(ip, port) + >>> inst.controller_configuration = "11" + """ + return self.axis[0].controller_configuration + + @controller_configuration.setter + def controller_configuration(self, value): + self.axis[0].controller_configuration = value + + @property + def dhcp_mode(self): + """Get / set if device is in DHCP mode. + + If not in DHCP mode, a static IP address, gateway, and netmask + must be set. + + :return: Status if DHCP mode is enabled + :rtype: `bool` + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> ip = "192.168.1.2" + >>> port = 23 # this is the default port + >>> inst = ik.newport.PicoMotorController8742.open_tcpip(ip, port) + >>> inst.dhcp_mode = True + """ + return bool(self.query("IPMODE?")) + + @dhcp_mode.setter + def dhcp_mode(self, newval): + nn = 1 if newval else 0 + self.sendcmd(f"IPMODE{nn}") + + @property + def error_code(self): + """Get error code only. + + Error code0 means no error detected. + + :return: Error code. + :rtype: int + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> ip = "192.168.1.2" + >>> port = 23 # this is the default port + >>> inst = ik.newport.PicoMotorController8742.open_tcpip(ip, port) + >>> inst.error_code + 0 + """ + return self.axis[0].error_code + + @property + def error_code_and_message(self): + """Get error code and message. + + :return: Error code, error message + :rtype: int, str + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> ip = "192.168.1.2" + >>> port = 23 # this is the default port + >>> inst = ik.newport.PicoMotorController8742.open_tcpip(ip, port) + >>> inst.error_code + (0, 'NO ERROR DETECTED') + """ + return self.axis[0].error_code_and_message + + @property + def firmware_version(self): + """Get the controller firmware version. + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> ip = "192.168.1.2" + >>> port = 23 # this is the default port + >>> inst = ik.newport.PicoMotorController8742.open_tcpip(ip, port) + >>> inst.firmware_version + '8742 Version 2.2 08/01/13' + """ + return self.axis[0].firmware_version + + @property + def gateway(self): + """Get / set the gateway of the instrument. + + :return: Gateway address + :rtype: str + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> ip = "192.168.1.2" + >>> port = 23 # this is the default port + >>> inst = ik.newport.PicoMotorController8742.open_tcpip(ip, port) + >>> inst.gateway = "192.168.1.1" + """ + return self.query("GATEWAY?") + + @gateway.setter + def gateway(self, value): + self.sendcmd(f"GATEWAY {value}") + + @property + def hostname(self): + """Get / set the hostname of the instrument. + + :return: Hostname + :rtype: `str` + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> ip = "192.168.1.2" + >>> port = 23 # this is the default port + >>> inst = ik.newport.PicoMotorController8742.open_tcpip(ip, port) + >>> inst.hostname = "asdf" + """ + return self.query("HOSTNAME?") + + @hostname.setter + def hostname(self, value): + self.sendcmd(f"HOSTNAME {value}") + + @property + def ip_address(self): + """Get / set the IP address of the instrument. + + :return: IP address + :rtype: `str` + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> ip = "192.168.1.2" + >>> port = 23 # this is the default port + >>> inst = ik.newport.PicoMotorController8742.open_tcpip(ip, port) + >>> inst.ip_address = "192.168.1.2" + """ + return self.query("IPADDR?") + + @ip_address.setter + def ip_address(self, value): + self.sendcmd(f"IPADDR {value}") + + @property + def mac_address(self): + """Get the MAC address of the instrument. + + :return: MAC address + :rtype: `str` + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> ip = "192.168.1.2" + >>> port = 23 # this is the default port + >>> inst = ik.newport.PicoMotorController8742.open_tcpip(ip, port) + >>> inst.mac_address + '5827809, 8087' + """ + return self.query("MACADDR?") + + @property + def multiple_controllers(self): + """Get / set if multiple controllers are used. + + By default, this is initialized as `False`. Set to `True` if you + have a main controller / secondary controller via RS-485 network + set up. + + Instrument commands will always be sent to main controller. + Axis specific commands will be set to the axis chosen, see + `axis` description. + + :return: Status if multiple controllers are activated + :rtype: bool + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> ip = "192.168.1.2" + >>> port = 23 # this is the default port + >>> inst = ik.newport.PicoMotorController8742.open_tcpip(ip, port) + >>> inst.multiple_controllers = True + """ + return self._multiple_controllers + + @multiple_controllers.setter + def multiple_controllers(self, newval): + self._multiple_controllers = True if newval else False + + @property + def name(self): + """Get the name of the controller. + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> ip = "192.168.1.2" + >>> port = 23 # this is the default port + >>> inst = ik.newport.PicoMotorController8742.open_tcpip(ip, port) + >>> inst.name + 'New_Focus 8742 v2.2 08/01/13 13991' + """ + return self.axis[0].name + + @property + def netmask(self): + """Get / set the Netmask of the instrument. + + :return: Netmask + :rtype: `str` + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> ip = "192.168.1.2" + >>> port = 23 # this is the default port + >>> inst = ik.newport.PicoMotorController8742.open_tcpip(ip, port) + >>> inst.netmask = "255.255.255.0" + """ + return self.query("NETMASK?") + + @netmask.setter + def netmask(self, value): + self.sendcmd(f"NETMASK {value}") + + @property + def scan_controllers(self): + """RS-485 controller address map query of all controllers. + + 32 bit string that represents the following: + Bit: Value: (True: 1, False: 0) + 0 Address conflict? + 1: Controller with address 1 exists? + ... + 31: Controller with address 31 exists + + Bits 1—31 are one-to-one mapped to controller addresses 1—31. The + bit value is set to 1 only when there are no conflicts with that + address. For example, if the master controller determines that there + are unique controllers at addresses 1,2, and 7 and more than one + controller at address 23, this query will return 135. The binary + representation of 135 is 10000111. Bit #0 = 1 implies that the scan + found at lease one address conflict during last scan. Bit #1,2, 7 = 1 + implies that the scan found controllers with addresses 1,2, and 7 + that do not conflict with any other controller. + + :return: Binary representation of controller configuration bitmask. + :rtype: str + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> ip = "192.168.1.2" + >>> port = 23 # this is the default port + >>> inst = ik.newport.PicoMotorController8742.open_tcpip(ip, port) + >>> inst.scan_controllers + "10000111" + """ + return self.query("SC?") + + @property + def scan_done(self): + """Queries if a controller scan is done or not. + + :return: Controller scan done? + :rtype: bool + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> ip = "192.168.1.2" + >>> port = 23 # this is the default port + >>> inst = ik.newport.PicoMotorController8742.open_tcpip(ip, port) + >>> inst.scan_done + True + """ + return bool(int(self.query("SD?"))) + + # METHODS # + + def abort_motion(self): + """Instantaneously stop any motion in progress. + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> ip = "192.168.1.2" + >>> port = 23 # this is the default port + >>> inst = ik.newport.PicoMotorController8742.open_tcpip(ip, port) + >>> inst.abort_motion() + """ + self.axis[0].abort_motion() + + def motor_check(self): + """Check what motors are connected and set parameters. + + Use the save command `save_settings` if you want to save the + configuration to the non-volatile memory. + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> ip = "192.168.1.2" + >>> port = 23 # this is the default port + >>> inst = ik.newport.PicoMotorController8742.open_tcpip(ip, port) + >>> inst.motor_check() + """ + self.axis[0].motor_check() + + def scan(self, value=2): + """Initialize and set controller addresses automatically. + + Scans the RS-485 network for connected controllers and set the + addresses automatically. Three possible scan modes can be + selected: + Mode 0: + Primary controller scans the network but does not resolve + any address conflicts. + Mode 1: + Primary controller scans the network and resolves address + conflicts, if any. This option preserves the non-conflicting + addresses and reassigns the conflicting addresses starting + with the lowest available address. + Mode 2 (default): + Primary controller reassigns the addresses of all + controllers on the network in a sequential order starting + with master controller set to address 1. + + See also: `scan_controllers` property. + + :param value: Scan mode. + :type: int + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> ip = "192.168.1.2" + >>> port = 23 # this is the default port + >>> inst = ik.newport.PicoMotorController8742.open_tcpip(ip, port) + >>> inst.scan(2) + """ + self.sendcmd(f"SC{value}") + + def purge(self): + """Purge the non-volatile memory of the controller. + + Perform a hard reset and reset all the saved variables. The + following variables are reset to factory settings: + 1. Hostname + 2. IP Mode + 3. IP Address + 4. Subnet mask address + 5. Gateway address + 6. Configuration register + 7. Motor type + 8. Desired Velocity + 9. Desired Acceleration + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> ip = "192.168.1.2" + >>> port = 23 # this is the default port + >>> inst = ik.newport.PicoMotorController8742.open_tcpip(ip, port) + >>> inst.purge() + """ + self.axis[0].purge() + + def recall_parameters(self, value=0): + """Recall parameter set. + + This command restores the controller working parameters from values + saved in its non-volatile memory. It is useful when, for example, + the user has been exploring and changing parameters (e.g., velocity) + but then chooses to reload from previously stored, qualified + settings. Note that “*RCL 0” command just restores the working + parameters to factory default settings. It does not change the + settings saved in EEPROM. + + :param value: 0 -> Recall factory default, + 1 -> Recall last saved settings + :type value: int + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> ip = "192.168.1.2" + >>> port = 23 # this is the default port + >>> inst = ik.newport.PicoMotorController8742.open_tcpip(ip, port) + >>> inst.recall_parameters(1) + """ + self.axis[0].recall_parameters(value) + + def reset(self): + """Reset the controller. + + Perform a soft reset. Saved variables are not deleted! For a + hard reset, see the `purge` command. + + ..note:: It might take up to 30 seconds to re-establish + communications via TCP/IP + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> ip = "192.168.1.2" + >>> port = 23 # this is the default port + >>> inst = ik.newport.PicoMotorController8742.open_tcpip(ip, port) + >>> inst.reset() + """ + self.axis[0].reset() + + def save_settings(self): + """Save user settings. + + This command saves the controller settings in its non-volatile memory. + The controller restores or reloads these settings to working registers + automatically after system reset or it reboots. The purge + command is used to clear non-volatile memory and restore to factory + settings. Note that the SM saves parameters for all motors. + + Saves the following variables: + 1. Controller address + 2. Hostname + 3. IP Mode + 4. IP Address + 5. Subnet mask address + 6. Gateway address + 7. Configuration register + 8. Motor type + 9. Desired Velocity + 10. Desired Acceleration + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> ip = "192.168.1.2" + >>> port = 23 # this is the default port + >>> inst = ik.newport.PicoMotorController8742.open_tcpip(ip, port) + >>> inst.save_settings() + """ + self.axis[0].save_settings() + + # QUERY # + + def query(self, cmd, size=-1): + """Query's the device and returns ASCII string. + + Must be queried as a raw string with terminator line ending. This is + currently not implemented in instrument and therefore must be called + directly from file. + + Sometimes, the instrument sends an undecodable 6 byte header along + (usually for the first query). We'll catch it with a try statement. + The 6 byte header was also remarked in this matlab script: + https://github.com/cnanders/matlab-newfocus-model-8742 + """ + self.sendcmd(cmd) + retval = self.read_raw(size=size) + try: + retval = retval.decode("utf-8") + except UnicodeDecodeError: + retval = retval[6:].decode("utf-8") + + return retval diff --git a/instruments/newport/newportesp301.py b/instruments/newport/newportesp301.py index b1adc13e2..cfa6b3d2f 100644 --- a/instruments/newport/newportesp301.py +++ b/instruments/newport/newportesp301.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides support for the Newport ESP-301 motor controller. @@ -10,74 +9,16 @@ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division -from functools import reduce -from time import time, sleep from contextlib import contextmanager - -from builtins import range, map from enum import IntEnum - -import quantities as pq +from functools import reduce +import time from instruments.abstract_instruments import Instrument from instruments.newport.errors import NewportError +from instruments.units import ureg as u from instruments.util_fns import assume_units, ProxyList -# ENUMS ####################################################################### - - -class NewportESP301HomeSearchMode(IntEnum): - - """ - Enum containing different search modes code - """ - #: Search along specified axes for the +0 position. - zero_position_count = 0 - #: Search for combined Home and Index signals. - home_index_signals = 1 - #: Search only for the Home signal. - home_signal_only = 2 - #: Search for the positive limit signal. - pos_limit_signal = 3 - #: Search for the negative limit signal. - neg_limit_signal = 4 - #: Search for the positive limit and Index signals. - pos_index_signals = 5 - #: Search for the negative limit and Index signals. - neg_index_signals = 6 - - -class NewportESP301Units(IntEnum): - - """ - Enum containing what `units` return means. - """ - encoder_step = 0 - motor_step = 1 - millimeter = 2 - micrometer = 3 - inches = 4 - milli_inches = 5 - micro_inches = 6 - degree = 7 - gradian = 8 - radian = 9 - milliradian = 10 - microradian = 11 - - -class NewportESP301MotorType(IntEnum): - - """ - Enum for different motor types. - """ - undefined = 0 - dc_servo = 1 - stepper_motor = 2 - commutated_stepper_motor = 3 - commutated_brushless_servo = 4 # CLASSES ##################################################################### @@ -96,13 +37,1184 @@ class NewportESP301(Instrument): """ def __init__(self, filelike): - super(NewportESP301, self).__init__(filelike) + super().__init__(filelike) self._execute_immediately = True self._command_list = [] self._bulk_query_resp = "" self.terminator = "\r" - # PROPERTIES ## + class Axis: + + """ + Encapsulates communication concerning a single axis + of an ESP-301 controller. This class should not be + instantiated by the user directly, but is + returned by `NewportESP301.axis`. + """ + + # quantities micro inch + # micro_inch = u.UnitQuantity('micro-inch', u.inch / 1e6, symbol='uin') + micro_inch = u.uinch + + # Some more work might need to be done here to make + # the encoder_step and motor_step functional + # I really don't have a concrete idea how I'm + # going to do this until I have a physical device + + _unit_dict = { + 0: u.count, + 1: u.count, + 2: u.mm, + 3: u.um, + 4: u.inch, + 5: u.mil, + 6: micro_inch, # compound unit for micro-inch + 7: u.deg, + 8: u.grad, + 9: u.rad, + 10: u.mrad, + 11: u.urad, + } + + def __init__(self, controller, axis_id): + if not isinstance(controller, NewportESP301): + raise TypeError( + "Axis must be controlled by a Newport ESP-301 " "motor controller." + ) + + self._controller = controller + self._axis_id = axis_id + 1 + + self._units = self.units + + # CONTEXT MANAGERS ## + + @contextmanager + def _units_of(self, units): + """ + Sets the units for the corresponding axis to a those given by an integer + label (see `NewportESP301.Units`), ensuring that the units are properly + reset at the completion of the context manager. + """ + old_units = self._get_units() + self._set_units(units) + yield + self._set_units(old_units) + + # PRIVATE METHODS ## + + def _get_units(self): + """ + Returns the integer label for the current units set for this axis. + + .. seealso:: + NewportESP301.Units + """ + return self._controller.Units( + int(self._newport_cmd("SN?", target=self.axis_id)) + ) + + def _set_units(self, new_units): + return self._newport_cmd("SN", target=self.axis_id, params=[int(new_units)]) + + # PROPERTIES ## + + @property + def axis_id(self): + """ + Get axis number of Newport Controller + + :type: `int` + """ + return self._axis_id + + @property + def is_motion_done(self): + """ + `True` if and only if all motion commands have + completed. This method can be used to wait for + a motion command to complete before sending the next + command. + + :type: `bool` + """ + return bool(int(self._newport_cmd("MD?", target=self.axis_id))) + + @property + def acceleration(self): + """ + Gets/sets the axis acceleration + + :units: As specified (if a `~pint.Quantity`) or assumed to be + of current newport unit + :type: `~pint.Quantity` or `float` + """ + + return assume_units( + float(self._newport_cmd("AC?", target=self.axis_id)), + self._units / (u.s ** 2), + ) + + @acceleration.setter + def acceleration(self, newval): + if newval is None: + return + newval = float( + assume_units(newval, self._units / (u.s ** 2)) + .to(self._units / (u.s ** 2)) + .magnitude + ) + self._newport_cmd("AC", target=self.axis_id, params=[newval]) + + @property + def deceleration(self): + """ + Gets/sets the axis deceleration + + :units: As specified (if a `~pint.Quantity`) or assumed to be + of current newport :math:`\\frac{unit}{s^2}` + :type: `~pint.Quantity` or float + """ + return assume_units( + float(self._newport_cmd("AG?", target=self.axis_id)), + self._units / (u.s ** 2), + ) + + @deceleration.setter + def deceleration(self, newval): + if newval is None: + return + newval = float( + assume_units(newval, self._units / (u.s ** 2)) + .to(self._units / (u.s ** 2)) + .magnitude + ) + self._newport_cmd("AG", target=self.axis_id, params=[newval]) + + @property + def estop_deceleration(self): + """ + Gets/sets the axis estop deceleration + + :units: As specified (if a `~pint.Quantity`) or assumed to be + of current newport :math:`\\frac{unit}{s^2}` + :type: `~pint.Quantity` or float + """ + return assume_units( + float(self._newport_cmd("AE?", target=self.axis_id)), + self._units / (u.s ** 2), + ) + + @estop_deceleration.setter + def estop_deceleration(self, decel): + decel = float( + assume_units(decel, self._units / (u.s ** 2)) + .to(self._units / (u.s ** 2)) + .magnitude + ) + self._newport_cmd("AE", target=self.axis_id, params=[decel]) + + @property + def jerk(self): + """ + Gets/sets the jerk rate for the controller + + :units: As specified (if a `~pint.Quantity`) or assumed to be + of current newport unit + :type: `~pint.Quantity` or `float` + """ + + return assume_units( + float(self._newport_cmd("JK?", target=self.axis_id)), + self._units / (u.s ** 3), + ) + + @jerk.setter + def jerk(self, jerk): + jerk = float( + assume_units(jerk, self._units / (u.s ** 3)) + .to(self._units / (u.s ** 3)) + .magnitude + ) + self._newport_cmd("JK", target=self.axis_id, params=[jerk]) + + @property + def velocity(self): + """ + Gets/sets the axis velocity + + :units: As specified (if a `~pint.Quantity`) or assumed to be + of current newport :math:`\\frac{unit}{s}` + :type: `~pint.Quantity` or `float` + """ + return assume_units( + float(self._newport_cmd("VA?", target=self.axis_id)), self._units / u.s + ) + + @velocity.setter + def velocity(self, velocity): + velocity = float( + assume_units(velocity, self._units / (u.s)) + .to(self._units / u.s) + .magnitude + ) + self._newport_cmd("VA", target=self.axis_id, params=[velocity]) + + @property + def max_velocity(self): + """ + Gets/sets the axis maximum velocity + + :units: As specified (if a `~pint.Quantity`) or assumed to be + of current newport :math:`\\frac{unit}{s}` + :type: `~pint.Quantity` or `float` + """ + return assume_units( + float(self._newport_cmd("VU?", target=self.axis_id)), self._units / u.s + ) + + @max_velocity.setter + def max_velocity(self, newval): + if newval is None: + return + newval = float( + assume_units(newval, self._units / u.s).to(self._units / u.s).magnitude + ) + self._newport_cmd("VU", target=self.axis_id, params=[newval]) + + @property + def max_base_velocity(self): + """ + Gets/sets the maximum base velocity for stepper motors + + :units: As specified (if a `~pint.Quantity`) or assumed to be + of current newport :math:`\\frac{unit}{s}` + :type: `~pint.Quantity` or `float` + """ + return assume_units( + float(self._newport_cmd("VB?", target=self.axis_id)), self._units / u.s + ) + + @max_base_velocity.setter + def max_base_velocity(self, newval): + if newval is None: + return + newval = float( + assume_units(newval, self._units / u.s).to(self._units / u.s).magnitude + ) + self._newport_cmd("VB", target=self.axis_id, params=[newval]) + + @property + def jog_high_velocity(self): + """ + Gets/sets the axis jog high velocity + + :units: As specified (if a `~pint.Quantity`) or assumed to be + of current newport :math:`\\frac{unit}{s}` + :type: `~pint.Quantity` or `float` + """ + return assume_units( + float(self._newport_cmd("JH?", target=self.axis_id)), self._units / u.s + ) + + @jog_high_velocity.setter + def jog_high_velocity(self, newval): + if newval is None: + return + newval = float( + assume_units(newval, self._units / u.s).to(self._units / u.s).magnitude + ) + self._newport_cmd("JH", target=self.axis_id, params=[newval]) + + @property + def jog_low_velocity(self): + """ + Gets/sets the axis jog low velocity + + :units: As specified (if a `~pint.Quantity`) or assumed to be + of current newport :math:`\\frac{unit}{s}` + :type: `~pint.Quantity` or `float` + """ + return assume_units( + float(self._newport_cmd("JW?", target=self.axis_id)), self._units / u.s + ) + + @jog_low_velocity.setter + def jog_low_velocity(self, newval): + if newval is None: + return + newval = float( + assume_units(newval, self._units / u.s).to(self._units / u.s).magnitude + ) + self._newport_cmd("JW", target=self.axis_id, params=[newval]) + + @property + def homing_velocity(self): + """ + Gets/sets the axis homing velocity + + :units: As specified (if a `~pint.Quantity`) or assumed to be + of current newport :math:`\\frac{unit}{s}` + :type: `~pint.Quantity` or `float` + """ + return assume_units( + float(self._newport_cmd("OH?", target=self.axis_id)), self._units / u.s + ) + + @homing_velocity.setter + def homing_velocity(self, newval): + if newval is None: + return + newval = float( + assume_units(newval, self._units / u.s).to(self._units / u.s).magnitude + ) + self._newport_cmd("OH", target=self.axis_id, params=[newval]) + + @property + def max_acceleration(self): + """ + Gets/sets the axis max acceleration + + :units: As specified (if a `~pint.Quantity`) or assumed to be + of current newport :math:`\\frac{unit}{s^2}` + :type: `~pint.Quantity` or `float` + """ + return assume_units( + float(self._newport_cmd("AU?", target=self.axis_id)), + self._units / (u.s ** 2), + ) + + @max_acceleration.setter + def max_acceleration(self, newval): + if newval is None: + return + newval = float( + assume_units(newval, self._units / (u.s ** 2)) + .to(self._units / (u.s ** 2)) + .magnitude + ) + self._newport_cmd("AU", target=self.axis_id, params=[newval]) + + @property + def max_deceleration(self): + """ + Gets/sets the axis max decceleration. + Max deaceleration is always the same as acceleration. + + :units: As specified (if a `~pint.Quantity`) or assumed to be + of current newport :math:`\\frac{unit}{s^2}` + :type: `~pint.Quantity` or `float` + """ + return self.max_acceleration + + @max_deceleration.setter + def max_deceleration(self, decel): + decel = float( + assume_units(decel, self._units / (u.s ** 2)) + .to(self._units / (u.s ** 2)) + .magnitude + ) + self.max_acceleration = decel + + @property + def position(self): + """ + Gets real position on axis in units + + :units: As specified (if a `~pint.Quantity`) or assumed to be + of current newport unit + :type: `~pint.Quantity` or `float` + """ + return assume_units( + float(self._newport_cmd("TP?", target=self.axis_id)), self._units + ) + + @property + def desired_position(self): + """ + Gets desired position on axis in units + + :units: As specified (if a `~pint.Quantity`) or assumed to be + of current newport unit + :type: `~pint.Quantity` or `float` + """ + return assume_units( + float(self._newport_cmd("DP?", target=self.axis_id)), self._units + ) + + @property + def desired_velocity(self): + """ + Gets the axis desired velocity in unit/s + + :units: As specified (if a `~pint.Quantity`) or assumed to be + of current newport unit/s + :type: `~pint.Quantity` or `float` + """ + return assume_units( + float(self._newport_cmd("DV?", target=self.axis_id)), self._units / u.s + ) + + @property + def home(self): + """ + Gets/sets the axis home position. + Default should be 0 as that sets current position as home + + :units: As specified (if a `~pint.Quantity`) or assumed to be + of current newport unit + :type: `~pint.Quantity` or `float` + """ + return assume_units( + float(self._newport_cmd("DH?", target=self.axis_id)), self._units + ) + + @home.setter + def home(self, newval): + if newval is None: + return + newval = float(assume_units(newval, self._units).to(self._units).magnitude) + self._newport_cmd("DH", target=self.axis_id, params=[newval]) + + @property + def units(self): + """ + Get the units that all commands are in reference to. + + :type: `~pint.Unit` corresponding to units of axis connected or + int which corresponds to Newport unit number + """ + self._units = self._get_pq_unit(self._get_units()) + return self._units + + @units.setter + def units(self, newval): + if newval is None: + return + if isinstance(newval, int): + self._units = self._get_pq_unit(self._controller.Units(int(newval))) + elif isinstance(newval, u.Unit): + self._units = newval + newval = self._get_unit_num(newval) + self._set_units(newval) + + @property + def encoder_resolution(self): + """ + Gets/sets the resolution of the encode. The minimum number of units + per step. Encoder functionality must be enabled. + + :units: The number of units per encoder step + :type: `~pint.Quantity` or `float` + """ + + return assume_units( + float(self._newport_cmd("SU?", target=self.axis_id)), self._units + ) + + @encoder_resolution.setter + def encoder_resolution(self, newval): + if newval is None: + return + newval = float(assume_units(newval, self._units).to(self._units).magnitude) + self._newport_cmd("SU", target=self.axis_id, params=[newval]) + + @property + def full_step_resolution(self): + """ + Gets/sets the axis resolution of the encode. The minimum number of + units per step. Encoder functionality must be enabled. + + :units: The number of units per encoder step + :type: `~pint.Quantity` or `float` + """ + + return assume_units( + float(self._newport_cmd("FR?", target=self.axis_id)), self._units + ) + + @full_step_resolution.setter + def full_step_resolution(self, newval): + if newval is None: + return + newval = float(assume_units(newval, self._units).to(self._units).magnitude) + self._newport_cmd("FR", target=self.axis_id, params=[newval]) + + @property + def left_limit(self): + """ + Gets/sets the axis left travel limit + + :units: The limit in units + :type: `~pint.Quantity` or `float` + """ + return assume_units( + float(self._newport_cmd("SL?", target=self.axis_id)), self._units + ) + + @left_limit.setter + def left_limit(self, limit): + limit = float(assume_units(limit, self._units).to(self._units).magnitude) + self._newport_cmd("SL", target=self.axis_id, params=[limit]) + + @property + def right_limit(self): + """ + Gets/sets the axis right travel limit + + :units: units + :type: `~pint.Quantity` or `float` + """ + return assume_units( + float(self._newport_cmd("SR?", target=self.axis_id)), self._units + ) + + @right_limit.setter + def right_limit(self, limit): + limit = float(assume_units(limit, self._units).to(self._units).magnitude) + self._newport_cmd("SR", target=self.axis_id, params=[limit]) + + @property + def error_threshold(self): + """ + Gets/sets the axis error threshold + + :units: units + :type: `~pint.Quantity` or `float` + """ + return assume_units( + float(self._newport_cmd("FE?", target=self.axis_id)), self._units + ) + + @error_threshold.setter + def error_threshold(self, newval): + if newval is None: + return + newval = float(assume_units(newval, self._units).to(self._units).magnitude) + self._newport_cmd("FE", target=self.axis_id, params=[newval]) + + @property + def current(self): + """ + Gets/sets the axis current (amps) + + :units: As specified (if a `~pint.Quantity`) or assumed to be + of current newport :math:`\\text{A}` + :type: `~pint.Quantity` or `float` + """ + return assume_units( + float(self._newport_cmd("QI?", target=self.axis_id)), u.A + ) + + @current.setter + def current(self, newval): + if newval is None: + return + current = float(assume_units(newval, u.A).to(u.A).magnitude) + self._newport_cmd("QI", target=self.axis_id, params=[current]) + + @property + def voltage(self): + """ + Gets/sets the axis voltage + + :units: As specified (if a `~pint.Quantity`) or assumed to be + of current newport :math:`\\text{V}` + :type: `~pint.Quantity` or `float` + """ + return assume_units( + float(self._newport_cmd("QV?", target=self.axis_id)), u.V + ) + + @voltage.setter + def voltage(self, newval): + if newval is None: + return + voltage = float(assume_units(newval, u.V).to(u.V).magnitude) + self._newport_cmd("QV", target=self.axis_id, params=[voltage]) + + @property + def motor_type(self): + """ + Gets/sets the axis motor type + * 0 = undefined + * 1 = DC Servo + * 2 = Stepper motor + * 3 = commutated stepper motor + * 4 = commutated brushless servo motor + + :type: `int` + :rtype: `NewportESP301.MotorType` + """ + return self._controller.MotorType( + int(self._newport_cmd("QM?", target=self._axis_id)) + ) + + @motor_type.setter + def motor_type(self, newval): + if newval is None: + return + self._newport_cmd("QM", target=self._axis_id, params=[int(newval)]) + + @property + def feedback_configuration(self): + """ + Gets/sets the axis Feedback configuration + + :type: `int` + """ + return int(self._newport_cmd("ZB?", target=self._axis_id)[:-2], 16) + + @feedback_configuration.setter + def feedback_configuration(self, newval): + if newval is None: + return + self._newport_cmd("ZB", target=self._axis_id, params=[int(newval)]) + + @property + def position_display_resolution(self): + """ + Gets/sets the position display resolution + + :type: `int` + """ + return int(self._newport_cmd("FP?", target=self._axis_id)) + + @position_display_resolution.setter + def position_display_resolution(self, newval): + if newval is None: + return + self._newport_cmd("FP", target=self._axis_id, params=[int(newval)]) + + @property + def trajectory(self): + """ + Gets/sets the axis trajectory + + :type: `int` + """ + return int(self._newport_cmd("TJ?", target=self._axis_id)) + + @trajectory.setter + def trajectory(self, newval): + if newval is None: + return + self._newport_cmd("TJ", target=self._axis_id, params=[int(newval)]) + + @property + def microstep_factor(self): + """ + Gets/sets the axis microstep_factor + + :type: `int` + """ + return int(self._newport_cmd("QS?", target=self._axis_id)) + + @microstep_factor.setter + def microstep_factor(self, newval): + if newval is None: + return + newval = int(newval) + if newval < 1 or newval > 250: + raise ValueError("Microstep factor must be between 1 and 250") + else: + self._newport_cmd("QS", target=self._axis_id, params=[newval]) + + @property + def hardware_limit_configuration(self): + """ + Gets/sets the axis hardware_limit_configuration + + :type: `int` + """ + return int(self._newport_cmd("ZH?", target=self._axis_id)[:-2]) + + @hardware_limit_configuration.setter + def hardware_limit_configuration(self, newval): + if newval is None: + return + self._newport_cmd("ZH", target=self._axis_id, params=[int(newval)]) + + @property + def acceleration_feed_forward(self): + """ + Gets/sets the axis acceleration_feed_forward setting + + :type: `int` + """ + return float(self._newport_cmd("AF?", target=self._axis_id)) + + @acceleration_feed_forward.setter + def acceleration_feed_forward(self, newval): + if newval is None: + return + self._newport_cmd("AF", target=self._axis_id, params=[float(newval)]) + + @property + def proportional_gain(self): + """ + Gets/sets the axis proportional_gain + + :type: `float` + """ + return float(self._newport_cmd("KP?", target=self._axis_id)[:-1]) + + @proportional_gain.setter + def proportional_gain(self, newval): + if newval is None: + return + self._newport_cmd("KP", target=self._axis_id, params=[float(newval)]) + + @property + def derivative_gain(self): + """ + Gets/sets the axis derivative_gain + + :type: `float` + """ + return float(self._newport_cmd("KD?", target=self._axis_id)) + + @derivative_gain.setter + def derivative_gain(self, newval): + if newval is None: + return + self._newport_cmd("KD", target=self._axis_id, params=[float(newval)]) + + @property + def integral_gain(self): + """ + Gets/sets the axis integral_gain + + :type: `float` + """ + return float(self._newport_cmd("KI?", target=self._axis_id)) + + @integral_gain.setter + def integral_gain(self, newval): + if newval is None: + return + self._newport_cmd("KI", target=self._axis_id, params=[float(newval)]) + + @property + def integral_saturation_gain(self): + """ + Gets/sets the axis integral_saturation_gain + + :type: `float` + """ + return float(self._newport_cmd("KS?", target=self._axis_id)) + + @integral_saturation_gain.setter + def integral_saturation_gain(self, newval): + if newval is None: + return + self._newport_cmd("KS", target=self._axis_id, params=[float(newval)]) + + @property + def encoder_position(self): + """ + Gets the encoder position + + :type: + """ + with self._units_of(self._controller.Units.encoder_step): + return self.position + + # MOVEMENT METHODS # + + def search_for_home(self, search_mode=None): + """ + Searches this axis only + for home using the method specified by ``search_mode``. + + :param HomeSearchMode search_mode: Method to detect when + Home has been found. If None, the search mode is taken from + HomeSearchMode. + """ + if search_mode is None: + search_mode = self._controller.HomeSearchMode.zero_position_count.value + self._controller.search_for_home(axis=self.axis_id, search_mode=search_mode) + + def move(self, position, absolute=True, wait=False, block=False): + """ + :param position: Position to set move to along this axis. + :type position: `float` or :class:`~pint.Quantity` + :param bool absolute: If `True`, the position ``pos`` is + interpreted as relative to the zero-point of the encoder. + If `False`, ``pos`` is interpreted as relative to the current + position of this axis. + :param bool wait: If True, will tell axis to not execute other + commands until movement is finished + :param bool block: If True, will block code until movement is finished + """ + position = float( + assume_units(position, self._units).to(self._units).magnitude + ) + if absolute: + self._newport_cmd("PA", params=[position], target=self.axis_id) + else: + self._newport_cmd("PR", params=[position], target=self.axis_id) + + if wait: + self.wait_for_position(position) + if block: + time.sleep(0.003) + mot = self.is_motion_done + while not mot: + mot = self.is_motion_done + + def move_to_hardware_limit(self): + """ + move to hardware travel limit + """ + self._newport_cmd("MT", target=self.axis_id) + + def move_indefinitely(self): + """ + Move until told to stop + """ + self._newport_cmd("MV", target=self.axis_id) + + def abort_motion(self): + """ + Abort motion + """ + self._newport_cmd("AB", target=self.axis_id) + + def wait_for_stop(self): + """ + Waits for axis motion to stop before next command is executed + """ + self._newport_cmd("WS", target=self.axis_id) + + def stop_motion(self): + """ + Stop all motion on axis. With programmed deceleration rate + """ + self._newport_cmd("ST", target=self.axis_id) + + def wait_for_position(self, position): + """ + Wait for axis to reach position before executing next command + + :param position: Position to wait for on axis + + :type position: float or :class:`~pint.Quantity` + """ + position = float( + assume_units(position, self._units).to(self._units).magnitude + ) + self._newport_cmd("WP", target=self.axis_id, params=[position]) + + def wait_for_motion(self, poll_interval=0.01, max_wait=None): + """ + Blocks until all movement along this axis is complete, as reported + by `NewportESP301.Axis.is_motion_done`. + + :param float poll_interval: How long (in seconds) to sleep between + checking if the motion is complete. + :param float max_wait: Maximum amount of time to wait before + raising a `IOError`. If `None`, this method will wait + indefinitely. + """ + # FIXME: make sure that the controller is not in + # programming mode, or else this might not work. + # In programming mode, the "WS" command should be + # sent instead, and the two parameters to this method should + # be ignored. + poll_interval = float(assume_units(poll_interval, u.s).to(u.s).magnitude) + if max_wait is not None: + max_wait = float(assume_units(max_wait, u.s).to(u.s).magnitude) + tic = time.time() + while True: + if self.is_motion_done: + return + else: + if max_wait is None or (time.time() - tic) < max_wait: + time.sleep(poll_interval) + else: + raise OSError("Timed out waiting for motion to finish.") + + def enable(self): + """ + Turns motor axis on. + """ + self._newport_cmd("MO", target=self._axis_id) + + def disable(self): + """ + Turns motor axis off. + """ + self._newport_cmd("MF", target=self._axis_id) + + def setup_axis(self, **kwargs): + """ + Setup a non-newport DC servo motor stage. Necessary parameters are. + + * 'motor_type' = type of motor see 'QM' in Newport documentation + * 'current' = motor maximum current (A) + * 'voltage' = motor voltage (V) + * 'units' = set units (see NewportESP301.Units)(U) + * 'encoder_resolution' = value of encoder step in terms of (U) + * 'max_velocity' = maximum velocity (U/s) + * 'max_base_velocity' = maximum working velocity (U/s) + * 'homing_velocity' = homing speed (U/s) + * 'jog_high_velocity' = jog high speed (U/s) + * 'jog_low_velocity' = jog low speed (U/s) + * 'max_acceleration' = maximum acceleration (U/s^2) + * 'acceleration' = acceleration (U/s^2) + * 'velocity' = velocity (U/s) + * 'deceleration' = set deceleration (U/s^2) + * 'error_threshold' = set error threshold (U) + * 'estop_deceleration' = estop deceleration (U/s^2) + * 'jerk' = jerk rate (U/s^3) + * 'proportional_gain' = PID proportional gain (optional) + * 'derivative_gain' = PID derivative gain (optional) + * 'integral_gain' = PID internal gain (optional) + * 'integral_saturation_gain' = PID integral saturation (optional) + * 'trajectory' = trajectory mode (optional) + * 'position_display_resolution' (U per step) + * 'feedback_configuration' + * 'full_step_resolution' = (U per step) + * 'home' = (U) + * 'acceleration_feed_forward' = between 0 to 2e9 + * 'microstep_factor' = axis microstep factor + * 'reduce_motor_torque_time' = time (ms) between 0 and 60000, + * 'reduce_motor_torque_percentage' = percentage between 0 and 100 + """ + + self.motor_type = kwargs.get("motor_type") + self.feedback_configuration = kwargs.get("feedback_configuration") + self.full_step_resolution = kwargs.get("full_step_resolution") + self.position_display_resolution = kwargs.get( + "position_display_" "resolution" + ) + self.current = kwargs.get("current") + self.voltage = kwargs.get("voltage") + self.units = int(kwargs.get("units")) + self.encoder_resolution = kwargs.get("encoder_resolution") + self.max_acceleration = kwargs.get("max_acceleration") + self.max_velocity = kwargs.get("max_velocity") + self.max_base_velocity = kwargs.get("max_base_velocity") + self.homing_velocity = kwargs.get("homing_velocity") + self.jog_high_velocity = kwargs.get("jog_high_velocity") + self.jog_low_velocity = kwargs.get("jog_low_velocity") + self.acceleration = kwargs.get("acceleration") + self.velocity = kwargs.get("velocity") + self.deceleration = kwargs.get("deceleration") + self.estop_deceleration = kwargs.get("estop_deceleration") + self.jerk = kwargs.get("jerk") + self.error_threshold = kwargs.get("error_threshold") + self.proportional_gain = kwargs.get("proportional_gain") + self.derivative_gain = kwargs.get("derivative_gain") + self.integral_gain = kwargs.get("integral_gain") + self.integral_saturation_gain = kwargs.get("integral_saturation_gain") + self.home = kwargs.get("home") + self.microstep_factor = kwargs.get("microstep_factor") + self.acceleration_feed_forward = kwargs.get("acceleration_feed_forward") + self.trajectory = kwargs.get("trajectory") + self.hardware_limit_configuration = kwargs.get( + "hardware_limit_" "configuration" + ) + if ( + "reduce_motor_torque_time" in kwargs + and "reduce_motor_torque_percentage" in kwargs + ): + motor_time = kwargs["reduce_motor_torque_time"] + motor_time = int(assume_units(motor_time, u.ms).to(u.ms).magnitude) + if motor_time < 0 or motor_time > 60000: + raise ValueError("Time must be between 0 and 60000 ms") + percentage = kwargs["reduce_motor_torque_percentage"] + percentage = int( + assume_units(percentage, u.percent).to(u.percent).magnitude + ) + if percentage < 0 or percentage > 100: + raise ValueError(r"Percentage must be between 0 and 100%") + self._newport_cmd( + "QR", target=self._axis_id, params=[motor_time, percentage] + ) + + # update motor configuration + self._newport_cmd("UF", target=self._axis_id) + self._newport_cmd("QD", target=self._axis_id) + # save configuration + self._newport_cmd("SM") + return self.read_setup() + + def read_setup(self): + """ + Returns dictionary containing: + 'units' + 'motor_type' + 'feedback_configuration' + 'full_step_resolution' + 'position_display_resolution' + 'current' + 'max_velocity' + 'encoder_resolution' + 'acceleration' + 'deceleration' + 'velocity' + 'max_acceleration' + 'homing_velocity' + 'jog_high_velocity' + 'jog_low_velocity' + 'estop_deceleration' + 'jerk' + 'proportional_gain' + 'derivative_gain' + 'integral_gain' + 'integral_saturation_gain' + 'home' + 'microstep_factor' + 'acceleration_feed_forward' + 'trajectory' + 'hardware_limit_configuration' + + :rtype: dict of `pint.Quantity`, float and int + """ + + config = dict() + config["units"] = self.units + config["motor_type"] = self.motor_type + config["feedback_configuration"] = self.feedback_configuration + config["full_step_resolution"] = self.full_step_resolution + config["position_display_resolution"] = self.position_display_resolution + config["current"] = self.current + config["max_velocity"] = self.max_velocity + config["encoder_resolution"] = self.encoder_resolution + config["acceleration"] = self.acceleration + config["deceleration"] = self.deceleration + config["velocity"] = self.velocity + config["max_acceleration"] = self.max_acceleration + config["homing_velocity"] = self.homing_velocity + config["jog_high_velocity"] = self.jog_high_velocity + config["jog_low_velocity"] = self.jog_low_velocity + config["estop_deceleration"] = self.estop_deceleration + config["jerk"] = self.jerk + # config['error_threshold'] = self.error_threshold + config["proportional_gain"] = self.proportional_gain + config["derivative_gain"] = self.derivative_gain + config["integral_gain"] = self.integral_gain + config["integral_saturation_gain"] = self.integral_saturation_gain + config["home"] = self.home + config["microstep_factor"] = self.microstep_factor + config["acceleration_feed_forward"] = self.acceleration_feed_forward + config["trajectory"] = self.trajectory + config["hardware_limit_configuration"] = self.hardware_limit_configuration + return config + + def get_status(self): + """ + Returns Dictionary containing values: + 'units' + 'position' + 'desired_position' + 'desired_velocity' + 'is_motion_done' + + :rtype: dict + """ + status = dict() + status["units"] = self.units + status["position"] = self.position + status["desired_position"] = self.desired_position + status["desired_velocity"] = self.desired_velocity + status["is_motion_done"] = self.is_motion_done + + return status + + @staticmethod + def _get_pq_unit(num): + """ + Gets the units for the specified axis. + + :units: The units for the attached axis + :type num: int + """ + return NewportESP301.Axis._unit_dict[num] + + def _get_unit_num(self, quantity): + """ + Gets the integer label used by the Newport ESP 301 corresponding to a + given `~pint.Quantity`. + + :param pint.Quantity quantity: Units to return a label for. + + :return int: + """ + for num, quant in self._unit_dict.items(): + if quant == quantity: + return num + + raise KeyError(f"{quantity} is not a valid unit for Newport Axis") + + # pylint: disable=protected-access + def _newport_cmd(self, cmd, **kwargs): + """ + Passes the newport command from the axis class to the parent controller + + :param cmd: + :param kwargs: + :return: + """ + return self._controller._newport_cmd(cmd, **kwargs) + + # ENUMS # + + class HomeSearchMode(IntEnum): + + """ + Enum containing different search modes code + """ + + #: Search along specified axes for the +0 position. + zero_position_count = 0 + #: Search for combined Home and Index signals. + home_index_signals = 1 + #: Search only for the Home signal. + home_signal_only = 2 + #: Search for the positive limit signal. + pos_limit_signal = 3 + #: Search for the negative limit signal. + neg_limit_signal = 4 + #: Search for the positive limit and Index signals. + pos_index_signals = 5 + #: Search for the negative limit and Index signals. + neg_index_signals = 6 + + class MotorType(IntEnum): + + """ + Enum for different motor types. + """ + + undefined = 0 + dc_servo = 1 + stepper_motor = 2 + commutated_stepper_motor = 3 + commutated_brushless_servo = 4 + + class Units(IntEnum): + + """ + Enum containing what `units` return means. + """ + + encoder_step = 0 + motor_step = 1 + millimeter = 2 + micrometer = 3 + inches = 4 + milli_inches = 5 + micro_inches = 6 + degree = 7 + gradian = 8 + radian = 9 + milliradian = 10 + microradian = 11 + + # PROPERTIES # @property def axis(self): @@ -118,10 +1230,10 @@ def axis(self): used in the Newport ESP-301 user's manual, and so care must be taken when converting examples. - :type: :class:`NewportESP301Axis` + :type: :class:`NewportESP301.Axis` """ - return ProxyList(self, NewportESP301Axis, range(100)) + return ProxyList(self, self.Axis, range(100)) # return _AxisList(self) # LOW-LEVEL COMMAND METHODS ## @@ -143,12 +1255,12 @@ def _newport_cmd(self, cmd, params=tuple(), target=None, errcheck=True): during ``PGM`` mode. """ query_resp = None - if isinstance(target, NewportESP301Axis): + if isinstance(target, self.Axis): target = target.axis_id raw_cmd = "{target}{cmd}{params}".format( target=target if target is not None else "", cmd=cmd.upper(), - params=",".join(map(str, params)) + params=",".join(map(str, params)), ) if self._execute_immediately: @@ -180,7 +1292,7 @@ def _execute_cmd(self, raw_cmd, errcheck=True): self.sendcmd(raw_cmd) if errcheck: - err_resp = self.query('TB?') + err_resp = self.query("TB?") # pylint: disable=unused-variable code, timestamp, msg = err_resp.split(",") @@ -198,14 +1310,13 @@ def _home(self, axis, search_mode, errcheck=True): the methods in this class and the axis class can both point to the same thing. """ - self._newport_cmd( - "OR", target=axis, params=[search_mode], errcheck=errcheck) + self._newport_cmd("OR", target=axis, params=[search_mode], errcheck=errcheck) def search_for_home( - self, - axis=1, - search_mode=NewportESP301HomeSearchMode.zero_position_count.value, - errcheck=True + self, + axis=1, + search_mode=HomeSearchMode.zero_position_count.value, + errcheck=True, ): """ Searches the specified axis for home using the method specified @@ -213,7 +1324,7 @@ def search_for_home( :param int axis: Axis ID for which home should be searched for. This value is 1-based indexing. - :param NewportESP301HomeSearchMode search_mode: Method to detect when + :param HomeSearchMode search_mode: Method to detect when Home has been found. :param bool errcheck: Boolean to check for errors after each command that is sent to the instrument. @@ -250,8 +1361,9 @@ def define_program(self, program_id): Must be in ``range(1, 101)``. """ if program_id not in range(1, 101): - raise ValueError("Invalid program ID. Must be an integer from " - "1 to 100 (inclusive).") + raise ValueError( + "Invalid program ID. Must be an integer from " "1 to 100 (inclusive)." + ) self._newport_cmd("XX", target=program_id) try: self._newport_cmd("EP", target=program_id) @@ -275,8 +1387,7 @@ def execute_bulk_command(self, errcheck=True): """ self._execute_immediately = False yield - command_string = reduce( - lambda x, y: x + ' ; ' + y + ' ; ', self._command_list) + command_string = reduce(lambda x, y: x + " ; " + y + " ; ", self._command_list) # TODO: is _bulk_query_resp getting back to user? self._bulk_query_resp = self._execute_cmd(command_string, errcheck) self._command_list = [] @@ -289,1135 +1400,7 @@ def run_program(self, program_id): :param int program_id: ID number for previously saved user program """ if program_id not in range(1, 101): - raise ValueError("Invalid program ID. Must be an integer from " - "1 to 100 (inclusive).") - self._newport_cmd("EX", target=program_id) - - -# pylint: disable=too-many-public-methods,too-many-instance-attributes -class NewportESP301Axis(object): - - """ - Encapsulates communication concerning a single axis - of an ESP-301 controller. This class should not be - instantiated by the user directly, but is - returned by `NewportESP301.axis`. - """ - # quantities micro inch - micro_inch = pq.UnitQuantity('micro-inch', pq.inch / 1e6, symbol='uin') - - # Some more work might need to be done here to make - # the encoder_step and motor_step functional - # I really don't have a concrete idea how I'm - # going to do this until I have a physical device - - _unit_dict = { - 0: pq.count, - 1: pq.count, - 2: pq.mm, - 3: pq.um, - 4: pq.inch, - 5: pq.mil, - 6: micro_inch, # compound unit for micro-inch - 7: pq.deg, - 8: pq.grad, - 9: pq.rad, - 10: pq.mrad, - 11: pq.urad, - } - - def __init__(self, controller, axis_id): - if not isinstance(controller, NewportESP301): - raise TypeError("Axis must be controlled by a Newport ESP-301 " - "motor controller.") - - self._controller = controller - self._axis_id = axis_id + 1 - - self._units = self.units - - # CONTEXT MANAGERS ## - - @contextmanager - def _units_of(self, units): - """ - Sets the units for the corresponding axis to a those given by an integer - label (see `NewportESP301Units`), ensuring that the units are properly - reset at the completion of the context manager. - """ - old_units = self._get_units() - self._set_units(units) - yield - self._set_units(old_units) - - # PRIVATE METHODS ## - - def _get_units(self): - """ - Returns the integer label for the current units set for this axis. - - .. seealso:: - NewportESP301Units - """ - return NewportESP301Units( - int(self._newport_cmd("SN?", target=self.axis_id)) - ) - - def _set_units(self, new_units): - return self._newport_cmd( - "SN", - target=self.axis_id, - params=[int(new_units)] - ) - - # PROPERTIES ## - - @property - def axis_id(self): - """ - Get axis number of Newport Controller - - :type: `int` - """ - return self._axis_id - - @property - def is_motion_done(self): - """ - `True` if and only if all motion commands have - completed. This method can be used to wait for - a motion command to complete before sending the next - command. - - :type: `bool` - """ - return bool(int(self._newport_cmd("MD?", target=self.axis_id))) - - @property - def acceleration(self): - """ - Gets/sets the axis acceleration - - :units: As specified (if a `~quantities.Quantity`) or assumed to be - of current newport unit - :type: `~quantities.Quantity` or `float` - """ - - return assume_units( - float(self._newport_cmd("AC?", target=self.axis_id)), - self._units / (pq.s**2) - ) - - @acceleration.setter - def acceleration(self, newval): - if newval is None: - return - newval = float(assume_units(newval, self._units / (pq.s**2)).rescale( - self._units / (pq.s**2)).magnitude) - self._newport_cmd("AC", target=self.axis_id, params=[newval]) - - @property - def deceleration(self): - """ - Gets/sets the axis deceleration - - :units: As specified (if a `~quantities.Quantity`) or assumed to be - of current newport :math:`\\frac{unit}{s^2}` - :type: `~quantities.Quantity` or float - """ - return assume_units( - float(self._newport_cmd("AG?", target=self.axis_id)), - self._units / (pq.s**2) - ) - - @deceleration.setter - def deceleration(self, newval): - if newval is None: - return - newval = float(assume_units(newval, self._units / (pq.s**2)).rescale( - self._units / (pq.s**2)).magnitude) - self._newport_cmd("AG", target=self.axis_id, params=[newval]) - - @property - def estop_deceleration(self): - """ - Gets/sets the axis estop deceleration - - :units: As specified (if a `~quantities.Quantity`) or assumed to be - of current newport :math:`\\frac{unit}{s^2}` - :type: `~quantities.Quantity` or float - """ - return assume_units( - float(self._newport_cmd("AE?", target=self.axis_id)), - self._units / (pq.s**2) - ) - - @estop_deceleration.setter - def estop_deceleration(self, decel): - decel = float(assume_units(decel, self._units / (pq.s**2)).rescale( - self._units / (pq.s**2)).magnitude) - self._newport_cmd("AE", target=self.axis_id, params=[decel]) - - @property - def jerk(self): - """ - Gets/sets the jerk rate for the controller - - :units: As specified (if a `~quantities.Quantity`) or assumed to be - of current newport unit - :type: `~quantities.Quantity` or `float` - """ - - return assume_units( - float(self._newport_cmd("JK?", target=self.axis_id)), - self._units / (pq.s**3) - ) - - @jerk.setter - def jerk(self, jerk): - jerk = float(assume_units(jerk, self._units / (pq.s**3)).rescale( - self._units / (pq.s**3)).magnitude) - self._newport_cmd("JK", target=self.axis_id, params=[jerk]) - - @property - def velocity(self): - """ - Gets/sets the axis velocity - - :units: As specified (if a `~quantities.Quantity`) or assumed to be - of current newport :math:`\\frac{unit}{s}` - :type: `~quantities.Quantity` or `float` - """ - return assume_units( - float(self._newport_cmd("VA?", target=self.axis_id)), - self._units / pq.s - ) - - @velocity.setter - def velocity(self, velocity): - velocity = float(assume_units(velocity, self._units / (pq.s)).rescale( - self._units / pq.s).magnitude) - self._newport_cmd("VA", target=self.axis_id, params=[velocity]) - - @property - def max_velocity(self): - """ - Gets/sets the axis maximum velocity - - :units: As specified (if a `~quantities.Quantity`) or assumed to be - of current newport :math:`\\frac{unit}{s}` - :type: `~quantities.Quantity` or `float` - """ - return assume_units( - float(self._newport_cmd("VU?", target=self.axis_id)), - self._units / pq.s - ) - - @max_velocity.setter - def max_velocity(self, newval): - if newval is None: - return - newval = float(assume_units(newval, self._units / pq.s).rescale( - self._units / pq.s).magnitude) - self._newport_cmd("VU", target=self.axis_id, params=[newval]) - - @property - def max_base_velocity(self): - """ - Gets/sets the maximum base velocity for stepper motors - - :units: As specified (if a `~quantities.Quantity`) or assumed to be - of current newport :math:`\\frac{unit}{s}` - :type: `~quantities.Quantity` or `float` - """ - return assume_units( - float(self._newport_cmd("VB?", target=self.axis_id)), - self._units / pq.s - ) - - @max_base_velocity.setter - def max_base_velocity(self, newval): - if newval is None: - return - newval = float(assume_units(newval, self._units / pq.s).rescale( - self._units / pq.s).magnitude) - self._newport_cmd("VB", target=self.axis_id, params=[newval]) - - @property - def jog_high_velocity(self): - """ - Gets/sets the axis jog high velocity - - :units: As specified (if a `~quantities.Quantity`) or assumed to be - of current newport :math:`\\frac{unit}{s}` - :type: `~quantities.Quantity` or `float` - """ - return assume_units( - float(self._newport_cmd("JH?", target=self.axis_id)), - self._units / pq.s - ) - - @jog_high_velocity.setter - def jog_high_velocity(self, newval): - if newval is None: - return - newval = float(assume_units( - newval, - self._units / pq.s - ).rescale(self._units / pq.s).magnitude) - self._newport_cmd("JH", target=self.axis_id, params=[newval]) - - @property - def jog_low_velocity(self): - """ - Gets/sets the axis jog low velocity - - :units: As specified (if a `~quantities.Quantity`) or assumed to be - of current newport :math:`\\frac{unit}{s}` - :type: `~quantities.Quantity` or `float` - """ - return assume_units( - float(self._newport_cmd("JW?", target=self.axis_id)), - self._units / pq.s - ) - - @jog_low_velocity.setter - def jog_low_velocity(self, newval): - if newval is None: - return - newval = float(assume_units( - newval, - self._units / pq.s - ).rescale(self._units / pq.s).magnitude) - self._newport_cmd("JW", target=self.axis_id, params=[newval]) - - @property - def homing_velocity(self): - """ - Gets/sets the axis homing velocity - - :units: As specified (if a `~quantities.Quantity`) or assumed to be - of current newport :math:`\\frac{unit}{s}` - :type: `~quantities.Quantity` or `float` - """ - return assume_units( - float(self._newport_cmd("OH?", target=self.axis_id)), - self._units / pq.s - ) - - @homing_velocity.setter - def homing_velocity(self, newval): - if newval is None: - return - newval = float(assume_units( - newval, - self._units / pq.s - ).rescale(self._units / pq.s).magnitude) - self._newport_cmd("OH", target=self.axis_id, params=[newval]) - - @property - def max_acceleration(self): - """ - Gets/sets the axis max acceleration - - :units: As specified (if a `~quantities.Quantity`) or assumed to be - of current newport :math:`\\frac{unit}{s^2}` - :type: `~quantities.Quantity` or `float` - """ - return assume_units( - float(self._newport_cmd("AU?", target=self.axis_id)), - self._units / (pq.s**2) - ) - - @max_acceleration.setter - def max_acceleration(self, newval): - if newval is None: - return - newval = float(assume_units(newval, self._units / (pq.s**2)).rescale( - self._units / (pq.s**2)).magnitude) - self._newport_cmd("AU", target=self.axis_id, params=[newval]) - - @property - def max_deceleration(self): - """ - Gets/sets the axis max decceleration. - Max deaceleration is always the same as acceleration. - - :units: As specified (if a `~quantities.Quantity`) or assumed to be - of current newport :math:`\\frac{unit}{s^2}` - :type: `~quantities.Quantity` or `float` - """ - return self.max_acceleration - - @max_deceleration.setter - def max_deceleration(self, decel): - decel = float(assume_units(decel, self._units / (pq.s**2)).rescale( - self._units / (pq.s**2)).magnitude) - self.max_acceleration = decel - - @property - def position(self): - """ - Gets real position on axis in units - - :units: As specified (if a `~quantities.Quantity`) or assumed to be - of current newport unit - :type: `~quantities.Quantity` or `float` - """ - return assume_units( - float(self._newport_cmd("TP?", target=self.axis_id)), - self._units - ) - - @property - def desired_position(self): - """ - Gets desired position on axis in units - - :units: As specified (if a `~quantities.Quantity`) or assumed to be - of current newport unit - :type: `~quantities.Quantity` or `float` - """ - return assume_units( - float(self._newport_cmd("DP?", target=self.axis_id)), - self._units - ) - - @property - def desired_velocity(self): - """ - Gets the axis desired velocity in unit/s - - :units: As specified (if a `~quantities.Quantity`) or assumed to be - of current newport unit/s - :type: `~quantities.Quantity` or `float` - """ - return assume_units( - float(self._newport_cmd("DP?", target=self.axis_id)), - self._units / pq.s - ) - - @property - def home(self): - """ - Gets/sets the axis home position. - Default should be 0 as that sets current position as home - - :units: As specified (if a `~quantities.Quantity`) or assumed to be - of current newport unit - :type: `~quantities.Quantity` or `float` - """ - return assume_units( - float(self._newport_cmd("DH?", target=self.axis_id)), - self._units - ) - - @home.setter - def home(self, newval=0): - if newval is None: - return - newval = float(assume_units(newval, self._units).rescale( - self._units).magnitude) - self._newport_cmd("DH", target=self.axis_id, params=[newval]) - - @property - def units(self): - """ - Get the units that all commands are in reference to. - - :type: `~quantities.Quantity` with units corresponding to - units of axis connected or int which corresponds to Newport - unit number - """ - self._units = self._get_pq_unit(self._get_units()) - return self._units - - @units.setter - def units(self, newval): - if newval is None: - return - if isinstance(newval, int): - self._units = self._get_pq_unit(NewportESP301Units(int(newval))) - elif isinstance(newval, pq.Quantity): - self._units = newval - newval = self._get_unit_num(newval) - self._set_units(newval) - - @property - def encoder_resolution(self): - """ - Gets/sets the resolution of the encode. The minimum number of units - per step. Encoder functionality must be enabled. - - :units: The number of units per encoder step - :type: `~quantities.Quantity` or `float` - """ - - return assume_units( - float(self._newport_cmd("SU?", target=self.axis_id)), - self._units - ) - - @encoder_resolution.setter - def encoder_resolution(self, newval): - if newval is None: - return - newval = float(assume_units(newval, self._units).rescale( - self._units).magnitude) - self._newport_cmd("SU", target=self.axis_id, params=[newval]) - - @property - def full_step_resolution(self): - """ - Gets/sets the axis resolution of the encode. The minimum number of - units per step. Encoder functionality must be enabled. - - :units: The number of units per encoder step - :type: `~quantities.Quantity` or `float` - """ - - return assume_units( - float(self._newport_cmd("FR?", target=self.axis_id)), - self._units - ) - - @full_step_resolution.setter - def full_step_resolution(self, newval): - if newval is None: - return - newval = float(assume_units( - newval, - self._units - ).rescale(self._units).magnitude) - self._newport_cmd("FR", target=self.axis_id, params=[newval]) - - @property - def left_limit(self): - """ - Gets/sets the axis left travel limit - - :units: The limit in units - :type: `~quantities.Quantity` or `float` - """ - return assume_units( - float(self._newport_cmd("SL?", target=self.axis_id)), - self._units - ) - - @left_limit.setter - def left_limit(self, limit): - limit = float(assume_units(limit, self._units).rescale( - self._units).magnitude) - self._newport_cmd("SL", target=self.axis_id, params=[limit]) - - @property - def right_limit(self): - """ - Gets/sets the axis right travel limit - - :units: units - :type: `~quantities.Quantity` or `float` - """ - return assume_units( - float(self._newport_cmd("SR?", target=self.axis_id)), - self._units - ) - - @right_limit.setter - def right_limit(self, limit): - limit = float(assume_units(limit, self._units).rescale( - self._units).magnitude) - self._newport_cmd("SR", target=self.axis_id, params=[limit]) - - @property - def error_threshold(self): - """ - Gets/sets the axis error threshold - - :units: units - :type: `~quantities.Quantity` or `float` - """ - return assume_units( - float(self._newport_cmd("FE?", target=self.axis_id)), - self._units - ) - - @error_threshold.setter - def error_threshold(self, newval): - if newval is None: - return - newval = float(assume_units( - newval, - self._units - ).rescale(self._units).magnitude) - self._newport_cmd("FE", target=self.axis_id, params=[newval]) - - @property - def current(self): - """ - Gets/sets the axis current (amps) - - :units: As specified (if a `~quantities.Quantity`) or assumed to be - of current newport :math:`\\text{A}` - :type: `~quantities.Quantity` or `float` - """ - return assume_units( - float(self._newport_cmd("QI?", target=self.axis_id)), - pq.A - ) - - @current.setter - def current(self, newval): - if newval is None: - return - current = float(assume_units(newval, pq.A).rescale( - pq.A).magnitude) - self._newport_cmd("QI", target=self.axis_id, params=[current]) - - @property - def voltage(self): - """ - Gets/sets the axis voltage - - :units: As specified (if a `~quantities.Quantity`) or assumed to be - of current newport :math:`\\text{V}` - :type: `~quantities.Quantity` or `float` - """ - return assume_units( - float(self._newport_cmd("QV?", target=self.axis_id)), - pq.V - ) - - @voltage.setter - def voltage(self, newval): - if newval is None: - return - voltage = float(assume_units(newval, pq.V).rescale( - pq.V).magnitude) - self._newport_cmd("QV", target=self.axis_id, params=[voltage]) - - @property - def motor_type(self): - """ - Gets/sets the axis motor type - * 0 = undefined - * 1 = DC Servo - * 2 = Stepper motor - * 3 = commutated stepper motor - * 4 = commutated brushless servo motor - - :type: `int` - :rtype: `NewportESP301MotorType` - """ - return NewportESP301MotorType(int(self._newport_cmd( - "QM?", - target=self._axis_id - ))) - - @motor_type.setter - def motor_type(self, newval): - if newval is None: - return - self._newport_cmd("QM", target=self._axis_id, params=[int(newval)]) - - @property - def feedback_configuration(self): - """ - Gets/sets the axis Feedback configuration - - :type: `int` - """ - return int(self._newport_cmd("ZB?", target=self._axis_id)[:-2], 16) - - @feedback_configuration.setter - def feedback_configuration(self, newval): - if newval is None: - return - self._newport_cmd("ZB", target=self._axis_id, params=[int(newval)]) - - @property - def position_display_resolution(self): - """ - Gets/sets the position display resolution - - :type: `int` - """ - return int(self._newport_cmd("FP?", target=self._axis_id)) - - @position_display_resolution.setter - def position_display_resolution(self, newval): - if newval is None: - return - self._newport_cmd("FP", target=self._axis_id, params=[int(newval)]) - - @property - def trajectory(self): - """ - Gets/sets the axis trajectory - - :type: `int` - """ - return int(self._newport_cmd("TJ?", target=self._axis_id)) - - @trajectory.setter - def trajectory(self, newval): - if newval is None: - return - self._newport_cmd("TJ", target=self._axis_id, params=[int(newval)]) - - @property - def microstep_factor(self): - """ - Gets/sets the axis microstep_factor - - :type: `int` - """ - return int(self._newport_cmd("QS?", target=self._axis_id)) - - @microstep_factor.setter - def microstep_factor(self, newval): - if newval is None: - return - newval = int(newval) - if newval < 1 or newval > 250: - raise ValueError("Microstep factor must be between 1 and 250") - else: - self._newport_cmd( - "QS", - target=self._axis_id, - params=[newval] + raise ValueError( + "Invalid program ID. Must be an integer from " "1 to 100 (inclusive)." ) - - @property - def hardware_limit_configuration(self): - """ - Gets/sets the axis hardware_limit_configuration - - :type: `int` - """ - return int(self._newport_cmd("ZH?", target=self._axis_id)[:-2]) - - @hardware_limit_configuration.setter - def hardware_limit_configuration(self, newval): - if newval is None: - return - self._newport_cmd("ZH", target=self._axis_id, params=[int(newval)]) - - @property - def acceleration_feed_forward(self): - """ - Gets/sets the axis acceleration_feed_forward setting - - :type: `int` - """ - return float(self._newport_cmd("AF?", target=self._axis_id)) - - @acceleration_feed_forward.setter - def acceleration_feed_forward(self, newval): - if newval is None: - return - self._newport_cmd("AF", target=self._axis_id, params=[float(newval)]) - - @property - def proportional_gain(self): - """ - Gets/sets the axis proportional_gain - - :type: `float` - """ - return float(self._newport_cmd("KP?", target=self._axis_id)[:-1]) - - @proportional_gain.setter - def proportional_gain(self, newval): - if newval is None: - return - self._newport_cmd("KP", target=self._axis_id, params=[float(newval)]) - - @property - def derivative_gain(self): - """ - Gets/sets the axis derivative_gain - - :type: `float` - """ - return float(self._newport_cmd("KD?", target=self._axis_id)) - - @derivative_gain.setter - def derivative_gain(self, newval): - if newval is None: - return - self._newport_cmd("KD", target=self._axis_id, params=[float(newval)]) - - @property - def integral_gain(self): - """ - Gets/sets the axis integral_gain - - :type: `float` - """ - return float(self._newport_cmd("KI?", target=self._axis_id)) - - @integral_gain.setter - def integral_gain(self, newval): - if newval is None: - return - self._newport_cmd("KI", target=self._axis_id, params=[float(newval)]) - - @property - def integral_saturation_gain(self): - """ - Gets/sets the axis integral_saturation_gain - - :type: `float` - """ - return float(self._newport_cmd("KS?", target=self._axis_id)) - - @integral_saturation_gain.setter - def integral_saturation_gain(self, newval): - if newval is None: - return - self._newport_cmd("KS", target=self._axis_id, params=[float(newval)]) - - @property - def encoder_position(self): - """ - Gets the encoder position - - :type: - """ - with self._units_of(NewportESP301Units.encoder_step): - return self.position - - # MOVEMENT METHODS # - - def search_for_home( - self, - search_mode=NewportESP301HomeSearchMode.zero_position_count.value - ): - """ - Searches this axis only - for home using the method specified by ``search_mode``. - - :param NewportESP301HomeSearchMode search_mode: Method to detect when - Home has been found. - """ - self._controller.search_for_home(axis=self.axis_id, search_mode=search_mode) - - def move(self, position, absolute=True, wait=False, block=False): - """ - :param position: Position to set move to along this axis. - :type position: `float` or :class:`~quantities.Quantity` - :param bool absolute: If `True`, the position ``pos`` is - interpreted as relative to the zero-point of the encoder. - If `False`, ``pos`` is interpreted as relative to the current - position of this axis. - :param bool wait: If True, will tell axis to not execute other - commands until movement is finished - :param bool block: If True, will block code until movement is finished - """ - position = float(assume_units(position, self._units).rescale( - self._units).magnitude) - if absolute: - self._newport_cmd("PA", params=[position], target=self.axis_id) - else: - self._newport_cmd("PR", params=[position], target=self.axis_id) - - if wait: - self.wait_for_position(position) - if block: - sleep(0.003) - mot = self.is_motion_done - while not mot: - mot = self.is_motion_done - - def move_to_hardware_limit(self): - """ - move to hardware travel limit - """ - self._newport_cmd("MT", target=self.axis_id) - - def move_indefinitely(self): - """ - Move until told to stop - """ - self._newport_cmd("MV", target=self.axis_id) - - def abort_motion(self): - """ - Abort motion - """ - self._newport_cmd("AB", target=self.axis_id) - - def wait_for_stop(self): - """ - Waits for axis motion to stop before next command is executed - """ - self._newport_cmd("WS", target=self.axis_id) - - def stop_motion(self): - """ - Stop all motion on axis. With programmed deceleration rate - """ - try: - self._newport_cmd("ST", target=self.axis_id) - except NewportError as e: - raise NewportError(e) - - def wait_for_position(self, position): - """ - Wait for axis to reach position before executing next command - - :param position: Position to wait for on axis - - :type position: float or :class:`~quantities.Quantity` - """ - position = float(assume_units(position, self._units).rescale( - self._units).magnitude) - self._newport_cmd( - "WP", target=self.axis_id, params=[position]) - - def wait_for_motion(self, poll_interval=0.01, max_wait=None): - """ - Blocks until all movement along this axis is complete, as reported - by `~NewportESP301Axis.is_motion_done`. - - :param float poll_interval: How long (in seconds) to sleep between - checking if the motion is complete. - :param float max_wait: Maximum amount of time to wait before - raising a `IOError`. If `None`, this method will wait - indefinitely. - """ - # FIXME: make sure that the controller is not in - # programming mode, or else this might not work. - # In programming mode, the "WS" command should be - # sent instead, and the two parameters to this method should - # be ignored. - poll_interval = float(assume_units(poll_interval, pq.s).rescale( - pq.s).magnitude) - max_wait = float(assume_units(max_wait, pq.s).rescale( - pq.s).magnitude) - tic = time() - while True: - if self.is_motion_done: - return - else: - if max_wait is None or (time() - tic) < max_wait: - sleep(poll_interval) - else: - raise IOError("Timed out waiting for motion to finish.") - - def enable(self): - """ - Turns motor axis on. - """ - self._newport_cmd("MO", target=self._axis_id) - - def disable(self): - """ - Turns motor axis off. - """ - self._newport_cmd("MF", target=self._axis_id) - - def setup_axis(self, **kwargs): - """ - Setup a non-newport DC servo motor stage. Necessary parameters are. - - * 'motor_type' = type of motor see 'QM' in Newport documentation - * 'current' = motor maximum current (A) - * 'voltage' = motor voltage (V) - * 'units' = set units (see NewportESP301Units)(U) - * 'encoder_resolution' = value of encoder step in terms of (U) - * 'max_velocity' = maximum velocity (U/s) - * 'max_base_velocity' = maximum working velocity (U/s) - * 'homing_velocity' = homing speed (U/s) - * 'jog_high_velocity' = jog high speed (U/s) - * 'jog_low_velocity' = jog low speed (U/s) - * 'max_acceleration' = maximum acceleration (U/s^2) - * 'acceleration' = acceleration (U/s^2) - * 'deceleration' = set deceleration (U/s^2) - * 'error_threshold' = set error threshold (U) - * 'proportional_gain' = PID proportional gain (optional) - * 'derivative_gain' = PID derivative gain (optional) - * 'interal_gain' = PID internal gain (optional) - * 'integral_saturation_gain' = PID integral saturation (optional) - * 'trajectory' = trajectory mode (optional) - * 'position_display_resolution' (U per step) - * 'feedback_configuration' - * 'full_step_resolution' = (U per step) - * 'home' = (U) - * 'acceleration_feed_forward' = bewtween 0 to 2e9 - * 'reduce_motor_torque' = (time(ms),percentage) - """ - - self.motor_type = kwargs.get('motor_type') - self.feedback_configuration = kwargs.get('feedback_configuration') - self.full_step_resolution = kwargs.get('full_step_resolution') - self.position_display_resolution = kwargs.get('position_display_' - 'resolution') - self.current = kwargs.get('current') - self.voltage = kwargs.get('voltage') - self.units = int(kwargs.get('units')) - self.encoder_resolution = kwargs.get('encoder_resolution') - self.max_acceleration = kwargs.get('max_acceleration') - self.max_velocity = kwargs.get('max_velocity') - self.max_base_velocity = kwargs.get('max_base_velocity') - self.homing_velocity = kwargs.get('homing_velocity') - self.jog_high_velocity = kwargs.get('jog_high_velocity') - self.jog_low_velocity = kwargs.get('jog_low_velocity') - self.acceleration = kwargs.get('acceleration') - self.velocity = kwargs.get('velocity') - self.deceleration = kwargs.get('deceleration') - self.estop_deceleration = kwargs.get('estop_deceleration') - self.jerk = kwargs.get('jerk') - self.error_threshold = kwargs.get('error_threshold') - self.proportional_gain = kwargs.get('proportional_gain') - self.derivative_gain = kwargs.get('derivative_gain') - self.integral_gain = kwargs.get('integral_gain') - self.integral_saturation_gain = kwargs.get('integral_saturation_gain') - self.home = kwargs.get('home') - self.microstep_factor = kwargs.get('microstep_factor') - self.acceleration_feed_forward = kwargs.get('acceleration_feed_forward') - self.trajectory = kwargs.get('trajectory') - self.hardware_limit_configuration = kwargs.get('hardware_limit_' - 'configuration') - if 'reduce_motor_torque_time' in kwargs and 'reduce_motor_torque_percentage' in kwargs: - motor_time = kwargs['reduce_motor_torque_time'] - motor_time = int(assume_units(motor_time, pq.ms).rescale(pq.ms).magnitude) - if motor_time < 0 or motor_time > 60000: - raise ValueError("Time must be between 0 and 60000 ms") - percentage = kwargs['reduce_motor_torque_percentage'] - percentage = int(assume_units(percentage, pq.percent).rescale( - pq.percent).magnitude) - if percentage < 0 or percentage > 100: - raise ValueError("Time must be between 0 and 60000 ms") - self._newport_cmd( - "QR", target=self._axis_id, params=[motor_time, percentage]) - - # update motor configuration - self._newport_cmd("UF", target=self._axis_id) - self._newport_cmd("QD", target=self._axis_id) - # save configuration - self._newport_cmd("SM") - return self.read_setup() - - def read_setup(self): - """ - Returns dictionary containing: - 'units' - 'motor_type' - 'feedback_configuration' - 'full_step_resolution' - 'position_display_resolution' - 'current' - 'max_velocity' - 'encoder_resolution' - 'acceleration' - 'deceleration' - 'velocity' - 'max_acceleration' - 'homing_velocity' - 'jog_high_velocity' - 'jog_low_velocity' - 'estop_deceleration' - 'jerk' - 'proportional_gain' - 'derivative_gain' - 'integral_gain' - 'integral_saturation_gain' - 'home' - 'microstep_factor' - 'acceleration_feed_forward' - 'trajectory' - 'hardware_limit_configuration' - - :rtype: dict of `quantities.Quantity`, float and int - """ - - config = dict() - config['units'] = self.units - config['motor_type'] = self.motor_type - config['feedback_configuration'] = self.feedback_configuration - config['full_step_resolution'] = self.full_step_resolution - config[ - 'position_display_resolution'] = self.position_display_resolution - config['current'] = self.current - config['max_velocity'] = self.max_velocity - config['encoder_resolution'] = self.encoder_resolution - config['acceleration'] = self.acceleration - config['deceleration'] = self.deceleration - config['velocity'] = self.velocity - config['max_acceleration'] = self.max_acceleration - config['homing_velocity'] = self.homing_velocity - config['jog_high_velocity'] = self.jog_high_velocity - config['jog_low_velocity'] = self.jog_low_velocity - config['estop_deceleration'] = self.estop_deceleration - config['jerk'] = self.jerk - # config['error_threshold'] = self.error_threshold - config['proportional_gain'] = self.proportional_gain - config['derivative_gain'] = self.derivative_gain - config['integral_gain'] = self.integral_gain - config['integral_saturation_gain'] = self.integral_saturation_gain - config['home'] = self.home - config['microstep_factor'] = self.microstep_factor - config['acceleration_feed_forward'] = self.acceleration_feed_forward - config['trajectory'] = self.trajectory - config[ - 'hardware_limit_configuration'] = self.hardware_limit_configuration - return config - - def get_status(self): - """ - Returns Dictionary containing values: - 'units' - 'position' - 'desired_position' - 'desired_velocity' - 'is_motion_done' - - :rtype: dict - """ - status = dict() - status['units'] = self.units - status['position'] = self.position - status['desired_position'] = self.desired_position - status['desired_velocity'] = self.desired_velocity - status['is_motion_done'] = self.is_motion_done - - return status - - @staticmethod - def _get_pq_unit(num): - """ - Gets the units for the specified axis. - - :units: The units for the attached axis - :type num: int - """ - return NewportESP301Axis._unit_dict[num] - - def _get_unit_num(self, quantity): - """ - Gets the integer label used by the Newport ESP 301 corresponding to a - given `~quantities.Quantity`. - - :param quantities.Quantity quantity: Units to return a label for. - - :return int: - """ - for num, quant in self._unit_dict.items(): - if quant == quantity: - return num - - raise KeyError( - "{0} is not a valid unit for Newport Axis".format(quantity)) - - # pylint: disable=protected-access - def _newport_cmd(self, cmd, **kwargs): - """ - Passes the newport command from the axis class to the parent controller - - :param cmd: - :param kwargs: - :return: - """ - return self._controller._newport_cmd(cmd, **kwargs) + self._newport_cmd("EX", target=program_id) diff --git a/instruments/ondax/__init__.py b/instruments/ondax/__init__.py index 5b59b2cb7..4e232d1f9 100644 --- a/instruments/ondax/__init__.py +++ b/instruments/ondax/__init__.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing Ondax Instruments """ -from __future__ import absolute_import from .lm import LM diff --git a/instruments/ondax/lm.py b/instruments/ondax/lm.py index 11070253a..92b7950eb 100644 --- a/instruments/ondax/lm.py +++ b/instruments/ondax/lm.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides the support for the Ondax LM Laser. @@ -8,12 +7,10 @@ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division from enum import IntEnum -import quantities as pq +from instruments.units import ureg as u from instruments.abstract_instruments import Instrument from instruments.util_fns import convert_temperature, assume_units @@ -31,7 +28,7 @@ class LM(Instrument): """ def __init__(self, filelike): - super(LM, self).__init__(filelike) + super().__init__(filelike) self.terminator = "\r" self.apc = self._AutomaticPowerControl(self) self.acc = self._AutomaticCurrentControl(self) @@ -44,6 +41,7 @@ class Status(IntEnum): """ Enum containing the valid states of the laser """ + normal = 1 inner_modulation = 2 power_scan = 3 @@ -55,7 +53,7 @@ class Status(IntEnum): # INNER CLASSES # - class _AutomaticCurrentControl(object): + class _AutomaticCurrentControl: """ Options and functions related to the laser diode's automatic current control driver. @@ -63,6 +61,7 @@ class _AutomaticCurrentControl(object): .. warning:: This class is not designed to be accessed directly. It should be interfaced via `LM.acc` """ + def __init__(self, parent): self._parent = parent self._enabled = False @@ -82,10 +81,10 @@ def target(self): :return: Current ACC of the Laser :units: mA - :type: `~quantities.Quantity` + :type: `~pint.Quantity` """ response = float(self._parent.query("rstli?")) - return response*pq.mA + return response * u.mA @property def enabled(self): @@ -108,8 +107,10 @@ def enabled(self): @enabled.setter def enabled(self, newval): if not isinstance(newval, bool): - raise TypeError("ACC driver enabled property must be specified" - "with a boolean, got {}.".format(type(newval))) + raise TypeError( + "ACC driver enabled property must be specified" + "with a boolean, got {}.".format(type(newval)) + ) if newval: self._parent.sendcmd("lcen") else: @@ -144,7 +145,7 @@ def off(self): """ self._parent.sendcmd("lcoff") - class _AutomaticPowerControl(object): + class _AutomaticPowerControl: """ Options and functions related to the laser diode's automatic power control driver. @@ -152,6 +153,7 @@ class _AutomaticPowerControl(object): .. warning:: This class is not designed to be accessed directly. It should be interfaced via `LM.apc` """ + def __init__(self, parent): self._parent = parent self._enabled = False @@ -171,10 +173,10 @@ def target(self): :return: the target laser power :units: mW - :type: `~quantities.Quantities` + :type: `~pint.Quantity` """ response = self._parent.query("rslp?") - return float(response)*pq.mW + return float(response) * u.mW @property def enabled(self): @@ -197,8 +199,10 @@ def enabled(self): @enabled.setter def enabled(self, newval): if not isinstance(newval, bool): - raise TypeError("APC driver enabled property must be specified " - "with a boolean, got {}.".format(type(newval))) + raise TypeError( + "APC driver enabled property must be specified " + "with a boolean, got {}.".format(type(newval)) + ) if newval: self._parent.sendcmd("len") else: @@ -233,13 +237,14 @@ def stop(self): """ self._parent.sendcmd("cps") - class _Modulation(object): + class _Modulation: """ Options and functions related to the laser's optical output modulation. .. warning:: This class is not designed to be accessed directly. It should be interfaced via `LM.modulation` """ + def __init__(self, parent): self._parent = parent self._enabled = False @@ -254,23 +259,23 @@ def on_time(self): Example usage: >>> import instruments as ik - >>> import quantities as pq + >>> import instruments.units as u >>> laser = ik.ondax.LM.open_serial('/dev/ttyUSB0', baud=1234) >>> print(laser.modulation.on_time) - >>> laser.modulation.on_time = 1 * pq.ms + >>> laser.modulation.on_time = 1 * u.ms :return: The TTL modulation on time - :units: As specified (if a `~quantities.Quantity`) or assumed + :units: As specified (if a `~pint.Quantity`) or assumed to be of units milliseconds. - :type: `~quantities.Quantity` + :type: `~pint.Quantity` """ response = self._parent.query("stsont?") - return float(response)*pq.ms + return float(response) * u.ms @on_time.setter def on_time(self, newval): - newval = assume_units(newval, pq.ms).rescale(pq.ms).magnitude - self._parent.sendcmd("stsont:"+str(newval)) + newval = assume_units(newval, u.ms).to(u.ms).magnitude + self._parent.sendcmd("stsont:" + str(newval)) @property def off_time(self): @@ -282,23 +287,23 @@ def off_time(self): Example usage: >>> import instruments as ik - >>> import quantities as pq + >>> import instruments.units as u >>> laser = ik.ondax.LM.open_serial('/dev/ttyUSB0', baud=1234) >>> print(laser.modulation.on_time) - >>> laser.modulation.on_time = 1 * pq.ms + >>> laser.modulation.on_time = 1 * u.ms :return: The TTL modulation off time. - :units: As specified (if a `~quantities.Quantity`) or assumed + :units: As specified (if a `~pint.Quantity`) or assumed to be of units milliseconds. - :type: `~quantities.Quantity` + :type: `~pint.Quantity` """ response = self._parent.query("stsofft?") - return float(response)*pq.ms + return float(response) * u.ms @off_time.setter def off_time(self, newval): - newval = assume_units(newval, pq.ms).rescale(pq.ms).magnitude - self._parent.sendcmd("stsofft:"+str(newval)) + newval = assume_units(newval, u.ms).to(u.ms).magnitude + self._parent.sendcmd("stsofft:" + str(newval)) @property def enabled(self): @@ -321,15 +326,17 @@ def enabled(self): @enabled.setter def enabled(self, newval): if not isinstance(newval, bool): - raise TypeError("Modulation enabled property must be specified " - "with a boolean, got {}.".format(type(newval))) + raise TypeError( + "Modulation enabled property must be specified " + "with a boolean, got {}.".format(type(newval)) + ) if newval: self._parent.sendcmd("stm") else: self._parent.sendcmd("ctm") self._enabled = newval - class _ThermoElectricCooler(object): + class _ThermoElectricCooler: """ Options and functions relating to the laser diode's thermo electric cooler. @@ -337,6 +344,7 @@ class _ThermoElectricCooler(object): .. warning:: This class is not designed to be accessed directly. It should be interfaced via `LM.tec` """ + def __init__(self, parent): self._parent = parent self._enabled = False @@ -355,10 +363,10 @@ def current(self): >>> print(laser.tec.current) :units: mA - :type: `~quantities.Quantity` + :type: `~pint.Quantity` """ response = self._parent.query("rti?") - return float(response)*pq.mA + return float(response) * u.mA @property def target(self): @@ -374,10 +382,10 @@ def target(self): >>> print(laser.tec.target) :units: Degrees Celcius - :type: `~quantities.Quantity` + :type: `~pint.Quantity` """ response = self._parent.query("rstt?") - return float(response)*pq.degC + return u.Quantity(float(response), u.degC) @property def enabled(self): @@ -400,8 +408,10 @@ def enabled(self): @enabled.setter def enabled(self, newval): if not isinstance(newval, bool): - raise TypeError("TEC enabled property must be specified with " - "a boolean, got {}.".format(type(newval))) + raise TypeError( + "TEC enabled property must be specified with " + "a boolean, got {}.".format(type(newval)) + ) if newval: self._parent.sendcmd("tecon") else: @@ -411,8 +421,8 @@ def enabled(self, newval): def _ack_expected(self, msg=""): if msg.find("?") > 0: return None - else: - return "OK" + + return "OK" @property def firmware(self): @@ -429,17 +439,17 @@ def current(self): """ Gets/sets the laser diode current, in mA. - :units: As specified (if a `~quantities.Quantity`) or assumed + :units: As specified (if a `~pint.Quantity`) or assumed to be of units mA. - :type: `~quantities.Quantity` + :type: `~pint.Quantity` """ response = self.query("rli?") - return float(response)*pq.mA + return float(response) * u.mA @current.setter def current(self, newval): - newval = assume_units(newval, pq.mA).rescale(pq.mA).magnitude - self.sendcmd("slc:"+str(newval)) + newval = assume_units(newval, u.mA).to(u.mA).magnitude + self.sendcmd("slc:" + str(newval)) @property def maximum_current(self): @@ -447,16 +457,16 @@ def maximum_current(self): Get/Set the maximum laser diode current in mA. If the current is set over the limit, the laser will shut down. - :units: As specified (if a `~quantities.Quantity`) or assumed + :units: As specified (if a `~pint.Quantity`) or assumed to be of units mA. - :type: `~quantities.Quantity` + :type: `~pint.Quantity` """ response = self.query("rlcm?") - return float(response)*pq.mA + return float(response) * u.mA @maximum_current.setter def maximum_current(self, newval): - newval = assume_units(newval, pq.mA).rescale('mA').magnitude + newval = assume_units(newval, u.mA).to("mA").magnitude self.sendcmd("smlc:" + str(newval)) @property @@ -464,17 +474,17 @@ def power(self): """ Get/Set the laser's optical power in mW. - :units: As specified (if a `~quantities.Quantity`) or assumed + :units: As specified (if a `~pint.Quantity`) or assumed to be of units mW. - :rtype: `~quantities.Quantity` + :rtype: `~pint.Quantity` """ response = self.query("rlp?") - return float(response)*pq.mW + return float(response) * u.mW @power.setter def power(self, newval): - newval = assume_units(newval, pq.mW).rescale(pq.mW).magnitude - self.sendcmd("slp:"+str(newval)) + newval = assume_units(newval, u.mW).to(u.mW).magnitude + self.sendcmd("slp:" + str(newval)) @property def serial_number(self): @@ -501,17 +511,17 @@ def temperature(self): """ Gets/sets laser diode temperature. - :units: As specified (if a `~quantities.Quantity`) or assumed + :units: As specified (if a `~pint.Quantity`) or assumed to be of units degrees celcius. - :type: `~quantities.Quantity` + :type: `~pint.Quantity` """ response = self.query("rtt?") - return float(response)*pq.degC + return u.Quantity(float(response), u.degC) @temperature.setter def temperature(self, newval): - newval = convert_temperature(newval, pq.degC).magnitude - self.sendcmd("stt:"+str(newval)) + newval = convert_temperature(newval, u.degC).magnitude + self.sendcmd("stt:" + str(newval)) @property def enabled(self): @@ -525,8 +535,10 @@ def enabled(self): @enabled.setter def enabled(self, newval): if not isinstance(newval, bool): - raise TypeError("Laser module enabled property must be specified " - "with a boolean, got {}.".format(type(newval))) + raise TypeError( + "Laser module enabled property must be specified " + "with a boolean, got {}.".format(type(newval)) + ) if newval: self.sendcmd("lon") else: diff --git a/instruments/optional_dep_finder.py b/instruments/optional_dep_finder.py new file mode 100644 index 000000000..106f7507d --- /dev/null +++ b/instruments/optional_dep_finder.py @@ -0,0 +1,12 @@ +""" +Small module to obtain handles to optional dependencies +""" + +# pylint: disable=unused-import +try: + import numpy + + _numpy_installed = True +except ImportError: + numpy = None + _numpy_installed = False diff --git a/instruments/oxford/__init__.py b/instruments/oxford/__init__.py index a56fb0fbf..94cbe8a67 100644 --- a/instruments/oxford/__init__.py +++ b/instruments/oxford/__init__.py @@ -1,10 +1,7 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing Oxford instruments """ -from __future__ import absolute_import - from .oxforditc503 import OxfordITC503 diff --git a/instruments/oxford/oxforditc503.py b/instruments/oxford/oxforditc503.py index ab64ebcff..b54c8cd75 100644 --- a/instruments/oxford/oxforditc503.py +++ b/instruments/oxford/oxforditc503.py @@ -1,18 +1,12 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides support for the Oxford ITC 503 temperature controller. """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division -from builtins import range - -import quantities as pq - from instruments.abstract_instruments import Instrument +from instruments.units import ureg as u from instruments.util_fns import ProxyList # CLASSES ##################################################################### @@ -32,13 +26,13 @@ class OxfordITC503(Instrument): """ def __init__(self, filelike): - super(OxfordITC503, self).__init__(filelike) + super().__init__(filelike) self.terminator = "\r" - self.sendcmd('C3') # Enable remote commands + self.sendcmd("C3") # Enable remote commands # INNER CLASSES # - class Sensor(object): + class Sensor: """ Class representing a probe sensor on the Oxford ITC 503. @@ -59,10 +53,10 @@ def temperature(self): Read the temperature of the attached probe to the specified channel. :units: Kelvin - :type: `~quantities.quantity.Quantity` + :type: `~pint.Quantity` """ - value = float(self._parent.query('R{}'.format(self._idx))[1:]) - return pq.Quantity(value, pq.Kelvin) + value = float(self._parent.query(f"R{self._idx}")[1:]) + return u.Quantity(value, u.kelvin) # PROPERTIES # diff --git a/instruments/phasematrix/__init__.py b/instruments/phasematrix/__init__.py index c48687914..8417858a2 100644 --- a/instruments/phasematrix/__init__.py +++ b/instruments/phasematrix/__init__.py @@ -1,9 +1,7 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing Phase Matrix instruments """ -from __future__ import absolute_import from .phasematrix_fsw0020 import PhaseMatrixFSW0020 diff --git a/instruments/phasematrix/phasematrix_fsw0020.py b/instruments/phasematrix/phasematrix_fsw0020.py index ba4a7f917..ac1cb91b9 100644 --- a/instruments/phasematrix/phasematrix_fsw0020.py +++ b/instruments/phasematrix/phasematrix_fsw0020.py @@ -1,19 +1,14 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides support for the Phase Matrix FSW0020 signal generator. """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division - -from quantities import GHz from instruments.abstract_instruments.signal_generator import SingleChannelSG +from instruments.units import ureg as u from instruments.util_fns import assume_units -from instruments.units import dBm, cBm, mHz # CLASSES ##################################################################### @@ -27,9 +22,9 @@ class PhaseMatrixFSW0020(SingleChannelSG): Example:: >>> import instruments as ik - >>> import quantities as pq + >>> import instruments.units as u >>> inst = ik.phasematrix.PhaseMatrixFSW0020.open_serial("/dev/ttyUSB0", baud=115200) - >>> inst.frequency = 1 * pq.GHz + >>> inst.frequency = 1 * u.GHz >>> inst.power = 0 * ik.units.dBm # Can omit units and will assume dBm >>> inst.output = True """ @@ -40,7 +35,7 @@ def reset(self): Note that no commands will be accepted by the generator for at least :math:`5 \mu\text{s}`. """ - self.sendcmd('0E.') + self.sendcmd("0E.") @property def frequency(self): @@ -49,20 +44,20 @@ def frequency(self): If units are not specified, the frequency is assumed to be in gigahertz (GHz). - :type: `~quantities.Quantity` + :type: `~pint.Quantity` :units: frequency, assumed to be GHz """ - return (int(self.query('04.'), 16) * mHz).rescale(GHz) + return (int(self.query("04."), 16) * u.mHz).to(u.GHz) @frequency.setter def frequency(self, newval): # Rescale the input to millihertz as demanded by the signal # generator, then convert to an integer. - newval = int(assume_units(newval, GHz).rescale(mHz).magnitude) + newval = int(assume_units(newval, u.GHz).to(u.mHz).magnitude) # Write the integer to the serial port in ASCII-encoded # uppercase-hexadecimal format, with padding to 12 nybbles. - self.sendcmd('0C{:012X}.'.format(newval)) + self.sendcmd(f"0C{newval:012X}.") # No return data, so no readline needed. @@ -73,10 +68,10 @@ def power(self): If units are not specified, the power is assumed to be in decibel-milliwatts (dBm). - :type: `~quantities.Quantity` + :type: `~pint.Quantity` :units: log-power, assumed to be dBm """ - return (int(self.query('0D.'), 16) * cBm).rescale(dBm) + return u.Quantity((int(self.query("0D."), 16)), u.cBm).to(u.dBm) @power.setter def power(self, newval): @@ -85,10 +80,10 @@ def power(self, newval): # The Phase Matrix unit speaks in units of centibel-milliwats, # so convert and take the integer part. - newval = int(assume_units(newval, dBm).rescale(cBm).magnitude) + newval = int(assume_units(newval, u.dBm).to(u.cBm).magnitude) # Command code 0x03, parameter length 2 bytes (4 nybbles) - self.sendcmd('03{:04X}.'.format(newval)) + self.sendcmd(f"03{newval:04X}.") @property def phase(self): @@ -109,7 +104,7 @@ def blanking(self): @blanking.setter def blanking(self, newval): - self.sendcmd('05{:02X}.'.format(1 if newval else 0)) + self.sendcmd(f"05{1 if newval else 0:02X}.") @property def ref_output(self): @@ -122,7 +117,7 @@ def ref_output(self): @ref_output.setter def ref_output(self, newval): - self.sendcmd('08{:02X}.'.format(1 if newval else 0)) + self.sendcmd(f"08{1 if newval else 0:02X}.") @property def output(self): @@ -136,7 +131,7 @@ def output(self): @output.setter def output(self, newval): - self.sendcmd('0F{:02X}.'.format(1 if newval else 0)) + self.sendcmd(f"0F{1 if newval else 0:02X}.") @property def pulse_modulation(self): @@ -149,7 +144,7 @@ def pulse_modulation(self): @pulse_modulation.setter def pulse_modulation(self, newval): - self.sendcmd('09{:02X}.'.format(1 if newval else 0)) + self.sendcmd(f"09{1 if newval else 0:02X}.") @property def am_modulation(self): @@ -162,4 +157,4 @@ def am_modulation(self): @am_modulation.setter def am_modulation(self, newval): - self.sendcmd('0A{:02X}.'.format(1 if newval else 0)) + self.sendcmd(f"0A{1 if newval else 0:02X}.") diff --git a/instruments/picowatt/__init__.py b/instruments/picowatt/__init__.py index 296865e91..28afb8b71 100644 --- a/instruments/picowatt/__init__.py +++ b/instruments/picowatt/__init__.py @@ -1,9 +1,7 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing Picowatt instruments """ -from __future__ import absolute_import from .picowattavs47 import PicowattAVS47 diff --git a/instruments/picowatt/picowattavs47.py b/instruments/picowatt/picowattavs47.py index c255b8d28..8be9675d5 100644 --- a/instruments/picowatt/picowattavs47.py +++ b/instruments/picowatt/picowattavs47.py @@ -1,22 +1,15 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides support for the Picowatt AVS 47 resistance bridge """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division - -from builtins import range from enum import IntEnum -import quantities as pq - from instruments.generic_scpi import SCPIInstrument -from instruments.util_fns import (enum_property, bool_property, int_property, - ProxyList) +from instruments.units import ureg as u +from instruments.util_fns import enum_property, bool_property, int_property, ProxyList # CLASSES ##################################################################### @@ -35,12 +28,12 @@ class PicowattAVS47(SCPIInstrument): """ def __init__(self, filelike): - super(PicowattAVS47, self).__init__(filelike) + super().__init__(filelike) self.sendcmd("HDR 0") # Disables response headers from replies # INNER CLASSES # - class Sensor(object): + class Sensor: """ Class representing a sensor on the PicowattAVS47 @@ -60,7 +53,7 @@ def resistance(self): reading is up to date by first sending the "ADC" command. :units: :math:`\\Omega` (ohms) - :rtype: `~quantities.Quantity` + :rtype: `~pint.Quantity` """ # First make sure the mux is on the correct channel if self._parent.mux_channel != self._idx: @@ -69,7 +62,7 @@ def resistance(self): self._parent.input_source = self._parent.InputSource.actual # Next, prep a measurement with the ADC command self._parent.sendcmd("ADC") - return float(self._parent.query("RES?")) * pq.ohm + return float(self._parent.query("RES?")) * u.ohm # ENUMS # @@ -77,6 +70,7 @@ class InputSource(IntEnum): """ Enum containing valid input source modes for the AVS 47 """ + ground = 0 actual = 1 reference = 2 @@ -97,7 +91,7 @@ def sensor(self): return ProxyList(self, PicowattAVS47.Sensor, range(8)) remote = bool_property( - name="REM", + command="REM", inst_true="1", inst_false="0", doc=""" @@ -107,22 +101,22 @@ def sensor(self): interface and locks-out the front panel. :type: `bool` - """ + """, ) input_source = enum_property( - name="INP", + command="INP", enum=InputSource, input_decoration=int, doc=""" Gets/sets the input source. :type: `PicowattAVS47.InputSource` - """ + """, ) mux_channel = int_property( - name="MUX", + command="MUX", doc=""" Gets/sets the multiplexer sensor number. It is recommended that you ground the input before switching the @@ -132,11 +126,11 @@ def sensor(self): :type: `int` """, - valid_set=range(8) + valid_set=range(8), ) excitation = int_property( - name="EXC", + command="EXC", doc=""" Gets/sets the excitation sensor number. @@ -144,11 +138,11 @@ def sensor(self): :type: `int` """, - valid_set=range(8) + valid_set=range(8), ) display = int_property( - name="DIS", + command="DIS", doc=""" Gets/sets the sensor that is displayed on the front panel. @@ -156,5 +150,5 @@ def sensor(self): :type: `int` """, - valid_set=range(8) + valid_set=range(8), ) diff --git a/instruments/qubitekk/__init__.py b/instruments/qubitekk/__init__.py index e71003a05..107d9d946 100644 --- a/instruments/qubitekk/__init__.py +++ b/instruments/qubitekk/__init__.py @@ -1,10 +1,8 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing Qubitekk instruments """ -from __future__ import absolute_import from .cc1 import CC1 from .mc1 import MC1 diff --git a/instruments/qubitekk/cc1.py b/instruments/qubitekk/cc1.py index b6074755a..3f229a74a 100644 --- a/instruments/qubitekk/cc1.py +++ b/instruments/qubitekk/cc1.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides support for the Qubitekk CC1 Coincidence Counter instrument. @@ -8,17 +7,11 @@ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division -from builtins import range, map - from enum import Enum -import quantities as pq from instruments.generic_scpi.scpi_instrument import SCPIInstrument -from instruments.util_fns import ( - ProxyList, assume_units, split_unit_str -) +from instruments.units import ureg as u +from instruments.util_fns import ProxyList, assume_units, split_unit_str # CLASSES ##################################################################### @@ -39,7 +32,7 @@ class CC1(SCPIInstrument): """ def __init__(self, filelike): - super(CC1, self).__init__(filelike) + super().__init__(filelike) self.terminator = "\n" self._channel_count = 3 self._firmware = None @@ -69,8 +62,11 @@ def __init__(self, filelike): self.TriggerMode = self._TriggerModeOld def _ack_expected(self, msg=""): - return msg if self._ack_on and self.firmware[0] >= 2 and \ - self.firmware[1] > 1 else None + return ( + msg + if self._ack_on and self.firmware[0] >= 2 and self.firmware[1] > 1 + else None + ) # ENUMS # @@ -78,6 +74,7 @@ class _TriggerModeNew(Enum): """ Enum containing valid trigger modes for the CC1 """ + continuous = "MODE CONT" start_stop = "MODE STOP" @@ -85,22 +82,19 @@ class _TriggerModeOld(Enum): """ Enum containing valid trigger modes for the CC1 """ + continuous = "0" start_stop = "1" # INNER CLASSES # - class Channel(object): + class Channel: """ Class representing a channel on the Qubitekk CC1. """ - __CHANNEL_NAMES = { - 1: 'C1', - 2: 'C2', - 3: 'CO' - } + __CHANNEL_NAMES = {1: "C1", 2: "C2", 3: "CO"} def __init__(self, cc1, idx): self._cc1 = cc1 @@ -119,19 +113,23 @@ def count(self): :rtype: `int` """ - count = self._cc1.query("COUN:{0}?".format(self._chan)) - # FIXME: Does this property actually work? The try block seems - # wrong. + count = self._cc1.query(f"COUN:{self._chan}?") + tries = 5 try: count = int(count) - except ValueError: # pragma: no cover + except ValueError: count = None - while count is None: + while count is None and tries > 0: # try to read again try: count = int(self._cc1.read(-1)) except ValueError: count = None + tries -= 1 + + if tries == 0: + raise OSError(f"Could not read the count of channel " f"{self._chan}.") + self._count = count return self._count @@ -160,8 +158,9 @@ def acknowledge(self, new_val): self.sendcmd(":ACKN ON") self._ack_on = True else: - raise NotImplementedError("Acknowledge message not implemented in " - "this version.") + raise NotImplementedError( + "Acknowledge message not implemented in " "this version." + ) @property def gate(self): @@ -175,8 +174,7 @@ def gate(self): @gate.setter def gate(self, newval): if not isinstance(newval, bool): - raise TypeError("Bool properties must be specified with a " - "boolean value") + raise TypeError("Bool properties must be specified with a " "boolean value") self.sendcmd( self._set_fmt.format("GATE", self._bool[0] if newval else self._bool[1]) ) @@ -193,8 +191,7 @@ def subtract(self): @subtract.setter def subtract(self, newval): if not isinstance(newval, bool): - raise TypeError("Bool properties must be specified with a " - "boolean value") + raise TypeError("Bool properties must be specified with a " "boolean value") self.sendcmd( self._set_fmt.format("SUBT", self._bool[0] if newval else self._bool[1]) ) @@ -225,19 +222,19 @@ def window(self): """ Gets/sets the length of the coincidence window between the two signals. - :units: As specified (if a `~quantities.Quantity`) or assumed to be + :units: As specified (if a `~pint.Quantity`) or assumed to be of units nanoseconds. - :type: `~quantities.Quantity` + :type: `~pint.Quantity` """ - return pq.Quantity(*split_unit_str(self.query("WIND?"), "ns")) + return u.Quantity(*split_unit_str(self.query("WIND?"), "ns")) @window.setter def window(self, new_val): - new_val_mag = int(assume_units(new_val, pq.ns).rescale(pq.ns).magnitude) + new_val_mag = int(assume_units(new_val, u.ns).to(u.ns).magnitude) if new_val_mag < 0 or new_val_mag > 7: raise ValueError("Window is out of range.") # window must be an integer! - self.sendcmd(":WIND {}".format(new_val_mag)) + self.sendcmd(f":WIND {new_val_mag}") @property def delay(self): @@ -246,19 +243,19 @@ def delay(self): When setting, ``N`` may be ``0, 2, 4, 6, 8, 10, 12, or 14ns``. - :rtype: quantities.ns + :rtype: `~pint.Quantity` :return: the delay value """ - return pq.Quantity(*split_unit_str(self.query("DELA?"), "ns")) + return u.Quantity(*split_unit_str(self.query("DELA?"), "ns")) @delay.setter def delay(self, new_val): - new_val = assume_units(new_val, pq.ns).rescale(pq.ns) - if new_val < 0*pq.ns or new_val > 14*pq.ns: + new_val = assume_units(new_val, u.ns).to(u.ns) + if new_val < 0 * u.ns or new_val > 14 * u.ns: raise ValueError("New delay value is out of bounds.") if new_val.magnitude % 2 != 0: raise ValueError("New magnitude must be an even number") - self.sendcmd(":DELA "+str(int(new_val.magnitude))) + self.sendcmd(":DELA " + str(int(new_val.magnitude))) @property def dwell_time(self): @@ -266,24 +263,24 @@ def dwell_time(self): Gets/sets the length of time before a clear signal is sent to the counters. - :units: As specified (if a `~quantities.Quantity`) or assumed to be + :units: As specified (if a `~pint.Quantity`) or assumed to be of units seconds. - :type: `~quantities.Quantity` + :type: `~pint.Quantity` """ # the older versions of the firmware erroneously report the units of the # dwell time as being seconds rather than ms - dwell_time = pq.Quantity(*split_unit_str(self.query("DWEL?"), "s")) + dwell_time = u.Quantity(*split_unit_str(self.query("DWEL?"), "s")) if self.firmware[0] <= 2 and self.firmware[1] <= 1: - return dwell_time/1000.0 - else: - return dwell_time + return dwell_time / 1000.0 + + return dwell_time @dwell_time.setter def dwell_time(self, new_val): - new_val_mag = assume_units(new_val, pq.s).rescale(pq.s).magnitude + new_val_mag = assume_units(new_val, u.s).to(u.s).magnitude if new_val_mag < 0: raise ValueError("Dwell time cannot be negative.") - self.sendcmd(":DWEL {}".format(new_val_mag)) + self.sendcmd(f":DWEL {new_val_mag}") @property def firmware(self): @@ -303,7 +300,7 @@ def firmware(self): else: value = self._firmware.replace("Firmware v", "").split(".") if len(value) < 3: - for _ in range(3-len(value)): + for _ in range(3 - len(value)): value.append(0) value = tuple(map(int, value)) self._firmware = value diff --git a/instruments/qubitekk/mc1.py b/instruments/qubitekk/mc1.py index 98b353962..c95fa56b6 100644 --- a/instruments/qubitekk/mc1.py +++ b/instruments/qubitekk/mc1.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides support for the Qubitekk MC1 Motor Controller. @@ -8,16 +7,15 @@ # IMPORTS ##################################################################### -from __future__ import absolute_import, division - -from builtins import range, map from enum import Enum -import quantities as pq - from instruments.abstract_instruments import Instrument +from instruments.units import ureg as u from instruments.util_fns import ( - int_property, enum_property, unitful_property, assume_units + int_property, + enum_property, + unitful_property, + assume_units, ) # CLASSES ##################################################################### @@ -28,12 +26,13 @@ class MC1(Instrument): The MC1 is a controller for the qubitekk motor controller. Used with a linear actuator to perform a HOM dip. """ + def __init__(self, filelike): - super(MC1, self).__init__(filelike) + super().__init__(filelike) self.terminator = "\r" - self._increment = 1*pq.ms - self._lower_limit = -300*pq.ms - self._upper_limit = 300*pq.ms + self._increment = 1 * u.ms + self._lower_limit = -300 * u.ms + self._upper_limit = 300 * u.ms self._firmware = None self._controller = None @@ -43,6 +42,7 @@ class MotorType(Enum): """ Enum for the motor types for the MC1 """ + radio = "Radio" relay = "Relay" @@ -54,13 +54,13 @@ def increment(self): Gets/sets the stepping increment value of the motor controller :units: As specified, or assumed to be of units milliseconds - :type: `~quantities.Quantity` + :type: `~pint.Quantity` """ return self._increment @increment.setter def increment(self, newval): - self._increment = assume_units(newval, pq.ms).rescale(pq.ms) + self._increment = assume_units(newval, u.ms).to(u.ms) @property def lower_limit(self): @@ -68,13 +68,13 @@ def lower_limit(self): Gets/sets the stepping lower limit value of the motor controller :units: As specified, or assumed to be of units milliseconds - :type: `~quantities.Quantity` + :type: `~pint.Quantity` """ return self._lower_limit @lower_limit.setter def lower_limit(self, newval): - self._lower_limit = assume_units(newval, pq.ms).rescale(pq.ms) + self._lower_limit = assume_units(newval, u.ms).to(u.ms) @property def upper_limit(self): @@ -82,40 +82,40 @@ def upper_limit(self): Gets/sets the stepping upper limit value of the motor controller :units: As specified, or assumed to be of units milliseconds - :type: `~quantities.Quantity` + :type: `~pint.Quantity` """ return self._upper_limit @upper_limit.setter def upper_limit(self, newval): - self._upper_limit = assume_units(newval, pq.ms).rescale(pq.ms) + self._upper_limit = assume_units(newval, u.ms).to(u.ms) direction = unitful_property( - name="DIRE", + command="DIRE", doc=""" Get the internal direction variable, which is a function of how far the motor needs to go. - :type: `~quantities.Quantity` + :type: `~pint.Quantity` :units: milliseconds """, - units=pq.ms, - readonly=True + units=u.ms, + readonly=True, ) inertia = unitful_property( - name="INER", + command="INER", doc=""" Gets/Sets the amount of force required to overcome static inertia. Must be between 0 and 100 milliseconds. - :type: `~quantities.Quantity` + :type: `~pint.Quantity` :units: milliseconds """, - format_code='{:.0f}', - units=pq.ms, - valid_range=(0*pq.ms, 100*pq.ms), - set_fmt=":{} {}" + format_code="{:.0f}", + units=u.ms, + valid_range=(0 * u.ms, 100 * u.ms), + set_fmt=":{} {}", ) @property @@ -126,26 +126,26 @@ def internal_position(self): the positive direction minus the number of milliseconds that voltage has been applied to the motor in the negative direction. - :type: `~quantities.Quantity` + :type: `~pint.Quantity` :units: milliseconds """ - response = int(self.query("POSI?"))*self.step_size + response = int(self.query("POSI?")) * self.step_size return response metric_position = unitful_property( - name="METR", + command="METR", doc=""" Get the estimated motor position, in millimeters. - :type: `~quantities.Quantity` + :type: `~pint.Quantity` :units: millimeters """, - units=pq.mm, - readonly=True + units=u.mm, + readonly=True, ) setting = int_property( - name="OUTP", + command="OUTP", doc=""" Gets/sets the output port of the optical switch. 0 means input 1 is directed to output 1, and input 2 is directed to output 2. 1 means that @@ -154,22 +154,22 @@ def internal_position(self): :type: `int` """, valid_set=range(2), - set_fmt=":{} {}" + set_fmt=":{} {}", ) step_size = unitful_property( - name="STEP", + command="STEP", doc=""" Gets/Sets the number of milliseconds per step. Must be between 1 and 100 milliseconds. - :type: `~quantities.Quantity` + :type: `~pint.Quantity` :units: milliseconds """, - format_code='{:.0f}', - units=pq.ms, - valid_range=(1*pq.ms, 100*pq.ms), - set_fmt=":{} {}" + format_code="{:.0f}", + units=u.ms, + valid_range=(1 * u.ms, 100 * u.ms), + set_fmt=":{} {}", ) @property @@ -187,19 +187,19 @@ def firmware(self): self._firmware = self.query("FIRM?") value = self._firmware.split(".") if len(value) < 3: - for _ in range(3-len(value)): + for _ in range(3 - len(value)): value.append(0) value = tuple(map(int, value)) self._firmware = value return self._firmware controller = enum_property( - 'MOTO', + "MOTO", MotorType, doc=""" Get the motor controller type. """, - readonly=True + readonly=True, ) @property @@ -208,11 +208,11 @@ def move_timeout(self): Get the motor's timeout value, which indicates the number of milliseconds before the motor can start moving again. - :type: `~quantities.Quantity` + :type: `~pint.Quantity` :units: milliseconds """ response = int(self.query("TIME?")) - return response*self.step_size + return response * self.step_size # METHODS # @@ -244,12 +244,12 @@ def move(self, new_position): the number of motor steps. It varies between motors. :param new_position: the location - :type new_position: `~quantities.Quantity` + :type new_position: `~pint.Quantity` """ + new_position = assume_units(new_position, u.ms).to(u.ms) if self.lower_limit <= new_position <= self.upper_limit: - new_position = assume_units(new_position, pq.ms).rescale(pq.ms) - clock_cycles = new_position/self.step_size - cmd = ":MOVE "+str(int(clock_cycles)) + clock_cycles = new_position / self.step_size + cmd = f":MOVE {int(clock_cycles)}" self.sendcmd(cmd) else: raise ValueError("Location out of range") diff --git a/instruments/rigol/__init__.py b/instruments/rigol/__init__.py index ebe8bdd04..150631a28 100644 --- a/instruments/rigol/__init__.py +++ b/instruments/rigol/__init__.py @@ -1,9 +1,7 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing Rigol instruments """ -from __future__ import absolute_import from .rigolds1000 import RigolDS1000Series diff --git a/instruments/rigol/rigolds1000.py b/instruments/rigol/rigolds1000.py index 4f23ae5e5..9e4416ffe 100644 --- a/instruments/rigol/rigolds1000.py +++ b/instruments/rigol/rigolds1000.py @@ -1,20 +1,13 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides support for Rigol DS-1000 series oscilloscopes. """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division -from builtins import range - from enum import Enum -from instruments.abstract_instruments import ( - Oscilloscope, OscilloscopeChannel, OscilloscopeDataSource -) +from instruments.abstract_instruments import Oscilloscope from instruments.generic_scpi import SCPIInstrument from instruments.util_fns import ProxyList, bool_property, enum_property @@ -37,21 +30,14 @@ class AcquisitionType(Enum): """ Enum containing valid acquisition types for the Rigol DS1000 """ + normal = "NORM" average = "AVER" peak_detect = "PEAK" - class Coupling(Enum): - """ - Enum containing valid coupling modes for the Rigol DS1000 - """ - ac = "AC" - dc = "DC" - ground = "GND" - # INNER CLASSES # - class DataSource(OscilloscopeDataSource): + class DataSource(Oscilloscope.DataSource): """ Class representing a data source (channel, math, or ref) on the Rigol DS1000 @@ -60,9 +46,6 @@ class DataSource(OscilloscopeDataSource): is designed to be initialized by the `RigolDS1000Series` class. """ - def __init__(self, parent, name): - super(RigolDS1000Series.DataSource, self).__init__(parent, name) - @property def name(self): return self._name @@ -70,14 +53,16 @@ def name(self): def read_waveform(self, bin_format=True): # TODO: add DIG, FFT. if self.name not in ["CHAN1", "CHAN2", "DIG", "MATH", "FFT"]: - raise NotImplementedError("Rigol DS1000 series does not " - "supportreading waveforms from " - "{}.".format(self.name)) - self._parent.sendcmd(":WAV:DATA? {}".format(self.name)) + raise NotImplementedError( + "Rigol DS1000 series does not " + "supportreading waveforms from " + "{}.".format(self.name) + ) + self._parent.sendcmd(f":WAV:DATA? {self.name}") data = self._parent.binblockread(2) # TODO: check width return data - class Channel(DataSource, OscilloscopeChannel): + class Channel(DataSource, Oscilloscope.Channel): """ Class representing a channel on the Rigol DS1000. @@ -86,13 +71,24 @@ class Channel(DataSource, OscilloscopeChannel): .. warning:: This class should NOT be manually created by the user. It is designed to be initialized by the `RigolDS1000Series` class. """ + + class Coupling(Enum): + """ + Enum containing valid coupling modes for the Rigol DS1000 + """ + + ac = "AC" + dc = "DC" + ground = "GND" + def __init__(self, parent, idx): self._parent = parent self._idx = idx + 1 # Rigols are 1-based. # Initialize as a data source with name CHAN{}. super(RigolDS1000Series.Channel, self).__init__( - self._parent, "CHAN{}".format(self._idx)) + self._parent, f"CHAN{self._idx}" + ) def sendcmd(self, cmd): """ @@ -101,7 +97,7 @@ def sendcmd(self, cmd): :param str cmd: The command string to send to the instrument """ - self._parent.sendcmd(":CHAN{}:{}".format(self._idx, cmd)) + self._parent.sendcmd(f":CHAN{self._idx}:{cmd}") def query(self, cmd): """ @@ -112,23 +108,23 @@ def query(self, cmd): :return: The result as returned by the instrument :rtype: `str` """ - return self._parent.query(":CHAN{}:{}".format(self._idx, cmd)) + return self._parent.query(f":CHAN{self._idx}:{cmd}") - coupling = enum_property("COUP", lambda: RigolDS1000Series.Coupling) + coupling = enum_property("COUP", Coupling) - bw_limit = bool_property("BWL", "ON", "OFF") - display = bool_property("DISP", "ON", "OFF") - invert = bool_property("INV", "ON", "OFF") + bw_limit = bool_property("BWL", inst_true="ON", inst_false="OFF") + display = bool_property("DISP", inst_true="ON", inst_false="OFF") + invert = bool_property("INV", inst_true="ON", inst_false="OFF") # TODO: :CHAN:OFFset # TODO: :CHAN:PROBe # TODO: :CHAN:SCALe - filter = bool_property("FILT", "ON", "OFF") + filter = bool_property("FILT", inst_true="ON", inst_false="OFF") # TODO: :CHAN:MEMoryDepth - vernier = bool_property("VERN", "ON", "OFF") + vernier = bool_property("VERN", inst_true="ON", inst_false="OFF") # PROPERTIES # @@ -162,12 +158,12 @@ def acquire_averages(self): @acquire_averages.setter def acquire_averages(self, newval): - if newval not in [2**i for i in range(1, 9)]: + if newval not in [2 ** i for i in range(1, 9)]: raise ValueError( "Number of averages {} not supported by instrument; " "must be a power of 2 from 2 to 256.".format(newval) ) - self.sendcmd(":ACQ:AVER {}".format(newval)) + self.sendcmd(f":ACQ:AVER {newval}") # TODO: implement :ACQ:SAMP in a meaningful way. This should probably be # under Channel, and needs to be unitful. @@ -200,7 +196,7 @@ def stop(self): # # Many of the :KEY: commands are not yet implemented as methods. - panel_locked = bool_property(":KEY:LOCK", "ON", "OFF") + panel_locked = bool_property(":KEY:LOCK", inst_true="ENAB", inst_false="DIS") def release_panel(self): # TODO: better name? diff --git a/instruments/srs/__init__.py b/instruments/srs/__init__.py index 3854c870b..dcdf1a05b 100644 --- a/instruments/srs/__init__.py +++ b/instruments/srs/__init__.py @@ -1,10 +1,8 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing Lakeshore instruments """ -from __future__ import absolute_import from .srs345 import SRS345 from .srs830 import SRS830 diff --git a/instruments/srs/srs345.py b/instruments/srs/srs345.py index a344a4b5d..080a47cea 100644 --- a/instruments/srs/srs345.py +++ b/instruments/srs/srs345.py @@ -1,17 +1,14 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides support for the SRS 345 function generator. """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division from enum import IntEnum -import quantities as pq +from instruments.units import ureg as u from instruments.abstract_instruments import FunctionGenerator from instruments.generic_scpi import SCPIInstrument @@ -28,12 +25,13 @@ class SRS345(SCPIInstrument, FunctionGenerator): Example usage: >>> import instruments as ik - >>> import quantities as pq + >>> import instruments.units as u >>> srs = ik.srs.SRS345.open_gpib('/dev/ttyUSB0', 1) - >>> srs.frequency = 1 * pq.MHz + >>> srs.frequency = 1 * u.MHz >>> print(srs.offset) >>> srs.function = srs.Function.triangle """ + # FIXME: need to add OUTX 1 here, but doing so seems to cause a syntax # error on the instrument. @@ -41,26 +39,21 @@ class SRS345(SCPIInstrument, FunctionGenerator): _UNIT_MNEMONICS = { FunctionGenerator.VoltageMode.peak_to_peak: "VP", - FunctionGenerator.VoltageMode.rms: "VR", - FunctionGenerator.VoltageMode.dBm: "DB", + FunctionGenerator.VoltageMode.rms: "VR", + FunctionGenerator.VoltageMode.dBm: "DB", } - _MNEMONIC_UNITS = dict((mnem, unit) - for unit, mnem in _UNIT_MNEMONICS.items()) + _MNEMONIC_UNITS = {mnem: unit for unit, mnem in _UNIT_MNEMONICS.items()} # FunctionGenerator CONTRACT # def _get_amplitude_(self): resp = self.query("AMPL?").strip() - return ( - float(resp[:-2]), - self._MNEMONIC_UNITS[resp[-2:]] - ) + return (float(resp[:-2]), self._MNEMONIC_UNITS[resp[-2:]]) def _set_amplitude_(self, magnitude, units): - self.sendcmd( - "AMPL {}{}".format(magnitude, self._UNIT_MNEMONICS[units])) + self.sendcmd(f"AMPL {magnitude}{self._UNIT_MNEMONICS[units]}") # ENUMS ## @@ -68,6 +61,7 @@ class Function(IntEnum): """ Enum containing valid output function modes for the SRS 345 """ + sinusoid = 0 square = 1 triangle = 2 @@ -78,46 +72,46 @@ class Function(IntEnum): # PROPERTIES ## frequency = unitful_property( - name="FREQ", - units=pq.Hz, + command="FREQ", + units=u.Hz, doc=""" Gets/sets the output frequency. :units: As specified, or assumed to be :math:`\\text{Hz}` otherwise. - :type: `float` or `~quantities.quantity.Quantity` - """ + :type: `float` or `~pint.Quantity` + """, ) function = enum_property( - name="FUNC", + command="FUNC", enum=Function, input_decoration=int, doc=""" Gets/sets the output function of the function generator. :type: `~SRS345.Function` - """ + """, ) offset = unitful_property( - name="OFFS", - units=pq.volt, + command="OFFS", + units=u.volt, doc=""" Gets/sets the offset voltage for the output waveform. :units: As specified, or assumed to be :math:`\\text{V}` otherwise. - :type: `float` or `~quantities.quantity.Quantity` - """ + :type: `float` or `~pint.Quantity` + """, ) phase = unitful_property( - name="PHSE", - units=pq.degree, + command="PHSE", + units=u.degree, doc=""" Gets/sets the phase for the output waveform. :units: As specified, or assumed to be degrees (:math:`{}^{\\circ}`) otherwise. - :type: `float` or `~quantities.quantity.Quantity` - """ + :type: `float` or `~pint.Quantity` + """, ) diff --git a/instruments/srs/srs830.py b/instruments/srs/srs830.py index f533d0b9e..64df94776 100644 --- a/instruments/srs/srs830.py +++ b/instruments/srs/srs830.py @@ -1,37 +1,34 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides support for the SRS 830 lock-in amplifier. """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division import math import time import warnings - -from builtins import range, map from enum import Enum, IntEnum -import numpy as np -import quantities as pq - -from instruments.generic_scpi import SCPIInstrument from instruments.abstract_instruments.comm import ( GPIBCommunicator, SerialCommunicator, - LoopbackCommunicator + LoopbackCommunicator, ) +from instruments.generic_scpi import SCPIInstrument +from instruments.optional_dep_finder import numpy +from instruments.units import ureg as u from instruments.util_fns import ( - bool_property, bounded_unitful_property, enum_property, unitful_property + bool_property, + bounded_unitful_property, + enum_property, + unitful_property, ) # CONSTANTS ################################################################### -VALID_SAMPLE_RATES = [2.0**n for n in range(-4, 10)] +VALID_SAMPLE_RATES = [2.0 ** n for n in range(-4, 10)] VALID_SAMPLE_RATES += ["trigger"] # CLASSES ##################################################################### @@ -45,13 +42,13 @@ class SRS830(SCPIInstrument): Example usage: >>> import instruments as ik - >>> import quantities as pq + >>> import instruments.units as u >>> srs = ik.srs.SRS830.open_gpibusb('/dev/ttyUSB0', 1) - >>> srs.frequency = 1000 * pq.hertz # Lock-In frequency + >>> srs.frequency = 1000 * u.hertz # Lock-In frequency >>> data = srs.take_measurement(1, 10) # 1Hz sample rate, 10 samples total """ - def __init__(self, filelike, outx_mode=None): # pragma: no cover + def __init__(self, filelike, outx_mode=None): """ Class initialization method. @@ -60,10 +57,10 @@ def __init__(self, filelike, outx_mode=None): # pragma: no cover by the SRS830 manual. If left default, the correct ``OUTX`` command will be sent depending on what type of communicator self._file is. """ - super(SRS830, self).__init__(filelike) - if outx_mode is 1: + super().__init__(filelike) + if outx_mode == 1: self.sendcmd("OUTX 1") - elif outx_mode is 2: + elif outx_mode == 2: self.sendcmd("OUTX 2") else: if isinstance(self._file, GPIBCommunicator): @@ -73,8 +70,12 @@ def __init__(self, filelike, outx_mode=None): # pragma: no cover elif isinstance(self._file, LoopbackCommunicator): pass else: - warnings.warn("OUTX command has not been set. Instrument " - "behavour is unknown.", UserWarning) + warnings.warn( + "OUTX command has not been set. Instrument " + "behaviour is unknown.", + UserWarning, + ) + # ENUMS # class FreqSource(IntEnum): @@ -82,6 +83,7 @@ class FreqSource(IntEnum): """ Enum for the SRS830 frequency source settings. """ + external = 0 internal = 1 @@ -90,6 +92,7 @@ class Coupling(IntEnum): """ Enum for the SRS830 channel coupling settings. """ + ac = 0 dc = 1 @@ -97,6 +100,7 @@ class BufferMode(IntEnum): """ Enum for the SRS830 buffer modes. """ + one_shot = 0 loop = 1 @@ -104,6 +108,7 @@ class Mode(Enum): """ Enum containing valid modes for the SRS 830 """ + x = "x" y = "y" r = "r" @@ -134,50 +139,50 @@ class Mode(Enum): or uses the internal reference. :type: `SRS830.FreqSource` - """ + """, ) frequency = unitful_property( "FREQ", - pq.hertz, + u.hertz, valid_range=(0, None), doc=""" Gets/sets the lock-in amplifier reference frequency. - :units: As specified (if a `~quantities.Quantity`) or assumed to be + :units: As specified (if a `~pint.Quantity`) or assumed to be of units Hertz. - :type: `~quantities.Quantity` with units Hertz. - """ + :type: `~pint.Quantity` with units Hertz. + """, ) phase, phase_min, phase_max = bounded_unitful_property( "PHAS", - pq.degrees, - valid_range=(-360 * pq.degrees, 730 * pq.degrees), + u.degrees, + valid_range=(-360 * u.degrees, 730 * u.degrees), doc=""" Gets/set the phase of the internal reference signal. Set value should be -360deg <= newval < +730deg. - :units: As specified (if a `~quantities.Quantity`) or assumed to be + :units: As specified (if a `~pint.Quantity`) or assumed to be of units degrees. - :type: `~quantities.Quantity` with units degrees. - """ + :type: `~pint.Quantity` with units degrees. + """, ) amplitude, amplitude_min, amplitude_max = bounded_unitful_property( "SLVL", - pq.volt, - valid_range=(0.004 * pq.volt, 5 * pq.volt), + u.volt, + valid_range=(0.004 * u.volt, 5 * u.volt), doc=""" Gets/set the amplitude of the internal reference signal. Set value should be 0.004 <= newval <= 5.000 - :units: As specified (if a `~quantities.Quantity`) or assumed to be + :units: As specified (if a `~pint.Quantity`) or assumed to be of units volts. Value should be specified as peak-to-peak. - :type: `~quantities.Quantity` with units volts peak-to-peak. - """ + :type: `~pint.Quantity` with units volts peak-to-peak. + """, ) input_shield_ground = bool_property( @@ -188,7 +193,7 @@ class Mode(Enum): Function sets the input shield grounding to either 'float' or 'ground'. :type: `bool` - """ + """, ) coupling = enum_property( @@ -199,7 +204,7 @@ class Mode(Enum): Gets/sets the input coupling to either 'ac' or 'dc'. :type: `SRS830.Coupling` - """ + """, ) @property @@ -210,12 +215,12 @@ def sample_rate(self): Acceptable set values are :math:`2^n` where :math:`n \in \{-4...+9\}` or the string `trigger`. - :type: `~quantities.Quantity` with units Hertz. + :type: `~pint.Quantity` with units Hertz. """ - value = int(self.query('SRAT?')) + value = int(self.query("SRAT?")) if value == 14: return "trigger" - return pq.Quantity(VALID_SAMPLE_RATES[value], pq.Hz) + return u.Quantity(VALID_SAMPLE_RATES[value], u.Hz) @sample_rate.setter def sample_rate(self, newval): @@ -223,10 +228,12 @@ def sample_rate(self, newval): newval = newval.lower() if newval in VALID_SAMPLE_RATES: - self.sendcmd('SRAT {}'.format(VALID_SAMPLE_RATES.index(newval))) + self.sendcmd(f"SRAT {VALID_SAMPLE_RATES.index(newval)}") else: - raise ValueError('Valid samples rates given by {} ' - 'and "trigger".'.format(VALID_SAMPLE_RATES)) + raise ValueError( + "Valid samples rates given by {} " + 'and "trigger".'.format(VALID_SAMPLE_RATES) + ) buffer_mode = enum_property( "SEND", @@ -240,7 +247,7 @@ def sample_rate(self, newval): will repeat from the start. :type: `SRS830.BufferMode` - """ + """, ) @property @@ -253,12 +260,11 @@ def num_data_points(self): resp = None i = 0 while not resp and i < 10: - resp = self.query('SPTS?').strip() + resp = self.query("SPTS?").strip() i += 1 - if not resp: # pragma: no cover - raise IOError( - "Expected integer response from instrument, got {}".format( - repr(resp)) + if not resp: + raise OSError( + f"Expected integer response from instrument, got {repr(resp)}" ) return int(resp) @@ -274,7 +280,7 @@ def num_data_points(self): other, FAST1, is for legacy systems which this package does not support. :type: `bool` - """ + """, ) # AUTO- METHODS # @@ -295,11 +301,11 @@ def auto_offset(self, mode): mode = SRS830.Mode[mode] if mode not in self._XYR_MODE_MAP: - raise ValueError('Specified mode not valid for this function.') + raise ValueError("Specified mode not valid for this function.") mode = self._XYR_MODE_MAP[mode] - self.sendcmd('AOFF {}'.format(mode)) + self.sendcmd(f"AOFF {mode}") def auto_phase(self): """ @@ -309,7 +315,7 @@ def auto_phase(self): Do not send this message again without waiting the correct amount of time for the lock-in to finish. """ - self.sendcmd('APHS') + self.sendcmd("APHS") # META-METHODS # @@ -321,7 +327,7 @@ def init(self, sample_rate, buffer_mode): :param sample_rate: The desired sampling rate. Acceptable set values are :math:`2^n` where :math:`n \in \{-4...+9\}` in units Hertz or the string `trigger`. - :type sample_rate: `~quantities.Quantity` or `str` + :type sample_rate: `~pint.Quantity` or `str` :param `SRS830.BufferMode` buffer_mode: This sets the behaviour of the instrument when the data storage buffer is full. Setting to @@ -357,18 +363,18 @@ def take_measurement(self, sample_rate, num_samples): :param `int` num_samples: Number of samples to take. - :rtype: `list` + :rtype: `tuple`[`tuple`[`float`, ...], `tuple`[`float`, ...]] + or if numpy is installed, `numpy.array`[`numpy.array`, `numpy.array`] """ if num_samples > 16383: - raise ValueError('Number of samples cannot exceed 16383.') + raise ValueError("Number of samples cannot exceed 16383.") sample_time = math.ceil(num_samples / sample_rate) - self.init(sample_rate, SRS830.BufferMode['one_shot']) + self.init(sample_rate, SRS830.BufferMode["one_shot"]) self.start_data_transfer() - if not self._testing: - time.sleep(sample_time + 0.1) + time.sleep(sample_time + 0.1) self.pause() @@ -378,13 +384,15 @@ def take_measurement(self, sample_rate, num_samples): # in future versions. try: self.num_data_points - except IOError: # pragma: no cover + except OSError: pass - ch1 = self.read_data_buffer('ch1') - ch2 = self.read_data_buffer('ch2') + ch1 = self.read_data_buffer("ch1") + ch2 = self.read_data_buffer("ch2") - return np.array([ch1, ch2]) + if numpy: + return numpy.array([ch1, ch2]) + return ch1, ch2 # OTHER METHODS # @@ -409,25 +417,25 @@ def set_offset_expand(self, mode, offset, expand): mode = SRS830.Mode[mode] if mode not in self._XYR_MODE_MAP: - raise ValueError('Specified mode not valid for this function.') + raise ValueError("Specified mode not valid for this function.") mode = self._XYR_MODE_MAP[mode] if not isinstance(offset, (int, float)): - raise TypeError('Offset parameter must be an integer or a float.') + raise TypeError("Offset parameter must be an integer or a float.") if not isinstance(expand, (int, float)): - raise TypeError('Expand parameter must be an integer or a float.') + raise TypeError("Expand parameter must be an integer or a float.") if (offset > 105) or (offset < -105): - raise ValueError('Offset mustbe -105 <= offset <= +105.') + raise ValueError("Offset mustbe -105 <= offset <= +105.") valid = [1, 10, 100] if expand in valid: expand = valid.index(expand) else: - raise ValueError('Expand must be 1, 10, 100.') + raise ValueError("Expand must be 1, 10, 100.") - self.sendcmd('OEXP {},{},{}'.format(mode, int(offset), expand)) + self.sendcmd(f"OEXP {mode},{int(offset)},{expand}") def start_scan(self): """ @@ -435,18 +443,26 @@ def start_scan(self): this is used to start the scan. The scan starts after a delay of 0.5 seconds. """ - self.sendcmd('STRD') + self.sendcmd("STRD") def pause(self): """ Has the instrument pause data capture. """ - self.sendcmd('PAUS') + self.sendcmd("PAUS") _data_snap_modes = { - Mode.x: 1, Mode.y: 2, Mode.r: 3, Mode.theta: 4, Mode.aux1: 5, - Mode.aux2: 6, Mode.aux3: 7, Mode.aux4: 8, Mode.ref: 9, - Mode.ch1: 10, Mode.ch2: 11 + Mode.x: 1, + Mode.y: 2, + Mode.r: 3, + Mode.theta: 4, + Mode.aux1: 5, + Mode.aux2: 6, + Mode.aux3: 7, + Mode.aux4: 8, + Mode.ref: 9, + Mode.ch1: 10, + Mode.ch2: 11, } def data_snap(self, mode1, mode2): @@ -477,19 +493,17 @@ def data_snap(self, mode1, mode2): mode2 = mode2.lower() mode2 = SRS830.Mode[mode2] - if ((mode1 not in self._data_snap_modes) or - (mode2 not in self._data_snap_modes)): - raise ValueError('Specified mode not valid for this function.') + if (mode1 not in self._data_snap_modes) or (mode2 not in self._data_snap_modes): + raise ValueError("Specified mode not valid for this function.") mode1 = self._XYR_MODE_MAP[mode1] mode2 = self._XYR_MODE_MAP[mode2] if mode1 == mode2: - raise ValueError('Both parameters for the data snapshot are the ' - 'same.') + raise ValueError("Both parameters for the data snapshot are the " "same.") - result = self.query('SNAP? {},{}'.format(mode1, mode2)) - return list(map(float, result.split(','))) + result = self.query(f"SNAP? {mode1},{mode2}") + return list(map(float, result.split(","))) _valid_read_data_buffer = {Mode.ch1: 1, Mode.ch2: 2} @@ -505,14 +519,14 @@ def read_data_buffer(self, channel): given by {CH1|CH2}. :type channel: `SRS830.Mode` or `str` - :rtype: `list` + :rtype: `tuple`[`float`, ...] or if numpy is installed, `numpy.array` """ if isinstance(channel, str): channel = channel.lower() channel = SRS830.Mode[channel] if channel not in self._valid_read_data_buffer: - raise ValueError('Specified mode not valid for this function.') + raise ValueError("Specified mode not valid for this function.") channel = self._valid_read_data_buffer[channel] @@ -521,30 +535,31 @@ def read_data_buffer(self, channel): # Query device for entire buffer, returning in ASCII, then # converting to a list of floats before returning to the # calling method - return np.fromstring( - self.query('TRCA?{},0,{}'.format(channel, N)).strip(), - sep=',' - ) + data = self.query(f"TRCA?{channel},0,{N}").strip() + if numpy: + return numpy.fromstring(data, sep=",") + return tuple(map(float, data.split(","))) def clear_data_buffer(self): """ Clears the data buffer of the SRS830. """ - self.sendcmd('REST') + self.sendcmd("REST") _valid_channel_display = [ - { # channel1 - Mode.x: 0, Mode.r: 1, Mode.xnoise: 2, Mode.aux1: 3, Mode.aux2: 4 - }, + {Mode.x: 0, Mode.r: 1, Mode.xnoise: 2, Mode.aux1: 3, Mode.aux2: 4}, # channel1 { # channel2 - Mode.y: 0, Mode.theta: 1, Mode.ynoise: 2, Mode.aux3: 3, - Mode.aux4: 4 - } + Mode.y: 0, + Mode.theta: 1, + Mode.ynoise: 2, + Mode.aux3: 3, + Mode.aux4: 4, + }, ] _valid_channel_ratio = [ {Mode.none: 0, Mode.aux1: 1, Mode.aux2: 2}, # channel1 - {Mode.none: 0, Mode.aux3: 1, Mode.aux4: 2} # channel2 + {Mode.none: 0, Mode.aux3: 1, Mode.aux4: 2}, # channel2 ] _valid_channel = {Mode.ch1: 1, Mode.ch2: 2} @@ -581,18 +596,16 @@ def set_channel_display(self, channel, display, ratio): ratio = SRS830.Mode[ratio] if channel not in self._valid_channel: - raise ValueError('Specified channel not valid for this function.') + raise ValueError("Specified channel not valid for this function.") channel = self._valid_channel[channel] if display not in self._valid_channel_display[channel - 1]: - raise ValueError('Specified display mode not valid for this ' - 'function.') + raise ValueError("Specified display mode not valid for this " "function.") if ratio not in self._valid_channel_ratio[channel - 1]: - raise ValueError('Specified display ratio not valid for this ' - 'function.') + raise ValueError("Specified display ratio not valid for this " "function.") display = self._valid_channel_display[channel - 1][display] ratio = self._valid_channel_ratio[channel - 1][ratio] - self.sendcmd('DDEF {},{},{}'.format(channel, display, ratio)) + self.sendcmd(f"DDEF {channel},{display},{ratio}") diff --git a/instruments/srs/srsctc100.py b/instruments/srs/srsctc100.py index 827b3ee90..8b0e8057f 100644 --- a/instruments/srs/srsctc100.py +++ b/instruments/srs/srsctc100.py @@ -1,23 +1,16 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides support for the SRS CTC-100 cryogenic temperature controller. """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division from contextlib import contextmanager -from builtins import range - from enum import Enum -import quantities as pq -import numpy as np - - from instruments.generic_scpi import SCPIInstrument +from instruments.optional_dep_finder import numpy +from instruments.units import ureg as u from instruments.util_fns import ProxyList # CLASSES ##################################################################### @@ -31,23 +24,20 @@ class SRSCTC100(SCPIInstrument): """ def __init__(self, filelike): - super(SRSCTC100, self).__init__(filelike) + super().__init__(filelike) self._do_errcheck = True # DICTIONARIES # - _BOOL_NAMES = { - 'On': True, - 'Off': False - } + _BOOL_NAMES = {"On": True, "Off": False} # Note that the SRS CTC-100 uses '\xb0' to represent '°'. _UNIT_NAMES = { - '\xb0C': pq.celsius, - 'W': pq.watt, - 'V': pq.volt, - '\xea': pq.ohm, - '': pq.dimensionless + "\xb0C": u.celsius, + "W": u.watt, + "V": u.volt, + "\xea": u.ohm, + "": u.dimensionless, } # INNER CLASSES ## @@ -56,12 +46,13 @@ class SensorType(Enum): """ Enum containing valid sensor types for the SRS CTC-100 """ - rtd = 'RTD' - thermistor = 'Thermistor' - diode = 'Diode' - rox = 'ROX' - class Channel(object): + rtd = "RTD" + thermistor = "Thermistor" + diode = "Diode" + rox = "ROX" + + class Channel: """ Represents an input or output channel on an SRS CTC-100 cryogenic @@ -81,14 +72,10 @@ def __init__(self, ctc, chan_name): # PRIVATE METHODS # def _get(self, prop_name): - return self._ctc.query("{}.{}?".format( - self._rem_name, - prop_name - )).strip() + return self._ctc.query(f"{self._rem_name}.{prop_name}?").strip() def _set(self, prop_name, newval): - self._ctc.sendcmd( - '{}.{} = "{}"'.format(self._rem_name, prop_name, newval)) + self._ctc.sendcmd(f'{self._rem_name}.{prop_name} = "{newval}"') # DISPLAY AND PROGRAMMING # # These properties control how the channel is identified in scripts @@ -107,7 +94,7 @@ def name(self): @name.setter def name(self, newval): - self._set('name', newval) + self._set("name", newval) # TODO: check for errors! self._chan_name = newval self._rem_name = newval.replace(" ", "") @@ -121,15 +108,12 @@ def value(self): kind of sensor and/or channel you have specified. Units can be one of ``celsius``, ``watt``, ``volt``, ``ohm``, or ``dimensionless``. - :type: `~quantities.Quantity` + :type: `~pint.Quantity` """ # WARNING: Queries all units all the time. # TODO: Make an OutputChannel that subclasses this class, # and add a setter for value. - return pq.Quantity( - float(self._get('value')), - self.units - ) + return u.Quantity(float(self._get("value")), self.units) @property def units(self): @@ -139,7 +123,7 @@ def units(self): Units can be one of ``celsius``, ``watt``, ``volt``, ``ohm``, or ``dimensionless``. - :type: `~quantities.UnitQuantity` + :type: `~pint.Unit` """ # FIXME: does not respect "chan.d/dt" property. return self._ctc.channel_units()[self._chan_name] @@ -155,7 +139,7 @@ def sensor_type(self): :type: `SRSCTC100.SensorType` """ - return self._ctc.SensorType(self._get('sensor')) + return self._ctc.SensorType(self._get("sensor")) # STATS # # The following properties control and query the statistics of the @@ -168,12 +152,12 @@ def stats_enabled(self): :type: `bool` """ - return True if self._get('stats') is 'On' else False + return True if self._get("stats") == "On" else False @stats_enabled.setter def stats_enabled(self, newval): # FIXME: replace with bool_property factory - self._set('stats', 'On' if newval else 'Off') + self._set("stats", "On" if newval else "Off") @property def stats_points(self): @@ -183,11 +167,11 @@ def stats_points(self): :type: `int` """ - return int(self._get('points')) + return int(self._get("points")) @stats_points.setter def stats_points(self, newval): - self._set('points', int(newval)) + self._set("points", int(newval)) @property def average(self): @@ -195,12 +179,9 @@ def average(self): Gets the average measurement for the specified channel as determined by the statistics gathering. - :type: `~quantities.Quantity` + :type: `~pint.Quantity` """ - return pq.Quantity( - float(self._get('average')), - self.units - ) + return u.Quantity(float(self._get("average")), self.units) @property def std_dev(self): @@ -208,16 +189,13 @@ def std_dev(self): Gets the standard deviation for the specified channel as determined by the statistics gathering. - :type: `~quantities.Quantity` + :type: `~pint.Quantity` """ - return pq.Quantity( - float(self._get('SD')), - self.units - ) + return u.Quantity(float(self._get("SD")), self.units) # LOGGING # - def get_log_point(self, which='next', units=None): + def get_log_point(self, which="next", units=None): """ Get a log data point from the instrument. @@ -227,20 +205,20 @@ def get_log_point(self, which='next', units=None): :param units: Units to attach to the returned data point. If left with the value of `None` then the instrument will be queried for the current units setting. - :type units: `~quantities.UnitQuantity` + :type units: `~pint.Unit` :return: The log data point with units - :rtype: `~quantities.Quantity` + :rtype: `~pint.Quantity` """ if units is None: units = self.units point = [ - s.strip() for s in - self._ctc.query( - 'getLog.xy {}, {}'.format(self._chan_name, which) - ).split(',') + s.strip() + for s in self._ctc.query(f"getLog.xy {self._chan_name}, {which}").split( + "," + ) ] - return pq.Quantity(point[0], 'ms'), pq.Quantity(point[1], units) + return u.Quantity(float(point[0]), "ms"), u.Quantity(float(point[1]), units) def get_log(self): """ @@ -249,32 +227,40 @@ def get_log(self): :return: Tuple of all the log data points. First value is time, second is the measurement value. - :rtype: Tuple of 2x `~quantities.Quantity`, each comprised of - a numpy array (`numpy.dnarray`). + :rtype: If numpy is installed, tuple of 2x `~pint.Quantity`, + each comprised of a numpy array (`numpy.dnarray`). + Else, `tuple`[`tuple`[`~pint.Quantity`, ...], `tuple`[`~pint.Quantity`, ...]] """ # Remember the current units. units = self.units # Find out how many points there are. - n_points = int( - self._ctc.query('getLog.xy? {}'.format(self._chan_name))) + n_points = int(self._ctc.query(f"getLog.xy? {self._chan_name}")) # Make an empty quantity that size for the times and for the channel # values. - ts = pq.Quantity(np.empty((n_points,)), 'ms') - temps = pq.Quantity(np.empty((n_points,)), units) + if numpy: + ts = u.Quantity(numpy.empty((n_points,)), u.ms) + temps = u.Quantity(numpy.empty((n_points,)), units) + else: + ts = [u.Quantity(0, u.ms)] * n_points + temps = [u.Quantity(0, units)] * n_points # Reset the position to the first point, then save it. # pylint: disable=protected-access with self._ctc._error_checking_disabled(): - ts[0], temps[0] = self.get_log_point('first', units) + ts[0], temps[0] = self.get_log_point("first", units) for idx in range(1, n_points): - ts[idx], temps[idx] = self.get_log_point('next', units) + ts[idx], temps[idx] = self.get_log_point("next", units) # Do an actual error check now. if self._ctc.error_check_toggle: self._ctc.errcheck() + if not numpy: + ts = tuple(ts) + temps = tuple(temps) + return ts, temps # PRIVATE METHODS ## @@ -298,10 +284,7 @@ def _channel_names(self): # As a consequence, users of this instrument MUST use spaces # matching the pretty name and not the remote-programming name. # CG could not think of a good way around this. - names = [ - name.strip() - for name in self.query('getOutput.names?').split(',') - ] + names = [name.strip() for name in self.query("getOutput.names?").split(",")] return names def channel_units(self): @@ -309,18 +292,17 @@ def channel_units(self): Returns a dictionary from channel names to channel units, using the ``getOutput.units`` command. Unknown units and dimensionless quantities are presented the same way by the instrument, and so both are reported - using `pq.dimensionless`. + using `u.dimensionless`. :rtype: `dict` with channel names as keys and units as values """ unit_strings = [ - unit_str.strip() - for unit_str in self.query('getOutput.units?').split(',') + unit_str.strip() for unit_str in self.query("getOutput.units?").split(",") ] - return dict( - (chan_name, self._UNIT_NAMES[unit_str]) + return { + chan_name: self._UNIT_NAMES[unit_str] for chan_name, unit_str in zip(self._channel_names(), unit_strings) - ) + } def errcheck(self): """ @@ -330,13 +312,13 @@ def errcheck(self): :return: Nothing """ - errs = super(SRSCTC100, self).query('geterror?').strip() - err_code, err_descript = errs.split(',') + errs = super().query("geterror?").strip() + err_code, err_descript = errs.split(",") err_code = int(err_code) if err_code == 0: return err_code else: - raise IOError(err_descript.strip()) + raise OSError(err_descript.strip()) @contextmanager def _error_checking_disabled(self): @@ -372,14 +354,16 @@ def display_figures(self): :type: `int` """ - return int(self.query('system.display.figures?')) + return int(self.query("system.display.figures?")) @display_figures.setter def display_figures(self, newval): if newval not in range(7): - raise ValueError("Number of display figures must be an integer " - "from 0 to 6, inclusive.") - self.sendcmd('system.display.figures = {}'.format(newval)) + raise ValueError( + "Number of display figures must be an integer " + "from 0 to 6, inclusive." + ) + self.sendcmd(f"system.display.figures = {newval}") @property def error_check_toggle(self): @@ -401,12 +385,12 @@ def error_check_toggle(self, newval): # We override sendcmd() and query() to do error checking after each # command. def sendcmd(self, cmd): - super(SRSCTC100, self).sendcmd(cmd) + super().sendcmd(cmd) if self._do_errcheck: self.errcheck() def query(self, cmd, size=-1): - resp = super(SRSCTC100, self).query(cmd, size) + resp = super().query(cmd, size) if self._do_errcheck: self.errcheck() return resp @@ -419,4 +403,4 @@ def clear_log(self): Not sure if this works. """ - self.sendcmd('System.Log.Clear yes') + self.sendcmd("System.Log.Clear yes") diff --git a/instruments/srs/srsdg645.py b/instruments/srs/srsdg645.py index b1ea42072..ff64edc7d 100644 --- a/instruments/srs/srsdg645.py +++ b/instruments/srs/srsdg645.py @@ -1,98 +1,95 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides support for the SRS DG645 digital delay generator. """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division -from builtins import map - from enum import IntEnum -import quantities as pq - -from instruments.generic_scpi import SCPIInstrument from instruments.abstract_instruments.comm import GPIBCommunicator +from instruments.generic_scpi import SCPIInstrument +from instruments.units import ureg as u from instruments.util_fns import assume_units, ProxyList # CLASSES ##################################################################### -class _SRSDG645Channel(object): - - """ - Class representing a sensor attached to the SRS DG645. +class SRSDG645(SCPIInstrument): - .. warning:: This class should NOT be manually created by the user. It is - designed to be initialized by the `SRSDG645` class. """ + Communicates with a Stanford Research Systems DG645 digital delay generator, + using the SCPI commands documented in the `user's guide`_. - def __init__(self, ddg, chan): - if not isinstance(ddg, SRSDG645): - raise TypeError("Don't do that.") + Example usage: - if isinstance(chan, SRSDG645.Channels): - self._chan = chan.value - else: - self._chan = chan + >>> import instruments as ik + >>> import instruments.units as u + >>> srs = ik.srs.SRSDG645.open_gpibusb('/dev/ttyUSB0', 1) + >>> srs.channel["B"].delay = (srs.channel["A"], u.Quantity(10, 'ns')) + >>> srs.output["AB"].level_amplitude = u.Quantity(4.0, "V") - self._ddg = ddg + .. _user's guide: http://www.thinksrs.com/downloads/PDFs/Manuals/DG645m.pdf + """ - # PROPERTIES # + class Channel: - @property - def idx(self): """ - Gets the channel identifier number as used for communication + Class representing a sensor attached to the SRS DG644. - :return: The communication identification number for the specified - channel - :rtype: `int` + .. warning:: This class should NOT be manually created by the user. It is + designed to be initialized by the `SRSDG644` class. """ - return self._chan - @property - def delay(self): - """ - Gets/sets the delay of this channel. - Formatted as a two-tuple of the reference and the delay time. - For example, ``(SRSDG645.Channels.A, pq.Quantity(10, "ps"))`` - indicates a delay of 10 picoseconds from delay channel A. - """ - resp = self._ddg.query("DLAY?{}".format(int(self._chan))).split(",") - return SRSDG645.Channels(int(resp[0])), pq.Quantity(float(resp[1]), "s") + def __init__(self, parent, chan): - @delay.setter - def delay(self, newval): - self._ddg.sendcmd("DLAY {},{},{}".format( - int(self._chan), - int(newval[0].idx), - newval[1].rescale("s").magnitude - )) + if not isinstance(parent, SRSDG645): + raise TypeError("Don't do that.") + if isinstance(chan, parent.Channels): + self._chan = chan.value + else: + self._chan = chan -class SRSDG645(SCPIInstrument): + self._ddg = parent - """ - Communicates with a Stanford Research Systems DG645 digital delay generator, - using the SCPI commands documented in the `user's guide`_. + # PROPERTIES # - Example usage: + @property + def idx(self): + """ + Gets the channel identifier number as used for communication - >>> import instruments as ik - >>> import quantities as pq - >>> srs = ik.srs.SRSDG645.open_gpibusb('/dev/ttyUSB0', 1) - >>> srs.channel["B"].delay = (srs.channel["A"], pq.Quantity(10, 'ns')) - >>> srs.output["AB"].level_amplitude = pq.Quantity(4.0, "V") + :return: The communication identification number for the specified + channel + :rtype: `int` + """ + return self._chan - .. _user's guide: http://www.thinksrs.com/downloads/PDFs/Manuals/DG645m.pdf - """ + @property + def delay(self): + """ + Gets/sets the delay of this channel. + Formatted as a two-tuple of the reference and the delay time. + For example, ``(SRSDG644.Channels.A, u.Quantity(10, "ps"))`` + indicates a delay of 9 picoseconds from delay channel A. + + :units: Assume seconds if no units given. + """ + resp = self._ddg.query(f"DLAY?{int(self._chan)}").split(",") + return self._ddg.Channels(int(resp[0])), u.Quantity(float(resp[1]), "s") + + @delay.setter + def delay(self, newval): + newval = (newval[0], assume_units(newval[1], u.s)) + self._ddg.sendcmd( + "DLAY {},{},{}".format( + int(self._chan), int(newval[0].idx), newval[1].to("s").magnitude + ) + ) def __init__(self, filelike): - super(SRSDG645, self).__init__(filelike) + super().__init__(filelike) # This instrument requires stripping two characters. if isinstance(filelike, GPIBCommunicator): @@ -105,6 +102,7 @@ class LevelPolarity(IntEnum): """ Polarities for output levels. """ + positive = 1 negative = 0 @@ -113,6 +111,7 @@ class Outputs(IntEnum): """ Enumeration of valid outputs from the DDG. """ + T0 = 0 AB = 1 CD = 2 @@ -124,6 +123,7 @@ class Channels(IntEnum): """ Enumeration of valid delay channels for the DDG. """ + T0 = 0 T1 = 1 A = 2 @@ -140,6 +140,7 @@ class DisplayMode(IntEnum): """ Enumeration of possible modes for the physical front-panel display. """ + trigger_rate = 0 trigger_threshold = 1 trigger_single_shot = 2 @@ -161,6 +162,7 @@ class TriggerSource(IntEnum): """ Enumeration of the different allowed trigger sources and modes. """ + internal = 0 external_rising = 1 external_falling = 2 @@ -171,7 +173,7 @@ class TriggerSource(IntEnum): # INNER CLASSES # - class Output(object): + class Output: """ An output from the DDG. @@ -189,36 +191,48 @@ def polarity(self): :type: :class:`SRSDG645.LevelPolarity` """ return self._parent.LevelPolarity( - int(self._parent.query("LPOL? {}".format(self._idx))) + int(self._parent.query(f"LPOL? {self._idx}")) ) @polarity.setter def polarity(self, newval): if not isinstance(newval, self._parent.LevelPolarity): - raise TypeError("Mode must be specified as a " - "SRSDG645.LevelPolarity value, got {} " - "instead.".format(type(newval))) - self._parent.sendcmd("LPOL {},{}".format( - self._idx, int(newval.value) - )) + raise TypeError( + "Mode must be specified as a " + "SRSDG645.LevelPolarity value, got {} " + "instead.".format(type(newval)) + ) + self._parent.sendcmd(f"LPOL {self._idx},{int(newval.value)}") @property def level_amplitude(self): """ Amplitude (in voltage) of the output level for this output. - :type: `float` or :class:`~quantities.Quantity` + :type: `float` or :class:`~pint.Quantity` :units: As specified, or :math:`\\text{V}` by default. """ - return pq.Quantity( - float(self._parent.query('LAMP? {}'.format(self._idx))), - 'V' - ) + return u.Quantity(float(self._parent.query(f"LAMP? {self._idx}")), "V") @level_amplitude.setter def level_amplitude(self, newval): - newval = assume_units(newval, 'V').magnitude - self._parent.sendcmd("LAMP {},{}".format(self._idx, newval)) + newval = assume_units(newval, "V").magnitude + self._parent.sendcmd(f"LAMP {self._idx},{newval}") + + @property + def level_offset(self): + """ + Amplitude offset (in voltage) of the output level for this output. + + :type: `float` or :class:`~pint.Quantity` + :units: As specified, or :math:`\\text{V}` by default. + """ + return u.Quantity(float(self._parent.query(f"LOFF? {self._idx}")), "V") + + @level_offset.setter + def level_offset(self, newval): + newval = assume_units(newval, "V").magnitude + self._parent.sendcmd(f"LOFF {self._idx},{newval}") # PROPERTIES # @@ -228,7 +242,7 @@ def channel(self): Gets a specific channel object. The desired channel is accessed by passing an EnumValue from - `~SRSDG645.Channels`. For example, to access channel A: + `SRSDG645.Channels`. For example, to access channel A: >>> import instruments as ik >>> inst = ik.srs.SRSDG645.open_gpibusb('/dev/ttyUSB0', 1) @@ -236,9 +250,9 @@ def channel(self): See the example in `SRSDG645` for a more complete example. - :rtype: `_SRSDG645Channel` + :rtype: `SRSDG645.Channel` """ - return ProxyList(self, _SRSDG645Channel, SRSDG645.Channels) + return ProxyList(self, self.Channel, SRSDG645.Channels) @property def output(self): @@ -263,7 +277,7 @@ def display(self): @display.setter def display(self, newval): # TODO: check types here. - self.sendcmd("DISP {0},{1}".format(*map(int, newval))) + self.sendcmd("DISP {},{}".format(*map(int, newval))) @property def enable_adv_triggering(self): @@ -276,22 +290,22 @@ def enable_adv_triggering(self): @enable_adv_triggering.setter def enable_adv_triggering(self, newval): - self.sendcmd("ADVT {}".format(1 if newval else 0)) + self.sendcmd(f"ADVT {1 if newval else 0}") @property def trigger_rate(self): """ Gets/sets the rate of the internal trigger. - :type: `~quantities.Quantity` or `float` + :type: `~pint.Quantity` or `float` :units: As passed or Hz if not specified. """ - return pq.Quantity(float(self.query("TRAT?")), pq.Hz) + return u.Quantity(float(self.query("TRAT?")), u.Hz) @trigger_rate.setter def trigger_rate(self, newval): - newval = assume_units(newval, pq.Hz) - self.sendcmd("TRAT {}".format(newval.rescale(pq.Hz).magnitude)) + newval = assume_units(newval, u.Hz) + self.sendcmd(f"TRAT {newval.to(u.Hz).magnitude}") @property def trigger_source(self): @@ -304,18 +318,94 @@ def trigger_source(self): @trigger_source.setter def trigger_source(self, newval): - self.sendcmd("TSRC {}".format(int(newval))) + self.sendcmd(f"TSRC {int(newval)}") @property def holdoff(self): """ Gets/sets the trigger holdoff time. - :type: `~quantities.Quantity` or `float` + :type: `~pint.Quantity` or `float` :units: As passed, or s if not specified. """ - return pq.Quantity(float(self.query("HOLD?")), pq.s) + return u.Quantity(float(self.query("HOLD?")), u.s) @holdoff.setter def holdoff(self, newval): - self.sendcmd("HOLD {}".format(newval.rescale(pq.s).magnitude)) + newval = assume_units(newval, u.s) + self.sendcmd(f"HOLD {newval.to(u.s).magnitude}") + + @property + def enable_burst_mode(self): + """ + Gets/sets whether burst mode is enabled. + + :type: `bool` + """ + return bool(int(self.query("BURM?"))) + + @enable_burst_mode.setter + def enable_burst_mode(self, newval): + self.sendcmd(f"BURM {1 if newval else 0}") + + @property + def enable_burst_t0_first(self): + """ + Gets/sets whether T0 output in burst mode is on first. If + enabled, the T0 output is enabled for first delay cycle of the + burst only. If disabled, the T0 output is enabled for all delay + cycles of the burst. + + :type: `bool` + """ + return bool(int(self.query("BURT?"))) + + @enable_burst_t0_first.setter + def enable_burst_t0_first(self, newval): + self.sendcmd(f"BURT {1 if newval else 0}") + + @property + def burst_count(self): + """ + Gets/sets the burst count. When burst mode is enabled, the + DG645 outputs burst count delay cycles per trigger. + Valid numbers for burst count are between 1 and 2**32 - 1 + """ + return int(self.query("BURC?")) + + @burst_count.setter + def burst_count(self, newval): + self.sendcmd(f"BURC {int(newval)}") + + @property + def burst_period(self): + """ + Gets/sets the burst period. The burst period sets the time + between delay cycles during a burst. The burst period may + range from 100 ns to 2000 – 10 ns in 10 ns steps. + + :units: Assume seconds if no units given. + """ + return u.Quantity(float(self.query("BURP?")), u.s) + + @burst_period.setter + def burst_period(self, newval): + newval = assume_units(newval, u.sec) + self.sendcmd(f"BURP {newval.to(u.sec).magnitude}") + + @property + def burst_delay(self): + """ + Gets/sets the burst delay. When burst mode is enabled the DG645 + delays the first burst pulse relative to the trigger by the + burst delay. The burst delay may range from 0 ps to < 2000 s + with a resolution of 5 ps. + + :units: Assume seconds if no units given. + """ + return u.Quantity(float(self.query("BURD?")), u.s) + + @burst_delay.setter + def burst_delay(self, newval): + newval = assume_units(newval, u.s) + self.sendcmd(f"BURD {newval.to(u.sec).magnitude}") diff --git a/instruments/tektronix/__init__.py b/instruments/tektronix/__init__.py index 5718e2d63..664f34fa0 100644 --- a/instruments/tektronix/__init__.py +++ b/instruments/tektronix/__init__.py @@ -1,16 +1,10 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing Tektronix instruments """ -from __future__ import absolute_import -from .tekdpo4104 import ( - TekDPO4104, - _TekDPO4104Channel, - _TekDPO4104DataSource, -) +from .tekdpo4104 import TekDPO4104 from .tekdpo70000 import TekDPO70000 from .tekawg2000 import TekAWG2000 from .tektds224 import TekTDS224 diff --git a/instruments/tektronix/tekawg2000.py b/instruments/tektronix/tekawg2000.py index c595f78f9..d06263243 100644 --- a/instruments/tektronix/tekawg2000.py +++ b/instruments/tektronix/tekawg2000.py @@ -1,21 +1,15 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides support for the Tektronix AWG2000 series arbitrary wave generators. """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division -from builtins import range - from enum import Enum -import numpy as np -import quantities as pq - from instruments.generic_scpi import SCPIInstrument +from instruments.optional_dep_finder import numpy +from instruments.units import ureg as u from instruments.util_fns import assume_units, ProxyList # CLASSES ##################################################################### @@ -30,7 +24,7 @@ class TekAWG2000(SCPIInstrument): # INNER CLASSES # - class Channel(object): + class Channel: """ Class representing a physical channel on the Tektronix AWG 2000 @@ -43,7 +37,7 @@ def __init__(self, tek, idx): self._tek = tek # Zero-based for pythonic convienence, so we need to convert to # Tektronix's one-based notation here. - self._name = "CH{}".format(idx + 1) + self._name = f"CH{idx + 1}" # Remember what the old data source was for use as a context manager self._old_dsrc = None @@ -64,42 +58,42 @@ def amplitude(self): """ Gets/sets the amplitude of the specified channel. - :units: As specified (if a `~quantities.Quantity`) or assumed to be + :units: As specified (if a `~pint.Quantity`) or assumed to be of units Volts. - :type: `~quantities.Quantity` with units Volts peak-to-peak. + :type: `~pint.Quantity` with units Volts peak-to-peak. """ - return pq.Quantity( - float(self._tek.query("FG:{}:AMPL?".format(self._name)).strip()), - pq.V + return u.Quantity( + float(self._tek.query(f"FG:{self._name}:AMPL?").strip()), u.V ) @amplitude.setter def amplitude(self, newval): - self._tek.sendcmd("FG:{}:AMPL {}".format( - self._name, - assume_units(newval, pq.V).rescale(pq.V).magnitude - )) + self._tek.sendcmd( + "FG:{}:AMPL {}".format( + self._name, assume_units(newval, u.V).to(u.V).magnitude + ) + ) @property def offset(self): """ Gets/sets the offset of the specified channel. - :units: As specified (if a `~quantities.Quantity`) or assumed to be + :units: As specified (if a `~pint.Quantity`) or assumed to be of units Volts. - :type: `~quantities.Quantity` with units Volts. + :type: `~pint.Quantity` with units Volts. """ - return pq.Quantity( - float(self._tek.query("FG:{}:OFFS?".format(self._name)).strip()), - pq.V + return u.Quantity( + float(self._tek.query(f"FG:{self._name}:OFFS?").strip()), u.V ) @offset.setter def offset(self, newval): - self._tek.sendcmd("FG:{}:OFFS {}".format( - self._name, - assume_units(newval, pq.V).rescale(pq.V).magnitude - )) + self._tek.sendcmd( + "FG:{}:OFFS {}".format( + self._name, assume_units(newval, u.V).to(u.V).magnitude + ) + ) @property def frequency(self): @@ -107,20 +101,17 @@ def frequency(self): Gets/sets the frequency of the specified channel when using the built-in function generator. - ::units: As specified (if a `~quantities.Quantity`) or assumed to be + ::units: As specified (if a `~pint.Quantity`) or assumed to be of units Hertz. - :type: `~quantities.Quantity` with units Hertz. + :type: `~pint.Quantity` with units Hertz. """ - return pq.Quantity( - float(self._tek.query("FG:FREQ?").strip()), - pq.Hz - ) + return u.Quantity(float(self._tek.query("FG:FREQ?").strip()), u.Hz) @frequency.setter def frequency(self, newval): - self._tek.sendcmd("FG:FREQ {}HZ".format( - assume_units(newval, pq.Hz).rescale(pq.Hz).magnitude - )) + self._tek.sendcmd( + f"FG:FREQ {assume_units(newval, u.Hz).to(u.Hz).magnitude}HZ" + ) @property def polarity(self): @@ -129,17 +120,18 @@ def polarity(self): :type: `TekAWG2000.Polarity` """ - return TekAWG2000.Polarity[self._tek.query("FG:{}:POL?".format( - self._name)).strip()] + return TekAWG2000.Polarity(self._tek.query(f"FG:{self._name}:POL?").strip()) @polarity.setter def polarity(self, newval): if not isinstance(newval, TekAWG2000.Polarity): - raise TypeError("Polarity settings must be a " - "`TekAWG2000.Polarity` value, got {} " - "instead.".format(type(newval))) + raise TypeError( + "Polarity settings must be a " + "`TekAWG2000.Polarity` value, got {} " + "instead.".format(type(newval)) + ) - self._tek.sendcmd("FG:{}:POL {}".format(self._name, newval.value)) + self._tek.sendcmd(f"FG:{self._name}:POL {newval.value}") @property def shape(self): @@ -149,15 +141,18 @@ def shape(self): :type: `TekAWG2000.Shape` """ - return TekAWG2000.Shape[self._tek.query("FG:{}:SHAP?".format( - self._name)).strip().split(',')[0]] + return TekAWG2000.Shape( + self._tek.query(f"FG:{self._name}:SHAP?").strip().split(",")[0] + ) @shape.setter def shape(self, newval): if not isinstance(newval, TekAWG2000.Shape): - raise TypeError("Shape settings must be a `TekAWG2000.Shape` " - "value, got {} instead.".format(type(newval))) - self._tek.sendcmd("FG:{}:SHAP {}".format(self._name, newval.value)) + raise TypeError( + "Shape settings must be a `TekAWG2000.Shape` " + "value, got {} instead.".format(type(newval)) + ) + self._tek.sendcmd(f"FG:{self._name}:SHAP {newval.value}") # ENUMS # @@ -165,6 +160,7 @@ class Polarity(Enum): """ Enum containing valid polarity modes for the AWG2000 """ + normal = "NORMAL" inverted = "INVERTED" @@ -172,6 +168,7 @@ class Shape(Enum): """ Enum containing valid waveform shape modes for hte AWG2000 """ + sine = "SINUSOID" pulse = "PULSE" ramp = "RAMP" @@ -196,7 +193,7 @@ def waveform_name(self): def waveform_name(self, newval): if not isinstance(newval, str): raise TypeError("Waveform name must be specified as a string.") - self.sendcmd('DATA:DEST "{}"'.format(newval)) + self.sendcmd(f'DATA:DEST "{newval}"') @property def channel(self): @@ -235,6 +232,12 @@ def upload_waveform(self, yzero, ymult, xincr, waveform): that all absolute values contained within the array should not exceed 1. """ + if numpy is None: + raise ImportError( + "Missing optional dependency numpy, which is required" + "for uploading waveforms." + ) + if not isinstance(yzero, float) and not isinstance(yzero, int): raise TypeError("yzero must be specified as a float or int") @@ -244,21 +247,21 @@ def upload_waveform(self, yzero, ymult, xincr, waveform): if not isinstance(xincr, float) and not isinstance(xincr, int): raise TypeError("xincr must be specified as a float or int") - if not isinstance(waveform, np.ndarray): + if not isinstance(waveform, numpy.ndarray): raise TypeError("waveform must be specified as a numpy array") - self.sendcmd("WFMP:YZERO {}".format(yzero)) - self.sendcmd("WFMP:YMULT {}".format(ymult)) - self.sendcmd("WFMP:XINCR {}".format(xincr)) - - if np.max(np.abs(waveform)) > 1: + if numpy.max(numpy.abs(waveform)) > 1: raise ValueError("The max value for an element in waveform is 1.") - waveform *= (2**12 - 1) - waveform = waveform.astype(">> import instruments as ik + >>> tek = ik.tektronix.TekDPO4104.open_tcpip("192.168.0.2", 8888) + >>> [x, y] = tek.channel[0].read_waveform() + """ - :type: `str` - """ - return self._name - - def __enter__(self): - self._old_dsrc = self._tek.data_source - if self._old_dsrc != self: - # Set the new data source, and let __exit__ cleanup. - self._tek.data_source = self - else: - # There"s nothing to do or undo in this case. - self._old_dsrc = None - - def __exit__(self, type, value, traceback): - if self._old_dsrc is not None: - self._tek.data_source = self._old_dsrc - - def __eq__(self, other): - if not isinstance(other, type(self)): - return NotImplemented - else: - return other.name == self.name + class DataSource(Oscilloscope.DataSource): - def read_waveform(self, bin_format=True): """ - Read waveform from the oscilloscope. - This function is all inclusive. After reading the data from the - oscilloscope, it unpacks the data and scales it accordingly. - Supports both ASCII and binary waveform transfer. - - Function returns a tuple (x,y), where both x and y are numpy arrays. + Class representing a data source (channel, math, or ref) on the Tektronix + DPO 4104. - :param bool bin_format: If `True`, data is transfered - in a binary format. Otherwise, data is transferred in ASCII. + .. warning:: This class should NOT be manually created by the user. It is + designed to be initialized by the `TekDPO4104` class. """ - # Set the acquisition channel - with self: - - # TODO: move this out somewhere more appropriate. - old_dat_stop = self._tek.query("DAT:STOP?") - self._tek.sendcmd("DAT:STOP {}".format(10**7)) - - if not bin_format: - # Set data encoding format to ASCII - self._tek.sendcmd("DAT:ENC ASCI") - sleep(0.02) # Work around issue with 2.48 firmware. - raw = self._tek.query("CURVE?") - raw = raw.split(",") # Break up comma delimited string - raw = map(float, raw) # Convert each list element to int - raw = np.array(raw) # Convert into numpy array - else: - # Set encoding to signed, big-endian - self._tek.sendcmd("DAT:ENC RIB") - sleep(0.02) # Work around issue with 2.48 firmware. - data_width = self._tek.data_width - self._tek.sendcmd("CURVE?") - # Read in the binary block, data width of 2 bytes. - raw = self._tek.binblockread(data_width) - - yoffs = self._tek.y_offset # Retrieve Y offset - ymult = self._tek.query("WFMP:YMU?") # Retrieve Y multiplier - yzero = self._tek.query("WFMP:YZE?") # Retrieve Y zero - - y = ((raw - yoffs) * float(ymult)) + float(yzero) - - xzero = self._tek.query("WFMP:XZE?") # Retrieve X zero - xincr = self._tek.query("WFMP:XIN?") # Retrieve X incr - # Retrieve number of data points - ptcnt = self._tek.query("WFMP:NR_P?") - - x = np.arange(float(ptcnt)) * float(xincr) + float(xzero) + def __init__(self, tek, name): + super().__init__(tek, name) + self._tek = self._parent - self._tek.sendcmd("DAT:STOP {}".format(old_dat_stop)) + @property + def name(self): + """ + Gets the name of this data source, as identified over SCPI. - return x, y + :type: `str` + """ + return self._name - y_offset = _parent_property("y_offset") - - -class _TekDPO4104Channel(_TekDPO4104DataSource, OscilloscopeChannel): - - """ - Class representing a channel on the Tektronix DPO 4104. + def __enter__(self): + self._old_dsrc = self._tek.data_source + if self._old_dsrc != self: + # Set the new data source, and let __exit__ cleanup. + self._tek.data_source = self + else: + # There"s nothing to do or undo in this case. + self._old_dsrc = None - This class inherits from `_TekDPO4104DataSource`. + def __exit__(self, type, value, traceback): + if self._old_dsrc is not None: + self._tek.data_source = self._old_dsrc - .. warning:: This class should NOT be manually created by the user. It is - designed to be initialized by the `TekDPO4104` class. - """ + def __eq__(self, other): + if not isinstance(other, type(self)): + return NotImplemented - def __init__(self, parent, idx): - super(_TekDPO4104Channel, self).__init__( - parent, "CH{}".format(idx + 1)) - self._idx = idx + 1 + return other.name == self.name - @property - def coupling(self): - """ - Gets/sets the coupling setting for this channel. + __hash__ = None + + def read_waveform(self, bin_format=True): + """ + Read waveform from the oscilloscope. + This function is all inclusive. After reading the data from the + oscilloscope, it unpacks the data and scales it accordingly. + Supports both ASCII and binary waveform transfer. + + Function returns a tuple (x,y), where both x and y are numpy arrays. + + :param bool bin_format: If `True`, data is transfered + in a binary format. Otherwise, data is transferred in ASCII. + :rtype: `tuple`[`tuple`[`~pint.Quantity`, ...], `tuple`[`~pint.Quantity`, ...]] + or if numpy is installed, `tuple` of two `~pint.Quantity` with `numpy.array` data + """ + + # Set the acquisition channel + with self: + + # TODO: move this out somewhere more appropriate. + old_dat_stop = self._tek.query("DAT:STOP?") + self._tek.sendcmd(f"DAT:STOP {10 ** 7}") + + if not bin_format: + # Set data encoding format to ASCII + self._tek.sendcmd("DAT:ENC ASCI") + sleep(0.02) # Work around issue with 2.48 firmware. + raw = self._tek.query("CURVE?") + raw = raw.split(",") # Break up comma delimited string + if numpy: + raw = numpy.array( + raw, dtype=numpy.float + ) # Convert to numpy array + else: + raw = map(float, raw) + else: + # Set encoding to signed, big-endian + self._tek.sendcmd("DAT:ENC RIB") + sleep(0.02) # Work around issue with 2.48 firmware. + data_width = self._tek.data_width + self._tek.sendcmd("CURVE?") + # Read in the binary block, data width of 2 bytes. + raw = self._tek.binblockread(data_width) + # Read the new line character that is sent + self._tek._file.read_raw(1) # pylint: disable=protected-access + + yoffs = self._tek.y_offset # Retrieve Y offset + ymult = self._tek.query("WFMP:YMU?") # Retrieve Y multiplier + yzero = self._tek.query("WFMP:YZE?") # Retrieve Y zero + + xzero = self._tek.query("WFMP:XZE?") # Retrieve X zero + xincr = self._tek.query("WFMP:XIN?") # Retrieve X incr + # Retrieve number of data points + ptcnt = self._tek.query("WFMP:NR_P?") + + if numpy: + x = numpy.arange(float(ptcnt)) * float(xincr) + float(xzero) + y = ((raw - yoffs) * float(ymult)) + float(yzero) + else: + x = tuple( + float(val) * float(xincr) + float(xzero) + for val in range(int(ptcnt)) + ) + y = tuple(((x - yoffs) * float(ymult)) + float(yzero) for x in raw) + + self._tek.sendcmd(f"DAT:STOP {old_dat_stop}") + + return x, y + + y_offset = _parent_property("y_offset") + + class Channel(DataSource, Oscilloscope.Channel): - :type: `TekDPO4104.Coupling` """ - return TekDPO4104.Coupling( - self._tek.query("CH{}:COUPL?".format(self._idx)) - ) - - @coupling.setter - def coupling(self, newval): - if not isinstance(newval, TekDPO4104.Coupling): - raise TypeError("Coupling setting must be a `TekDPO4104.Coupling`" - " value, got {} instead.".format(type(newval))) + Class representing a channel on the Tektronix DPO 4104. - self._tek.sendcmd("CH{}:COUPL {}".format(self._idx, newval.value)) + This class inherits from `TekDPO4104.DataSource`. + .. warning:: This class should NOT be manually created by the user. It is + designed to be initialized by the `TekDPO4104` class. + """ -class TekDPO4104(SCPIInstrument, Oscilloscope): + def __init__(self, parent, idx): + super().__init__(parent, f"CH{idx + 1}") + self._idx = idx + 1 - """ - The Tektronix DPO4104 is a multi-channel oscilloscope with analog - bandwidths ranging from 100MHz to 1GHz. + @property + def coupling(self): + """ + Gets/sets the coupling setting for this channel. - This class inherits from `~instruments.generic_scpi.SCPIInstrument`. + :type: `TekDPO4104.Coupling` + """ + return TekDPO4104.Coupling(self._tek.query(f"CH{self._idx}:COUPL?")) - Example usage: + @coupling.setter + def coupling(self, newval): + if not isinstance(newval, TekDPO4104.Coupling): + raise TypeError( + "Coupling setting must be a `TekDPO4104.Coupling`" + " value, got {} instead.".format(type(newval)) + ) - >>> import instruments as ik - >>> tek = ik.tektronix.TekDPO4104.open_tcpip("192.168.0.2", 8888) - >>> [x, y] = tek.channel[0].read_waveform() - """ + self._tek.sendcmd(f"CH{self._idx}:COUPL {newval.value}") # ENUMS # @@ -201,6 +205,7 @@ class Coupling(Enum): Enum containing valid coupling modes for the channels on the Tektronix DPO 4104 """ + ac = "AC" dc = "DC" ground = "GND" @@ -218,9 +223,9 @@ def channel(self): >>> tek = ik.tektronix.TekDPO4104.open_tcpip("192.168.0.2", 8888) >>> [x, y] = tek.channel[0].read_waveform() - :rtype: `_TekDPO4104Channel` + :rtype: `TekDPO4104.Channel` """ - return ProxyList(self, _TekDPO4104Channel, range(4)) + return ProxyList(self, self.Channel, range(4)) @property def ref(self): @@ -234,12 +239,12 @@ def ref(self): >>> tek = ik.tektronix.TekDPO4104.open_tcpip("192.168.0.2", 8888) >>> [x, y] = tek.ref[0].read_waveform() - :rtype: `_TekDPO4104DataSource` + :rtype: `TekDPO4104.DataSource` """ return ProxyList( self, - lambda s, idx: _TekDPO4104DataSource(s, "REF{}".format(idx + 1)), - range(4) + lambda s, idx: self.DataSource(s, f"REF{idx + 1}"), + range(4), ) @property @@ -247,9 +252,9 @@ def math(self): """ Gets a data source object corresponding to the MATH channel. - :rtype: `_TekDPO4104DataSource` + :rtype: `TekDPO4104.DataSource` """ - return _TekDPO4104DataSource(self, "MATH") + return self.DataSource(self, "MATH") @property def data_source(self): @@ -258,9 +263,9 @@ def data_source(self): """ name = self.query("DAT:SOU?") if name.startswith("CH"): - return _TekDPO4104Channel(self, int(name[2:]) - 1) - else: - return _TekDPO4104DataSource(self, name) + return self.Channel(self, int(name[2:]) - 1) + + return self.DataSource(self, name) @data_source.setter def data_source(self, newval): @@ -270,7 +275,7 @@ def data_source(self, newval): newval = newval.value elif hasattr(newval, "name"): # Is a datasource with a name. newval = newval.name - self.sendcmd("DAT:SOU {}".format(newval)) + self.sendcmd(f"DAT:SOU {newval}") sleep(0.01) # Let the instrument catch up. @property @@ -284,7 +289,7 @@ def aquisition_length(self): @aquisition_length.setter def aquisition_length(self, newval): - self.sendcmd("HOR:RECO {}".format(newval)) + self.sendcmd(f"HOR:RECO {newval}") @property def aquisition_running(self): @@ -299,7 +304,7 @@ def aquisition_running(self): @aquisition_running.setter def aquisition_running(self, newval): - self.sendcmd("ACQ:STATE {}".format(1 if newval else 0)) + self.sendcmd(f"ACQ:STATE {1 if newval else 0}") @property def aquisition_continuous(self): @@ -332,7 +337,7 @@ def data_width(self, newval): if int(newval) not in [1, 2]: raise ValueError("Only one or two byte-width is supported.") - self.sendcmd("DATA:WIDTH {}".format(newval)) + self.sendcmd(f"DATA:WIDTH {newval}") # TODO: convert to read in unitful quantities. @property @@ -345,7 +350,7 @@ def y_offset(self): @y_offset.setter def y_offset(self, newval): - self.sendcmd("WFMP:YOF {}".format(newval)) + self.sendcmd(f"WFMP:YOF {newval}") # METHODS # diff --git a/instruments/tektronix/tekdpo70000.py b/instruments/tektronix/tekdpo70000.py index 3c4c7664a..be41424c2 100644 --- a/instruments/tektronix/tekdpo70000.py +++ b/instruments/tektronix/tekdpo70000.py @@ -1,33 +1,32 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides support for the Tektronix DPO 70000 oscilloscope series """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division - import abc -import time - -from builtins import range from enum import Enum +import time -import quantities as pq - -from instruments.abstract_instruments import ( - Oscilloscope, OscilloscopeChannel, OscilloscopeDataSource -) +from instruments.abstract_instruments import Oscilloscope from instruments.generic_scpi import SCPIInstrument +from instruments.optional_dep_finder import numpy +from instruments.units import ureg as u from instruments.util_fns import ( - enum_property, string_property, int_property, unitful_property, - unitless_property, bool_property, ProxyList + enum_property, + string_property, + int_property, + unitful_property, + unitless_property, + bool_property, + ProxyList, ) # CLASSES ##################################################################### +# pylint: disable=too-many-lines + class TekDPO70000(SCPIInstrument, Oscilloscope): @@ -58,6 +57,7 @@ class AcquisitionMode(Enum): Enum containing valid acquisition modes for the Tektronix 70000 series oscilloscopes. """ + sample = "SAM" peak_detect = "PEAK" hi_res = "HIR" @@ -71,10 +71,11 @@ class AcquisitionState(Enum): Enum containing valid acquisition states for the Tektronix 70000 series oscilloscopes. """ - on = 'ON' - off = 'OFF' - run = 'RUN' - stop = 'STOP' + + on = "ON" + off = "OFF" + run = "RUN" + stop = "STOP" class StopAfter(Enum): @@ -82,8 +83,9 @@ class StopAfter(Enum): Enum containing valid stop condition modes for the Tektronix 70000 series oscilloscopes. """ - run_stop = 'RUNST' - sequence = 'SEQ' + + run_stop = "RUNST" + sequence = "SEQ" class SamplingMode(Enum): @@ -91,6 +93,7 @@ class SamplingMode(Enum): Enum containing valid sampling modes for the Tektronix 70000 series oscilloscopes. """ + real_time = "RT" equivalent_time_allowed = "ET" interpolation_allowed = "IT" @@ -101,6 +104,7 @@ class HorizontalMode(Enum): Enum containing valid horizontal scan modes for the Tektronix 70000 series oscilloscopes. """ + auto = "AUTO" constant = "CONST" manual = "MAN" @@ -111,6 +115,7 @@ class WaveformEncoding(Enum): Enum containing valid waveform encoding modes for the Tektronix 70000 series oscilloscopes. """ + # NOTE: For some reason, it uses the full names here instead of # returning the mneonics listed in the manual. ascii = "ASCII" @@ -122,6 +127,7 @@ class BinaryFormat(Enum): Enum containing valid binary formats for the Tektronix 70000 series oscilloscopes (int, unsigned-int, floating-point). """ + int = "RI" uint = "RP" float = "FP" # Single-precision! @@ -132,6 +138,7 @@ class ByteOrder(Enum): Enum containing valid byte order (big-/little-endian) for the Tektronix 70000 series oscilloscopes. """ + little_endian = "LSB" big_endian = "MSB" @@ -141,6 +148,7 @@ class TriggerState(Enum): Enum containing valid trigger states for the Tektronix 70000 series oscilloscopes. """ + armed = "ARMED" auto = "AUTO" dpo = "DPO" @@ -151,18 +159,22 @@ class TriggerState(Enum): @staticmethod def _dtype(binary_format, byte_order, n_bytes): - return "{}{}{}".format({ - TekDPO70000.ByteOrder.big_endian: ">", - TekDPO70000.ByteOrder.little_endian: "<" - }[byte_order], { - TekDPO70000.BinaryFormat.int: "i", - TekDPO70000.BinaryFormat.uint: "u", - TekDPO70000.BinaryFormat.float: "f" - }[binary_format], n_bytes) + return "{}{}{}".format( + { + TekDPO70000.ByteOrder.big_endian: ">", + TekDPO70000.ByteOrder.little_endian: "<", + }[byte_order], + (n_bytes if n_bytes is not None else ""), + { + TekDPO70000.BinaryFormat.int: "i", + TekDPO70000.BinaryFormat.uint: "u", + TekDPO70000.BinaryFormat.float: "f", + }[binary_format], + ) # CLASSES # - class DataSource(OscilloscopeDataSource): + class DataSource(Oscilloscope.DataSource): """ Class representing a data source (channel, math, or ref) on the @@ -172,9 +184,6 @@ class DataSource(OscilloscopeDataSource): is designed to be initialized by the `TekDPO70000` class. """ - def __init__(self, parent, name): - super(TekDPO70000.DataSource, self).__init__(parent, name) - @property def name(self): return self._name @@ -195,16 +204,12 @@ def read_waveform(self, bin_format=True): dtype = self._parent._dtype( self._parent.outgoing_binary_format, self._parent.outgoing_byte_order, - n_bytes + n_bytes=None, ) self._parent.sendcmd("CURV?") raw = self._parent.binblockread(n_bytes, fmt=dtype) - # Clear the queue by trying to read. - # FIXME: this is a hack-y way of doing so. - if hasattr(self._parent._file, 'flush_input'): - self._parent._file.flush_input() - else: - self._parent._file.read() + # Clear the queue by reading the end of line character + self._parent._file.read_raw(1) return self._scale_raw_data(raw) @@ -237,10 +242,7 @@ def __init__(self, parent, idx): self._idx = idx + 1 # 1-based. # Initialize as a data source with name MATH{}. - super(TekDPO70000.Math, self).__init__( - parent, - "MATH{}".format(self._idx) - ) + super(TekDPO70000.Math, self).__init__(parent, f"MATH{self._idx}") def sendcmd(self, cmd): """ @@ -249,7 +251,7 @@ def sendcmd(self, cmd): :param str cmd: Command to send to the instrument """ - self._parent.sendcmd("MATH{}:{}".format(self._idx, cmd)) + self._parent.sendcmd(f"MATH{self._idx}:{cmd}") def query(self, cmd, size=-1): """ @@ -262,13 +264,14 @@ def query(self, cmd, size=-1): :return: The query response :rtype: `str` """ - return self._parent.query("MATH{}:{}".format(self._idx, cmd), size) + return self._parent.query(f"MATH{self._idx}:{cmd}", size) class FilterMode(Enum): """ Enum containing valid filter modes for a math channel on the TekDPO70000 series oscilloscope. """ + centered = "CENT" shifted = "SHIF" @@ -277,6 +280,7 @@ class Mag(Enum): Enum containing valid amplitude units for a math channel on the TekDPO70000 series oscilloscope. """ + linear = "LINEA" db = "DB" dbm = "DBM" @@ -286,6 +290,7 @@ class Phase(Enum): Enum containing valid phase units for a math channel on the TekDPO70000 series oscilloscope. """ + degrees = "DEG" radians = "RAD" group_delay = "GROUPD" @@ -295,6 +300,7 @@ class SpectralWindow(Enum): Enum containing valid spectral windows for a math channel on the TekDPO70000 series oscilloscope. """ + rectangular = "RECTANG" hamming = "HAMM" hanning = "HANN" @@ -308,37 +314,31 @@ class SpectralWindow(Enum): "DEF", doc=""" A text string specifying the math to do, ex. CH1+CH2 - """ + """, ) - filter_mode = enum_property( - "FILT:MOD", - FilterMode - ) + filter_mode = enum_property("FILT:MOD", FilterMode) - filter_risetime = unitful_property( - "FILT:RIS", - pq.second - ) + filter_risetime = unitful_property("FILT:RIS", u.second) label = string_property( "LAB:NAM", doc=""" Just a human readable label for the channel. - """ + """, ) label_xpos = unitless_property( "LAB:XPOS", doc=""" The x position, in divisions, to place the label. - """ + """, ) label_ypos = unitless_property( "LAB:YPOS", doc="""The y position, in divisions, to place the label. - """ + """, ) num_avg = unitless_property( @@ -346,55 +346,51 @@ class SpectralWindow(Enum): doc=""" The number of acquisistions over which exponential averaging is performed. - """ + """, ) spectral_center = unitful_property( "SPEC:CENTER", - pq.Hz, + u.Hz, doc=""" The desired frequency of the spectral analyzer output data span in Hz. - """ + """, ) spectral_gatepos = unitful_property( "SPEC:GATEPOS", - pq.second, + u.second, doc=""" The gate position. Units are represented in seconds, with respect to trigger position. - """ + """, ) spectral_gatewidth = unitful_property( "SPEC:GATEWIDTH", - pq.second, + u.second, doc=""" The time across the 10-division screen in seconds. - """ + """, ) - spectral_lock = bool_property( - "SPEC:LOCK", - inst_true="ON", - inst_false="OFF" - ) + spectral_lock = bool_property("SPEC:LOCK", inst_true="ON", inst_false="OFF") - spectral_mag = unitful_property( + spectral_mag = enum_property( "SPEC:MAG", Mag, doc=""" Whether the spectral magnitude is linear, db, or dbm. - """ + """, ) - spectral_phase = unitful_property( + spectral_phase = enum_property( "SPEC:PHASE", - Mag, + Phase, doc=""" Whether the spectral phase is degrees, radians, or group delay. - """ + """, ) spectral_reflevel = unitless_property( @@ -402,29 +398,27 @@ class SpectralWindow(Enum): doc=""" The value that represents the topmost display screen graticule. The units depend on spectral_mag. - """ + """, ) - spectral_reflevel_offset = unitless_property( - "SPEC:REFLEVELO" - ) + spectral_reflevel_offset = unitless_property("SPEC:REFLEVELO") spectral_resolution_bandwidth = unitful_property( "SPEC:RESB", - pq.Hz, + u.Hz, doc=""" The desired resolution bandwidth value. Units are represented in Hertz. - """ + """, ) spectral_span = unitful_property( "SPEC:SPAN", - pq.Hz, + u.Hz, doc=""" Specifies the frequency span of the output data vector from the spectral analyzer. - """ + """, ) spectral_suppress = unitless_property( @@ -432,7 +426,7 @@ class SpectralWindow(Enum): doc=""" The magnitude level that data with magnitude values below this value are displayed as zero phase. - """ + """, ) spectral_unwrap = bool_property( @@ -441,27 +435,24 @@ class SpectralWindow(Enum): inst_false="OFF", doc=""" Enables or disables phase wrapping. - """ + """, ) - spectral_window = enum_property( - "SPEC:WIN", - SpectralWindow - ) + spectral_window = enum_property("SPEC:WIN", SpectralWindow) threshhold = unitful_property( "THRESH", - pq.volt, + u.volt, doc=""" The math threshhold in volts - """ + """, ) unit_string = string_property( "UNITS", doc=""" Just a label for the units...doesn"t actually change anything. - """ + """, ) autoscale = bool_property( @@ -470,33 +461,42 @@ class SpectralWindow(Enum): inst_false="OFF", doc=""" Enables or disables the auto-scaling of new math waveforms. - """ + """, ) position = unitless_property( "VERT:POS", doc=""" The vertical position, in divisions from the center graticule. - """ + """, ) scale = unitful_property( "VERT:SCALE", - pq.volt, + u.volt, doc=""" The scale in volts per division. The range is from ``100e-36`` to ``100e+36``. - """ + """, ) def _scale_raw_data(self, data): # TODO: incorperate the unit_string somehow - return self.scale * ( - (TekDPO70000.VERT_DIVS / 2) * - data.astype(float) / (2**15) - self.position + if numpy: + return self.scale * ( + (TekDPO70000.VERT_DIVS / 2) * data.astype(float) / (2 ** 15) + - self.position + ) + + scale = self.scale + position = self.position + rval = tuple( + scale * ((TekDPO70000.VERT_DIVS / 2) * d / (2 ** 15) - position) + for d in map(float, data) ) + return rval - class Channel(DataSource, OscilloscopeChannel): + class Channel(DataSource, Oscilloscope.Channel): """ Class representing a channel on the Tektronix DPO 70000. @@ -512,10 +512,7 @@ def __init__(self, parent, idx): self._idx = idx + 1 # 1-based. # Initialize as a data source with name CH{}. - super(TekDPO70000.Channel, self).__init__( - self._parent, - "CH{}".format(self._idx) - ) + super(TekDPO70000.Channel, self).__init__(self._parent, f"CH{self._idx}") def sendcmd(self, cmd): """ @@ -524,7 +521,7 @@ def sendcmd(self, cmd): :param str cmd: Command to send to the instrument """ - self._parent.sendcmd("CH{}:{}".format(self._idx, cmd)) + self._parent.sendcmd(f"CH{self._idx}:{cmd}") def query(self, cmd, size=-1): """ @@ -537,12 +534,13 @@ def query(self, cmd, size=-1): :return: The query response :rtype: `str` """ - return self._parent.query("CH{}:{}".format(self._idx, cmd), size) + return self._parent.query(f"CH{self._idx}:{cmd}", size) class Coupling(Enum): """ Enum containing valid coupling modes for the oscilloscope channel """ + ac = "AC" dc = "DC" dc_reject = "DCREJ" @@ -560,77 +558,83 @@ class Coupling(Enum): >>> inst = ik.tektronix.TekDPO70000.open_tcpip("192.168.0.1", 8080) >>> channel = inst.channel[0] >>> channel.coupling = channel.Coupling.ac - """ + """, ) - bandwidth = unitful_property( - 'BAN', - pq.Hz - ) + bandwidth = unitful_property("BAN", u.Hz) - deskew = unitful_property( - 'DESK', - pq.second - ) + deskew = unitful_property("DESK", u.second) - termination = unitful_property( - 'TERM', - pq.ohm - ) + termination = unitful_property("TERM", u.ohm) label = string_property( - 'LAB:NAM', + "LAB:NAM", doc=""" Just a human readable label for the channel. - """ + """, ) label_xpos = unitless_property( - 'LAB:XPOS', + "LAB:XPOS", doc=""" The x position, in divisions, to place the label. - """ + """, ) label_ypos = unitless_property( - 'LAB:YPOS', + "LAB:YPOS", doc=""" The y position, in divisions, to place the label. - """ + """, ) offset = unitful_property( - 'OFFS', - pq.volt, + "OFFS", + u.volt, doc=""" The vertical offset in units of volts. Voltage is given by ``offset+scale*(5*raw/2^15 - position)``. - """ + """, ) position = unitless_property( - 'POS', + "POS", doc=""" The vertical position, in divisions from the center graticule, ranging from ``-8`` to ``8``. Voltage is given by ``offset+scale*(5*raw/2^15 - position)``. - """ + """, ) scale = unitful_property( - 'SCALE', - pq.volt, + "SCALE", + u.volt, doc=""" Vertical channel scale in units volts/division. Voltage is given by ``offset+scale*(5*raw/2^15 - position)``. - """ + """, ) def _scale_raw_data(self, data): - return self.scale * ( - (TekDPO70000.VERT_DIVS / 2) * - data.astype(float) / (2**15) - self.position - ) + self.offset + scale = self.scale + position = self.position + offset = self.offset + + if numpy: + return ( + scale + * ( + (TekDPO70000.VERT_DIVS / 2) * data.astype(float) / (2 ** 15) + - position + ) + + offset + ) + + return tuple( + scale * ((TekDPO70000.VERT_DIVS / 2) * d / (2 ** 15) - position) + + offset + for d in map(float, data) + ) # PROPERTIES ## @@ -649,134 +653,116 @@ def ref(self): # For some settings that probably won't be used that often, use # string_property instead of setting up an enum property. acquire_enhanced_enob = string_property( - 'ACQ:ENHANCEDE', - bookmark_symbol='', + "ACQ:ENHANCEDE", + bookmark_symbol="", doc=""" Valid values are AUTO and OFF. - """ + """, ) acquire_enhanced_state = bool_property( - 'ACQ:ENHANCEDE:STATE', - inst_false='0', # TODO: double check that these are correct - inst_true='1' + "ACQ:ENHANCEDE:STATE", + inst_false="0", # TODO: double check that these are correct + inst_true="1", ) acquire_interp_8bit = string_property( - 'ACQ:INTERPE', - bookmark_symbol='', + "ACQ:INTERPE", + bookmark_symbol="", doc=""" Valid values are AUTO, ON and OFF. - """ + """, ) - acquire_magnivu = bool_property( - 'ACQ:MAG', - inst_true='ON', - inst_false='OFF' - ) + acquire_magnivu = bool_property("ACQ:MAG", inst_true="ON", inst_false="OFF") - acquire_mode = enum_property( - 'ACQ:MOD', - AcquisitionMode - ) + acquire_mode = enum_property("ACQ:MOD", AcquisitionMode) - acquire_mode_actual = enum_property( - 'ACQ:MOD:ACT', - AcquisitionMode, - readonly=True - ) + acquire_mode_actual = enum_property("ACQ:MOD:ACT", AcquisitionMode, readonly=True) acquire_num_acquisitions = int_property( - 'ACQ:NUMAC', + "ACQ:NUMAC", readonly=True, doc=""" The number of waveform acquisitions that have occurred since starting acquisition with the ACQuire:STATE RUN command - """ + """, ) acquire_num_avgs = int_property( - 'ACQ:NUMAV', + "ACQ:NUMAV", doc=""" The number of waveform acquisitions to average. - """ + """, ) acquire_num_envelop = int_property( - 'ACQ:NUME', + "ACQ:NUME", doc=""" The number of waveform acquisitions to be enveloped - """ + """, ) acquire_num_frames = int_property( - 'ACQ:NUMFRAMESACQ', + "ACQ:NUMFRAMESACQ", readonly=True, doc=""" The number of frames acquired when in FastFrame Single Sequence and acquisitions are running. - """ + """, ) acquire_num_samples = int_property( - 'ACQ:NUMSAM', + "ACQ:NUMSAM", doc=""" The minimum number of acquired samples that make up a waveform database (WfmDB) waveform for single sequence mode and Mask Pass/Fail Completion Test. The default value is 16,000 samples. The range is 5,000 to 2,147,400,000 samples. - """ + """, ) - acquire_sampling_mode = enum_property( - 'ACQ:SAMP', - SamplingMode - ) + acquire_sampling_mode = enum_property("ACQ:SAMP", SamplingMode) acquire_state = enum_property( - 'ACQ:STATE', + "ACQ:STATE", AcquisitionState, doc=""" This command starts or stops acquisitions. - """ + """, ) acquire_stop_after = enum_property( - 'ACQ:STOPA', + "ACQ:STOPA", StopAfter, doc=""" This command sets or queries whether the instrument continually acquires acquisitions or acquires a single sequence. - """ + """, ) - data_framestart = int_property('DAT:FRAMESTAR') + data_framestart = int_property("DAT:FRAMESTAR") - data_framestop = int_property('DAT:FRAMESTOP') + data_framestop = int_property("DAT:FRAMESTOP") data_start = int_property( - 'DAT:STAR', + "DAT:STAR", doc=""" The first data point that will be transferred, which ranges from 1 to the record length. - """ + """, ) # TODO: Look into the following troublesome datasheet note: "When using the # CURVe command, DATa:STOP is ignored and WFMInpre:NR_Pt is used." data_stop = int_property( - 'DAT:STOP', + "DAT:STOP", doc=""" The last data point that will be transferred. - """ + """, ) - data_sync_sources = bool_property( - 'DAT:SYNCSOU', - inst_true='ON', - inst_false='OFF' - ) + data_sync_sources = bool_property("DAT:SYNCSOU", inst_true="ON", inst_false="OFF") @property def data_source(self): @@ -788,12 +774,12 @@ def data_source(self): :type: `TekDPO70000.Channel` or `TekDPO70000.Math` """ - val = self.query('DAT:SOU?') - if val[0:2] == 'CH': + val = self.query("DAT:SOU?") + if val[0:2] == "CH": out = self.channel[int(val[2]) - 1] - elif val[0:2] == 'MA': + elif val[0:2] == "MA": out = self.math[int(val[4]) - 1] - elif val[0:2] == 'RE': + elif val[0:2] == "RE": out = self.ref[int(val[3]) - 1] else: raise NotImplementedError @@ -802,141 +788,129 @@ def data_source(self): @data_source.setter def data_source(self, newval): if not isinstance(newval, self.DataSource): - raise TypeError( - "{} is not a valid data source.".format(type(newval))) - self.sendcmd("DAT:SOU {}".format(newval.name)) + raise TypeError(f"{type(newval)} is not a valid data source.") + self.sendcmd(f"DAT:SOU {newval.name}") # Some Tek scopes require this after the DAT:SOU command, or else # they will stop responding. - if not self._testing: - time.sleep(0.02) + time.sleep(0.02) horiz_acq_duration = unitful_property( - 'HOR:ACQDURATION', - pq.second, + "HOR:ACQDURATION", + u.second, readonly=True, doc=""" The duration of the acquisition. - """ + """, ) horiz_acq_length = int_property( - 'HOR:ACQLENGTH', + "HOR:ACQLENGTH", readonly=True, doc=""" The record length. - """ + """, ) - horiz_delay_mode = bool_property( - 'HOR:DEL:MOD', - inst_true='1', - inst_false='0' - ) + horiz_delay_mode = bool_property("HOR:DEL:MOD", inst_true="1", inst_false="0") horiz_delay_pos = unitful_property( - 'HOR:DEL:POS', - pq.percent, + "HOR:DEL:POS", + u.percent, doc=""" The percentage of the waveform that is displayed left of the center graticule. - """ + """, ) horiz_delay_time = unitful_property( - 'HOR:DEL:TIM', - pq.second, + "HOR:DEL:TIM", + u.second, doc=""" The base trigger delay time setting. - """ + """, ) horiz_interp_ratio = unitless_property( - 'HOR:MAI:INTERPR', + "HOR:MAI:INTERPR", readonly=True, doc=""" The ratio of interpolated points to measured points. - """ + """, ) horiz_main_pos = unitful_property( - 'HOR:MAI:POS', - pq.percent, + "HOR:MAI:POS", + u.percent, doc=""" The percentage of the waveform that is displayed left of the center graticule. - """ + """, ) - horiz_unit = string_property('HOR:MAI:UNI') + horiz_unit = string_property("HOR:MAI:UNI") - horiz_mode = enum_property( - 'HOR:MODE', - HorizontalMode - ) + horiz_mode = enum_property("HOR:MODE", HorizontalMode) horiz_record_length_lim = int_property( - 'HOR:MODE:AUTO:LIMIT', + "HOR:MODE:AUTO:LIMIT", doc=""" The recond length limit in samples. - """ + """, ) horiz_record_length = int_property( - 'HOR:MODE:RECO', + "HOR:MODE:RECO", doc=""" The recond length in samples. See `horiz_mode`; manual mode lets you change the record length, while the length is readonly for auto and constant mode. - """ + """, ) horiz_sample_rate = unitful_property( - 'HOR:MODE:SAMPLER', - pq.Hz, + "HOR:MODE:SAMPLER", + u.Hz, doc=""" The sample rate in samples per second. - """ + """, ) horiz_scale = unitful_property( - 'HOR:MODE:SCA', - pq.second, + "HOR:MODE:SCA", + u.second, doc=""" The horizontal scale in seconds per division. The horizontal scale is readonly when `horiz_mode` is manual. - """ + """, ) horiz_pos = unitful_property( - 'HOR:POS', - pq.percent, + "HOR:POS", + u.percent, doc=""" The position of the trigger point on the screen, left is 0%, right is 100%. - """ + """, ) horiz_roll = string_property( - 'HOR:ROLL', - bookmark_symbol='', + "HOR:ROLL", + bookmark_symbol="", doc=""" Valid arguments are AUTO, OFF, and ON. - """ + """, ) - trigger_state = enum_property( - 'TRIG:STATE', - TriggerState - ) + trigger_state = enum_property("TRIG:STATE", TriggerState) # Waveform Transfer Properties outgoing_waveform_encoding = enum_property( - 'WFMO:ENC', + "WFMO:ENC", WaveformEncoding, doc=""" Controls the encoding used for outgoing waveforms (instrument → host). - """ + """, ) outgoing_binary_format = enum_property( @@ -945,7 +919,7 @@ def data_source(self, newval): doc=""" Controls the data type of samples when transferring waveforms from the instrument to the host using binary encoding. - """ + """, ) outgoing_byte_order = enum_property( @@ -953,18 +927,18 @@ def data_source(self, newval): ByteOrder, doc=""" Controls whether binary data is returned in little or big endian. - """ + """, ) outgoing_n_bytes = int_property( "WFMO:BYT_N", - valid_set=set((1, 2, 4, 8)), + valid_set={1, 2, 4, 8}, doc=""" The number of bytes per sample used in representing outgoing waveforms in binary encodings. Must be either 1, 2, 4 or 8. - """ + """, ) # METHODS # @@ -980,7 +954,7 @@ def force_trigger(self): """ Forces a trigger event to happen for the oscilloscope. """ - self.sendcmd('TRIG FORC') + self.sendcmd("TRIG FORC") # TODO: consider moving the next few methods to Oscilloscope. def run(self): diff --git a/instruments/tektronix/tektds224.py b/instruments/tektronix/tektds224.py index ba7238d50..c041df77e 100644 --- a/instruments/tektronix/tektds224.py +++ b/instruments/tektronix/tektds224.py @@ -1,167 +1,162 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides support for the Tektronix TDS 224 oscilloscope """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division import time -from builtins import range, map from enum import Enum -import numpy as np -import quantities as pq - -from instruments.abstract_instruments import ( - OscilloscopeChannel, - OscilloscopeDataSource, - Oscilloscope, -) +from instruments.abstract_instruments import Oscilloscope from instruments.generic_scpi import SCPIInstrument +from instruments.optional_dep_finder import numpy from instruments.util_fns import ProxyList - -# CLASSES ##################################################################### +from instruments.units import ureg as u -class _TekTDS224DataSource(OscilloscopeDataSource): +# CLASSES ##################################################################### - """ - Class representing a data source (channel, math, or ref) on the Tektronix - TDS 224. - .. warning:: This class should NOT be manually created by the user. It is - designed to be initialized by the `TekTDS224` class. +class TekTDS224(SCPIInstrument, Oscilloscope): """ + The Tektronix TDS224 is a multi-channel oscilloscope with analog + bandwidths of 100MHz. - def __init__(self, tek, name): - super(_TekTDS224DataSource, self).__init__(tek, name) - self._tek = self._parent - - @property - def name(self): - """ - Gets the name of this data source, as identified over SCPI. - - :type: `str` - """ - return self._name - - def read_waveform(self, bin_format=True): - """ - Read waveform from the oscilloscope. - This function is all inclusive. After reading the data from the - oscilloscope, it unpacks the data and scales it accordingly. - - Supports both ASCII and binary waveform transfer. For 2500 data - points, with a width of 2 bytes, transfer takes approx 2 seconds for - binary, and 7 seconds for ASCII over Galvant Industries' GPIBUSB - adapter. - - Function returns a tuple (x,y), where both x and y are numpy arrays. - - :param bool bin_format: If `True`, data is transfered - in a binary format. Otherwise, data is transferred in ASCII. - - :rtype: two item `tuple` of `numpy.ndarray` - """ - with self: - - if not bin_format: - self._tek.sendcmd('DAT:ENC ASCI') - # Set the data encoding format to ASCII - raw = self._tek.query('CURVE?') - raw = raw.split(',') # Break up comma delimited string - raw = map(float, raw) # Convert each list element to int - raw = np.array(raw) # Convert into numpy array - else: - self._tek.sendcmd('DAT:ENC RIB') - # Set encoding to signed, big-endian - data_width = self._tek.data_width - self._tek.sendcmd('CURVE?') - raw = self._tek.binblockread( - data_width) # Read in the binary block, - # data width of 2 bytes - - # pylint: disable=protected-access - self._tek._file.flush_input() # Flush input buffer - - yoffs = self._tek.query( - 'WFMP:{}:YOF?'.format(self.name)) # Retrieve Y offset - ymult = self._tek.query( - 'WFMP:{}:YMU?'.format(self.name)) # Retrieve Y multiply - yzero = self._tek.query( - 'WFMP:{}:YZE?'.format(self.name)) # Retrieve Y zero - - y = ((raw - float(yoffs)) * float(ymult)) + float(yzero) - - xzero = self._tek.query('WFMP:XZE?') # Retrieve X zero - xincr = self._tek.query('WFMP:XIN?') # Retrieve X incr - ptcnt = self._tek.query( - 'WFMP:{}:NR_P?'.format(self.name)) # Retrieve number - # of data - # points - - x = np.arange(float(ptcnt)) * float(xincr) + float(xzero) - - return (x, y) - - -class _TekTDS224Channel(_TekTDS224DataSource, OscilloscopeChannel): - - """ - Class representing a channel on the Tektronix TDS 224. + This class inherits from `~instruments.generic_scpi.SCPIInstrument`. - This class inherits from `_TekTDS224DataSource`. + Example usage: - .. warning:: This class should NOT be manually created by the user. It is - designed to be initialized by the `TekTDS224` class. + >>> import instruments as ik + >>> tek = ik.tektronix.TekTDS224.open_gpibusb("/dev/ttyUSB0", 1) + >>> [x, y] = tek.channel[0].read_waveform() """ - def __init__(self, parent, idx): - super(_TekTDS224Channel, self).__init__(parent, "CH{}".format(idx + 1)) - self._idx = idx + 1 + def __init__(self, filelike): + super().__init__(filelike) + self._file.timeout = 3 * u.second - @property - def coupling(self): + class DataSource(Oscilloscope.DataSource): """ - Gets/sets the coupling setting for this channel. + Class representing a data source (channel, math, or ref) on the Tektronix + TDS 224. - :type: `TekTDS224.Coupling` + .. warning:: This class should NOT be manually created by the user. It is + designed to be initialized by the `TekTDS224` class. """ - return TekTDS224.Coupling( - self._tek.query("CH{}:COUPL?".format(self._idx)) - ) - - @coupling.setter - def coupling(self, newval): - if not isinstance(newval, TekTDS224.Coupling): - raise TypeError("Coupling setting must be a `TekTDS224.Coupling`" - " value, got {} instead.".format(type(newval))) - self._tek.sendcmd("CH{}:COUPL {}".format(self._idx, newval.value)) + def __init__(self, tek, name): + super().__init__(tek, name) + self._tek = self._parent + + @property + def name(self): + """ + Gets the name of this data source, as identified over SCPI. + + :type: `str` + """ + return self._name + + def read_waveform(self, bin_format=True): + """ + Read waveform from the oscilloscope. + This function is all inclusive. After reading the data from the + oscilloscope, it unpacks the data and scales it accordingly. + + Supports both ASCII and binary waveform transfer. For 2500 data + points, with a width of 2 bytes, transfer takes approx 2 seconds for + binary, and 7 seconds for ASCII over Galvant Industries' GPIBUSB + adapter. + + Function returns a tuple (x,y), where both x and y are numpy arrays. + + :param bool bin_format: If `True`, data is transfered + in a binary format. Otherwise, data is transferred in ASCII. + + :rtype: `tuple`[`tuple`[`float`, ...], `tuple`[`float`, ...]] + or if numpy is installed, `tuple`[`numpy.array`, `numpy.array`] + """ + with self: + + if not bin_format: + self._tek.sendcmd("DAT:ENC ASCI") + # Set the data encoding format to ASCII + raw = self._tek.query("CURVE?") + raw = raw.split(",") # Break up comma delimited string + if numpy: + raw = numpy.array(raw, dtype=numpy.float) # Convert to ndarray + else: + raw = tuple(map(float, raw)) + else: + self._tek.sendcmd("DAT:ENC RIB") + # Set encoding to signed, big-endian + data_width = self._tek.data_width + self._tek.sendcmd("CURVE?") + raw = self._tek.binblockread( + data_width + ) # Read in the binary block, + # data width of 2 bytes + + # pylint: disable=protected-access + self._tek._file.flush_input() # Flush input buffer + + yoffs = self._tek.query(f"WFMP:{self.name}:YOF?") # Retrieve Y offset + ymult = self._tek.query(f"WFMP:{self.name}:YMU?") # Retrieve Y multiply + yzero = self._tek.query(f"WFMP:{self.name}:YZE?") # Retrieve Y zero + + xzero = self._tek.query("WFMP:XZE?") # Retrieve X zero + xincr = self._tek.query("WFMP:XIN?") # Retrieve X incr + ptcnt = self._tek.query( + f"WFMP:{self.name}:NR_P?" + ) # Retrieve number of data points + + if numpy: + x = numpy.arange(float(ptcnt)) * float(xincr) + float(xzero) + y = ((raw - float(yoffs)) * float(ymult)) + float(yzero) + else: + x = tuple( + float(val) * float(xincr) + float(xzero) + for val in range(int(ptcnt)) + ) + y = tuple( + ((x - float(yoffs)) * float(ymult)) + float(yzero) for x in raw + ) + + return x, y + + class Channel(DataSource, Oscilloscope.Channel): + """ + Class representing a channel on the Tektronix TDS 224. -class TekTDS224(SCPIInstrument, Oscilloscope): + This class inherits from `TekTDS224.DataSource`. - """ - The Tektronix TDS224 is a multi-channel oscilloscope with analog - bandwidths of 100MHz. + .. warning:: This class should NOT be manually created by the user. It is + designed to be initialized by the `TekTDS224` class. + """ - This class inherits from `~instruments.generic_scpi.SCPIInstrument`. + def __init__(self, parent, idx): + super().__init__(parent, f"CH{idx + 1}") + self._idx = idx + 1 - Example usage: + @property + def coupling(self): + """ + Gets/sets the coupling setting for this channel. - >>> import instruments as ik - >>> tek = ik.tektronix.TekTDS224.open_gpibusb("/dev/ttyUSB0", 1) - >>> [x, y] = tek.channel[0].read_waveform() - """ + :type: `TekTDS224.Coupling` + """ + return TekTDS224.Coupling(self._tek.query(f"CH{self._idx}:COUPL?")) - def __init__(self, filelike): - super(TekTDS224, self).__init__(filelike) - self._file.timeout = 3 * pq.second + @coupling.setter + def coupling(self, newval): + if not isinstance(newval, TekTDS224.Coupling): + raise TypeError( + f"Coupling setting must be a `TekTDS224.Coupling` value," + f"got {type(newval)} instead." + ) + self._tek.sendcmd(f"CH{self._idx}:COUPL {newval.value}") # ENUMS # @@ -169,6 +164,7 @@ class Coupling(Enum): """ Enum containing valid coupling modes for the Tek TDS224 """ + ac = "AC" dc = "DC" ground = "GND" @@ -187,9 +183,9 @@ def channel(self): >>> tek = ik.tektronix.TekTDS224.open_tcpip('192.168.0.2', 8888) >>> [x, y] = tek.channel[0].read_waveform() - :rtype: `_TekTDS224Channel` + :rtype: `TekTDS224.Channel` """ - return ProxyList(self, _TekTDS224Channel, range(4)) + return ProxyList(self, self.Channel, range(4)) @property def ref(self): @@ -203,21 +199,20 @@ def ref(self): >>> tek = ik.tektronix.TekTDS224.open_tcpip('192.168.0.2', 8888) >>> [x, y] = tek.ref[0].read_waveform() - :rtype: `_TekTDS224DataSource` + :rtype: `TekTDS224.DataSource` """ - return ProxyList(self, - lambda s, idx: _TekTDS224DataSource( - s, "REF{}".format(idx + 1)), - range(4)) + return ProxyList( + self, lambda s, idx: self.DataSource(s, f"REF{idx + 1}"), range(4) + ) @property def math(self): """ Gets a data source object corresponding to the MATH channel. - :rtype: `_TekTDS224DataSource` + :rtype: `TekTDS224.DataSource` """ - return _TekTDS224DataSource(self, "MATH") + return self.DataSource(self, "MATH") @property def data_source(self): @@ -226,9 +221,9 @@ def data_source(self): """ name = self.query("DAT:SOU?") if name.startswith("CH"): - return _TekTDS224Channel(self, int(name[2:]) - 1) - else: - return _TekTDS224DataSource(self, name) + return self.Channel(self, int(name[2:]) - 1) + + return self.DataSource(self, name) @data_source.setter def data_source(self, newval): @@ -238,9 +233,8 @@ def data_source(self, newval): newval = newval.value elif hasattr(newval, "name"): # Is a datasource with a name. newval = newval.name - self.sendcmd("DAT:SOU {}".format(newval)) - if not self._testing: - time.sleep(0.01) # Let the instrument catch up. + self.sendcmd(f"DAT:SOU {newval}") + time.sleep(0.01) # Let the instrument catch up. @property def data_width(self): @@ -257,8 +251,7 @@ def data_width(self, newval): if int(newval) not in [1, 2]: raise ValueError("Only one or two byte-width is supported.") - self.sendcmd("DATA:WIDTH {}".format(newval)) + self.sendcmd(f"DATA:WIDTH {newval}") - @property def force_trigger(self): raise NotImplementedError diff --git a/instruments/tektronix/tektds5xx.py b/instruments/tektronix/tektds5xx.py index d41c930c3..5f83dcd66 100644 --- a/instruments/tektronix/tektds5xx.py +++ b/instruments/tektronix/tektds5xx.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # # tektds5xx.py: Driver for the Tektronix TDS 5xx series oscilloscope. # @@ -33,248 +32,256 @@ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division -from functools import reduce - -import time -from time import sleep from datetime import datetime +from enum import Enum +from functools import reduce import operator import struct +import time -from builtins import range, map, round -from enum import Enum - -import numpy as np -from instruments.abstract_instruments import ( - OscilloscopeChannel, - OscilloscopeDataSource, - Oscilloscope, -) +from instruments.abstract_instruments import Oscilloscope from instruments.generic_scpi import SCPIInstrument +from instruments.optional_dep_finder import numpy from instruments.util_fns import ProxyList # CLASSES ##################################################################### -class _TekTDS5xxMeasurement(object): +class TekTDS5xx(SCPIInstrument, Oscilloscope): """ - Class representing a measurement channel on the Tektronix TDS5xx + Support for the TDS5xx series of oscilloscopes + Implemented from: + | TDS Family Digitizing Oscilloscopes + | (TDS 410A, 420A, 460A, 520A, 524A, 540A, 544A, + | 620A, 640A, 644A, 684A, 744A & 784A) + | Tektronix Document: 070-8709-07 """ - def __init__(self, tek, idx): - self._tek = tek - self._id = idx + 1 - resp = self._tek.query('MEASU:MEAS{}?'.format(self._id)) - self._data = dict(zip(['enabled', 'type', 'units', 'src1', 'src2', - 'edge1', 'edge2', 'dir'], resp.split(';'))) - - def read(self): - """ - Gets the current measurement value of the channel, and returns a dict - of all relevent information + class Measurement: + + """ + Class representing a measurement channel on the Tektronix TDS5xx + """ + + def __init__(self, tek, idx): + self._tek = tek + self._id = idx + 1 + resp = self._tek.query(f"MEASU:MEAS{self._id}?") + self._data = dict( + zip( + [ + "enabled", + "type", + "units", + "src1", + "src2", + "edge1", + "edge2", + "dir", + ], + resp.split(";"), + ) + ) + + def read(self): + """ + Gets the current measurement value of the channel, and returns a dict + of all relevant information + + :rtype: `dict` of measurement parameters + """ + if int(self._data["enabled"]): + resp = self._tek.query(f"MEASU:MEAS{self._id}:VAL?") + self._data["value"] = float(resp) + return self._data - :rtype: `dict` of measurement parameters - """ - if int(self._data['enabled']): - resp = self._tek.query('MEASU:MEAS{}:VAL?'.format(self._id)) - self._data['value'] = float(resp) return self._data - else: - return self._data - -class _TekTDS5xxDataSource(OscilloscopeDataSource): + class DataSource(Oscilloscope.DataSource): - """ - Class representing a data source (channel, math, or ref) on the Tektronix - TDS 5xx. - - .. warning:: This class should NOT be manually created by the user. It is - designed to be initialized by the `TekTDS5xx` class. - """ - - def __init__(self, parent, name): - super(_TekTDS5xxDataSource, self).__init__(parent, name) - - @property - def name(self): """ - Gets the name of this data source, as identified over SCPI. + Class representing a data source (channel, math, or ref) on the Tektronix + TDS 5xx. - :type: `str` + .. warning:: This class should NOT be manually created by the user. It is + designed to be initialized by the `TekTDS5xx` class. """ - return self._name - def read_waveform(self, bin_format=True): - """ - Read waveform from the oscilloscope. - This function is all inclusive. After reading the data from the - oscilloscope, it unpacks the data and scales it accordingly. + @property + def name(self): + """ + Gets the name of this data source, as identified over SCPI. + + :type: `str` + """ + return self._name - Supports both ASCII and binary waveform transfer. For 2500 data - points, with a width of 2 bytes, transfer takes approx 2 seconds for - binary, and 7 seconds for ASCII over Galvant Industries' GPIBUSB - adapter. + def read_waveform(self, bin_format=True): + """ + Read waveform from the oscilloscope. + This function is all inclusive. After reading the data from the + oscilloscope, it unpacks the data and scales it accordingly. - Function returns a tuple (x,y), where both x and y are numpy arrays. + Supports both ASCII and binary waveform transfer. For 2500 data + points, with a width of 2 bytes, transfer takes approx 2 seconds for + binary, and 7 seconds for ASCII over Galvant Industries' GPIBUSB + adapter. - :param bool bin_format: If `True`, data is transfered - in a binary format. Otherwise, data is transferred in ASCII. + Function returns a tuple (x,y), where both x and y are numpy arrays. + + :param bool bin_format: If `True`, data is transfered + in a binary format. Otherwise, data is transferred in ASCII. + + :rtype: `tuple`[`tuple`[`float`, ...], `tuple`[`float`, ...]] + or if numpy is installed, `tuple`[`numpy.array`, `numpy.array`] + """ + with self: + + if not bin_format: + # Set the data encoding format to ASCII + self._parent.sendcmd("DAT:ENC ASCI") + raw = self._parent.query("CURVE?") + raw = raw.split(",") # Break up comma delimited string + if numpy: + raw = numpy.array( + raw, dtype=numpy.float + ) # Convert to numpy array + else: + raw = map(float, raw) + else: + # Set encoding to signed, big-endian + self._parent.sendcmd("DAT:ENC RIB") + data_width = self._parent.data_width + self._parent.sendcmd("CURVE?") + # Read in the binary block, data width of 2 bytes + raw = self._parent.binblockread(data_width) + + # pylint: disable=protected-access + # read line separation character + self._parent._file.read_raw(1) + + # Retrieve Y offset + yoffs = float(self._parent.query(f"WFMP:{self.name}:YOF?")) + # Retrieve Y multiply + ymult = float(self._parent.query(f"WFMP:{self.name}:YMU?")) + # Retrieve Y zero + yzero = float(self._parent.query(f"WFMP:{self.name}:YZE?")) + + # Retrieve X incr + xincr = float(self._parent.query(f"WFMP:{self.name}:XIN?")) + # Retrieve number of data points + ptcnt = int(self._parent.query(f"WFMP:{self.name}:NR_P?")) + + if numpy: + x = numpy.arange(float(ptcnt)) * float(xincr) + y = ((raw - yoffs) * float(ymult)) + float(yzero) + else: + x = tuple(float(val) * float(xincr) for val in range(ptcnt)) + y = tuple(((x - yoffs) * float(ymult)) + float(yzero) for x in raw) + + return x, y + + class Channel(DataSource, Oscilloscope.Channel): + + """ + Class representing a channel on the Tektronix TDS 5xx. + + This class inherits from `TekTDS5xx.DataSource`. + + .. warning:: This class should NOT be manually created by the user. It is + designed to be initialized by the `TekTDS5xx` class. + """ + + def __init__(self, parent, idx): + super().__init__(parent, f"CH{idx + 1}") + self._idx = idx + 1 + + @property + def coupling(self): + """ + Gets/sets the coupling setting for this channel. + + :type: `TekTDS5xx.Coupling` + """ + return TekTDS5xx.Coupling(self._parent.query(f"CH{self._idx}:COUPL?")) + + @coupling.setter + def coupling(self, newval): + if not isinstance(newval, TekTDS5xx.Coupling): + raise TypeError( + "Coupling setting must be a `TekTDS5xx.Coupling`" + " value, got {} instead.".format(type(newval)) + ) + + self._parent.sendcmd(f"CH{self._idx}:COUPL {newval.value}") + + @property + def bandwidth(self): + """ + Gets/sets the Bandwidth setting for this channel. + + :type: `TekTDS5xx.Bandwidth` + """ + return TekTDS5xx.Bandwidth(self._parent.query(f"CH{self._idx}:BAND?")) + + @bandwidth.setter + def bandwidth(self, newval): + if not isinstance(newval, TekTDS5xx.Bandwidth): + raise TypeError( + "Bandwidth setting must be a `TekTDS5xx.Bandwidth`" + " value, got {} instead.".format(type(newval)) + ) + + self._parent.sendcmd(f"CH{self._idx}:BAND {newval.value}") + + @property + def impedance(self): + """ + Gets/sets the impedance setting for this channel. + + :type: `TekTDS5xx.Impedance` + """ + return TekTDS5xx.Impedance(self._parent.query(f"CH{self._idx}:IMP?")) + + @impedance.setter + def impedance(self, newval): + if not isinstance(newval, TekTDS5xx.Impedance): + raise TypeError( + "Impedance setting must be a `TekTDS5xx.Impedance`" + " value, got {} instead.".format(type(newval)) + ) + + self._parent.sendcmd(f"CH{self._idx}:IMP {newval.value}") + + @property + def probe(self): + """ + Gets the connected probe value for this channel + + :type: `float` + """ + return round(1 / float(self._parent.query(f"CH{self._idx}:PRO?")), 0) + + @property + def scale(self): + """ + Gets/sets the scale setting for this channel. - :rtype: two item `tuple` of `numpy.ndarray` - """ - with self: - - if not bin_format: - # Set the data encoding format to ASCII - self._parent.sendcmd('DAT:ENC ASCI') - raw = self._parent.query('CURVE?') - raw = raw.split(',') # Break up comma delimited string - raw = map(float, raw) # Convert each list element to int - raw = np.array(raw) # Convert into numpy array - else: - # Set encoding to signed, big-endian - self._parent.sendcmd('DAT:ENC RIB') - data_width = self._parent.data_width - self._parent.sendcmd('CURVE?') - # Read in the binary block, data width of 2 bytes - raw = self._parent.binblockread(data_width) - - # pylint: disable=protected-access - self._parent._file.flush_input() # Flush input buffer - - # Retrieve Y offset - yoffs = self._parent.query('WFMP:{}:YOF?'.format(self.name)) - # Retrieve Y multiply - ymult = self._parent.query('WFMP:{}:YMU?'.format(self.name)) - # Retrieve Y zero - yzero = self._parent.query('WFMP:{}:YZE?'.format(self.name)) - - y = ((raw - float(yoffs)) * float(ymult)) + float(yzero) - - # Retrieve X incr - xincr = self._parent.query('WFMP:{}:XIN?'.format(self.name)) - # Retrieve number of data points - ptcnt = self._parent.query('WFMP:{}:NR_P?'.format(self.name)) - - x = np.arange(float(ptcnt)) * float(xincr) - - return (x, y) - - -class _TekTDS5xxChannel(_TekTDS5xxDataSource, OscilloscopeChannel): - - """ - Class representing a channel on the Tektronix TDS 5xx. - - This class inherits from `_TekTDS5xxDataSource`. + :type: `float` + """ + return float(self._parent.query(f"CH{self._idx}:SCA?")) - .. warning:: This class should NOT be manually created by the user. It is - designed to be initialized by the `TekTDS5xx` class. - """ - - def __init__(self, parent, idx): - super(_TekTDS5xxChannel, self).__init__(parent, "CH{}".format(idx + 1)) - self._idx = idx + 1 - - @property - def coupling(self): - """ - Gets/sets the coupling setting for this channel. - - :type: `TekTDS5xx.Coupling` - """ - return TekTDS5xx.Coupling( - self._parent.query("CH{}:COUPL?".format(self._idx)) - ) - - @coupling.setter - def coupling(self, newval): - if not isinstance(newval, TekTDS5xx.Coupling): - raise TypeError("Coupling setting must be a `TekTDS5xx.Coupling`" - " value, got {} instead.".format(type(newval))) - - self._parent.sendcmd("CH{}:COUPL {}".format(self._idx, newval.value)) - - @property - def bandwidth(self): - """ - Gets/sets the Bandwidth setting for this channel. - - :type: `TekTDS5xx.Bandwidth` - """ - return TekTDS5xx.Bandwidth( - self._parent.query("CH{}:BAND?".format(self._idx)) - ) - - @bandwidth.setter - def bandwidth(self, newval): - if not isinstance(newval, TekTDS5xx.Bandwidth): - raise TypeError("Bandwidth setting must be a `TekTDS5xx.Bandwidth`" - " value, got {} instead.".format(type(newval))) - - self._parent.sendcmd("CH{}:BAND {}".format(self._idx, newval.value)) - - @property - def impedance(self): - """ - Gets/sets the impedance setting for this channel. - - :type: `TekTDS5xx.Impedance` - """ - return TekTDS5xx.Impedance( - self._parent.query("CH{}:IMP?".format(self._idx)) - ) - - @impedance.setter - def impedance(self, newval): - if not isinstance(newval, TekTDS5xx.Impedance): - raise TypeError("Impedance setting must be a `TekTDS5xx.Impedance`" - " value, got {} instead.".format(type(newval))) - - self._parent.sendcmd("CH{}:IMP {}".format(self._idx, newval.value)) - - @property - def probe(self): - """ - Gets the connected probe value for this channel - - :type: `float` - """ - return round(1 / float(self._parent.query("CH{}:PRO?".format(self._idx))), 0) - - @property - def scale(self): - """ - Gets/sets the scale setting for this channel. - - :type: `TekTDS5xx.Impedance` - """ - return float(self._parent.query("CH{}:SCA?".format(self._idx))) - - @scale.setter - def scale(self, newval): - self._parent.sendcmd("CH{0}:SCA {1:.3E}".format(self._idx, newval)) - resp = float(self._parent.query("CH{}:SCA?".format(self._idx))) - if newval != resp: - raise ValueError("Tried to set CH{0} Scale to {1} but got {2}" - " instead".format(self._idx, newval, resp)) - - -class TekTDS5xx(SCPIInstrument, Oscilloscope): - - """ - Support for the TDS5xx series of oscilloscopes - Implemented from: - | TDS Family Digitizing Oscilloscopes - | (TDS 410A, 420A, 460A, 520A, 524A, 540A, 544A, - | 620A, 640A, 644A, 684A, 744A & 784A) - | Tektronix Document: 070-8709-07 - """ + @scale.setter + def scale(self, newval): + self._parent.sendcmd(f"CH{self._idx}:SCA {newval:.3E}") + resp = float(self._parent.query(f"CH{self._idx}:SCA?")) + if newval != resp: + raise ValueError( + "Tried to set CH{} Scale to {} but got {}" + " instead".format(self._idx, newval, resp) + ) # ENUMS ## @@ -283,6 +290,7 @@ class Coupling(Enum): """ Available coupling options for input sources and trigger """ + ac = "AC" dc = "DC" ground = "GND" @@ -292,6 +300,7 @@ class Bandwidth(Enum): """ Bandwidth in MHz """ + Twenty = "TWE" OneHundred = "HUN" TwoHundred = "TWO" @@ -302,6 +311,7 @@ class Impedance(Enum): """ Available options for input source impedance """ + Fifty = "FIF" OneMeg = "MEG" @@ -310,8 +320,9 @@ class Edge(Enum): """ Available Options for trigger slope """ - Rising = 'RIS' - Falling = 'FALL' + + Rising = "RIS" + Falling = "FALL" class Trigger(Enum): @@ -319,18 +330,20 @@ class Trigger(Enum): Available Trigger sources (AUX not Available on TDS520A/TDS540A) """ - CH1 = 'CH1' - CH2 = 'CH2' - CH3 = 'CH3' - CH4 = 'CH4' - AUX = 'AUX' - LINE = 'LINE' + + CH1 = "CH1" + CH2 = "CH2" + CH3 = "CH3" + CH4 = "CH4" + AUX = "AUX" + LINE = "LINE" class Source(Enum): """ Available Data sources """ + CH1 = "CH1" CH2 = "CH2" CH3 = "CH3" @@ -350,9 +363,9 @@ def measurement(self): Gets a specific oscilloscope measurement object. The desired channel is specified like one would access a list. - :rtype: `_TDS5xxMeasurement` + :rtype: `TekTDS5xx.Measurement` """ - return ProxyList(self, _TekTDS5xxMeasurement, range(3)) + return ProxyList(self, self.Measurement, range(3)) @property def channel(self): @@ -365,9 +378,9 @@ def channel(self): >>> tek = ik.tektronix.TekTDS5xx.open_tcpip('192.168.0.2', 8888) >>> [x, y] = tek.channel[0].read_waveform() - :rtype: `_TekTDS5xxChannel` + :rtype: `TekTDS5xx.Channel` """ - return ProxyList(self, _TekTDS5xxChannel, range(4)) + return ProxyList(self, self.Channel, range(4)) @property def ref(self): @@ -380,12 +393,12 @@ def ref(self): >>> tek = ik.tektronix.TekTDS5xx.open_tcpip('192.168.0.2', 8888) >>> [x, y] = tek.ref[0].read_waveform() - :rtype: `_TekTDS5xxDataSource` + :rtype: `TekTDS5xx.DataSource` """ return ProxyList( self, - lambda s, idx: _TekTDS5xxDataSource(s, "REF{}".format(idx + 1)), - range(4) + lambda s, idx: self.DataSource(s, f"REF{idx + 1}"), + range(4), ) @property @@ -393,12 +406,12 @@ def math(self): """ Gets a data source object corresponding to the MATH channel. - :rtype: `_TekTDS5xxDataSource` + :rtype: `TekTDS5xx.DataSource` """ return ProxyList( self, - lambda s, idx: _TekTDS5xxDataSource(s, "MATH{}".format(idx + 1)), - range(3) + lambda s, idx: self.DataSource(s, f"MATH{idx + 1}"), + range(3), ) @property @@ -409,18 +422,16 @@ def sources(self): :rtype: `list` """ active = [] - channels = map(int, self.query('SEL?').split(';')[0:11]) + channels = list(map(int, self.query("SEL?").split(";")[0:11])) for idx in range(0, 4): if channels[idx]: - active.append(_TekTDS5xxChannel(self, idx)) + active.append(self.Channel(self, idx)) for idx in range(4, 7): if channels[idx]: - active.append(_TekTDS5xxDataSource(self, "MATH{}".format( - idx - 3))) + active.append(self.DataSource(self, f"MATH{idx - 3}")) for idx in range(7, 11): if channels[idx]: - active.append( - _TekTDS5xxDataSource(self, "REF{}".format(idx - 6))) + active.append(self.DataSource(self, f"REF{idx - 6}")) return active @property @@ -428,24 +439,26 @@ def data_source(self): """ Gets/sets the the data source for waveform transfer. - :type: `TekTDS5xx.Source` or `_TekTDS5xxDataSource` - :rtype: '_TekTDS5xxDataSource` + :type: `TekTDS5xx.Source` or `TekTDS5xx.DataSource` + :rtype: `TekTDS5xx.DataSource` """ name = self.query("DAT:SOU?") if name.startswith("CH"): - return _TekTDS5xxChannel(self, int(name[2:]) - 1) - else: - return _TekTDS5xxDataSource(self, name) + return self.Channel(self, int(name[2:]) - 1) + + return self.DataSource(self, name) @data_source.setter def data_source(self, newval): - if isinstance(newval, _TekTDS5xxDataSource): - newval = TekTDS5xx.Source[newval.name] + if isinstance(newval, self.DataSource): + newval = TekTDS5xx.Source(newval.name) if not isinstance(newval, TekTDS5xx.Source): - raise TypeError("Source setting must be a `TekTDS5xx.Source`" - " value, got {} instead.".format(type(newval))) + raise TypeError( + "Source setting must be a `TekTDS5xx.Source`" + " value, got {} instead.".format(type(newval)) + ) - self.sendcmd("DAT:SOU {}".format(newval.value)) + self.sendcmd(f"DAT:SOU {newval.value}") time.sleep(0.01) # Let the instrument catch up. @property @@ -462,9 +475,8 @@ def data_width(self, newval): if int(newval) not in [1, 2]: raise ValueError("Only one or two byte-width is supported.") - self.sendcmd("DATA:WIDTH {}".format(newval)) + self.sendcmd(f"DATA:WIDTH {newval}") - @property def force_trigger(self): raise NotImplementedError @@ -475,15 +487,17 @@ def horizontal_scale(self): :type: `float` """ - return float(self.query('HOR:MAI:SCA?')) + return float(self.query("HOR:MAI:SCA?")) @horizontal_scale.setter def horizontal_scale(self, newval): - self.sendcmd("HOR:MAI:SCA {0:.3E}".format(newval)) - resp = float(self.query('HOR:MAI:SCA?')) + self.sendcmd(f"HOR:MAI:SCA {newval:.3E}") + resp = float(self.query("HOR:MAI:SCA?")) if newval != resp: - raise ValueError("Tried to set Horizontal Scale to {} but got {}" - " instead".format(newval, resp)) + raise ValueError( + "Tried to set Horizontal Scale to {} but got {}" + " instead".format(newval, resp) + ) @property def trigger_level(self): @@ -492,15 +506,17 @@ def trigger_level(self): :type: `float` """ - return float(self.query('TRIG:MAI:LEV?')) + return float(self.query("TRIG:MAI:LEV?")) @trigger_level.setter def trigger_level(self, newval): - self.sendcmd("TRIG:MAI:LEV {0:.3E}".format(newval)) - resp = float(self.query('TRIG:MAI:LEV?')) + self.sendcmd(f"TRIG:MAI:LEV {newval:.3E}") + resp = float(self.query("TRIG:MAI:LEV?")) if newval != resp: - raise ValueError("Tried to set trigger level to {} but got {}" - " instead".format(newval, resp)) + raise ValueError( + "Tried to set trigger level to {} but got {}" + " instead".format(newval, resp) + ) @property def trigger_coupling(self): @@ -509,15 +525,17 @@ def trigger_coupling(self): :type: `TekTDS5xx.Coupling` """ - return TekTDS5xx.Coupling[self.query("TRIG:MAI:EDGE:COUP?")] + return TekTDS5xx.Coupling(self.query("TRIG:MAI:EDGE:COUP?")) @trigger_coupling.setter def trigger_coupling(self, newval): if not isinstance(newval, TekTDS5xx.Coupling): - raise TypeError("Coupling setting must be a `TekTDS5xx.Coupling`" - " value, got {} instead.".format(type(newval))) + raise TypeError( + "Coupling setting must be a `TekTDS5xx.Coupling`" + " value, got {} instead.".format(type(newval)) + ) - self.sendcmd("TRIG:MAI:EDGE:COUP {}".format(newval.value)) + self.sendcmd(f"TRIG:MAI:EDGE:COUP {newval.value}") @property def trigger_slope(self): @@ -531,10 +549,12 @@ def trigger_slope(self): @trigger_slope.setter def trigger_slope(self, newval): if not isinstance(newval, TekTDS5xx.Edge): - raise TypeError("Edge setting must be a `TekTDS5xx.Edge`" - " value, got {} instead.".format(type(newval))) + raise TypeError( + "Edge setting must be a `TekTDS5xx.Edge`" + " value, got {} instead.".format(type(newval)) + ) - self.sendcmd("TRIG:MAI:EDGE:SLO {}".format(newval.value)) + self.sendcmd(f"TRIG:MAI:EDGE:SLO {newval.value}") @property def trigger_source(self): @@ -548,10 +568,13 @@ def trigger_source(self): @trigger_source.setter def trigger_source(self, newval): if not isinstance(newval, TekTDS5xx.Trigger): - raise TypeError("Trigger source setting must be a" - "`TekTDS5xx.source` value, got {} instead.".format(type(newval))) + raise TypeError( + "Trigger source setting must be a " + "`TekTDS5xx.Trigger` value, got {} " + "instead.".format(type(newval)) + ) - self.sendcmd("TRIG:MAI:EDGE:SOU {}".format(newval.value)) + self.sendcmd(f"TRIG:MAI:EDGE:SOU {newval.value}") @property def clock(self): @@ -560,14 +583,15 @@ def clock(self): :type: `datetime.datetime` """ - resp = self.query('DATE?;:TIME?') + resp = self.query("DATE?;:TIME?") return datetime.strptime(resp, '"%Y-%m-%d";"%H:%M:%S"') @clock.setter def clock(self, newval): if not isinstance(newval, datetime): - raise ValueError("Expected datetime.datetime" - "but got {} instead".format(type(newval))) + raise ValueError( + "Expected datetime.datetime " "but got {} instead".format(type(newval)) + ) self.sendcmd(newval.strftime('DATE "%Y-%m-%d";:TIME "%H:%M:%S"')) @property @@ -577,14 +601,13 @@ def display_clock(self): :type: `bool` """ - return bool(int(self.query('DISPLAY:CLOCK?'))) + return bool(int(self.query("DISPLAY:CLOCK?"))) @display_clock.setter def display_clock(self, newval): if not isinstance(newval, bool): - raise ValueError("Expected bool but got" - "{} instead".format(type(newval))) - self.sendcmd('DISPLAY:CLOCK {}'.format(int(newval))) + raise ValueError("Expected bool but got " "{} instead".format(type(newval))) + self.sendcmd(f"DISPLAY:CLOCK {int(newval)}") def get_hardcopy(self): """ @@ -592,15 +615,14 @@ def get_hardcopy(self): :rtype: `string` """ - self.sendcmd('HARDC:PORT GPI;HARDC:LAY PORT;:HARDC:FORM BMP') - self.sendcmd('HARDC START') - sleep(1) - header = self.query("", size=54) + self.sendcmd("HARDC:PORT GPI;HARDC:LAY PORT;:HARDC:FORM BMP") + self.sendcmd("HARDC START") + time.sleep(1) + header = self._file.read_raw(size=54) # Get BMP Length in kilobytes from DIB header, because file header is # bad - length = reduce( - operator.mul, struct.unpack('>> import instruments as ik + >>> import instruments.units as u + >>> inst = ik.teledyne.MAUI.open_visa("TCPIP0::192.168.0.10::INSTR") + >>> # start the trigger in automatic mode + >>> inst.run() + >>> print(inst.trigger_state) # print the trigger state + + >>> # set timebase to 20 ns per division + >>> inst.time_div = u.Quantity(20, u.ns) + >>> # call the first oscilloscope channel + >>> channel = inst.channel[0] + >>> channel.trace = True # turn the trace on + >>> channel.coupling = channel.Coupling.dc50 # coupling to 50 Ohm + >>> channel.scale = u.Quantity(1, u.V) # vertical scale to 1V/division + >>> # transfer a waveform into xdat and ydat: + >>> xdat, ydat = channel.read_waveform() + """ + + # CONSTANTS # + + # number of horizontal and vertical divisions on the scope + # HOR_DIVS = 10 + # VERT_DIVS = 8 + + def __init__(self, filelike): + super().__init__(filelike) + + # turn off command headers -> for SCPI like behavior + self.sendcmd("COMM_HEADER OFF") + + # constants + self._number_channels = 4 + self._number_functions = 2 + self._number_measurements = 6 + + # ENUMS # + + class MeasurementParameters(Enum): + """ + Enum containing valid measurement parameters that only require + one or more sources. Only single source parameters are currently + implemented. + """ + + amplitude = "AMPL" + area = "AREA" + base = "BASE" + delay = "DLY" + duty_cycle = "DUTY" + fall_time_80_20 = "FALL82" + fall_time_90_10 = "FALL" + frequency = "FREQ" + maximum = "MAX" + minimum = "MIN" + mean = "MEAN" + none = "NULL" + overshoot_pos = "OVSP" + overshoot_neg = "OVSN" + peak_to_peak = "PKPK" + period = "PER" + phase = "PHASE" + rise_time_20_80 = "RISE28" + rise_time_10_90 = "RISE" + rms = "RMS" + stdev = "SDEV" + top = "TOP" + width_50_pos = "WID" + width_50_neg = "WIDN" + + class TriggerState(Enum): + + """ + Enum containing valid trigger state for the oscilloscope. + """ + + auto = "AUTO" + normal = "NORM" + single = "SINGLE" + stop = "STOP" + + class TriggerType(Enum): + """Enum containing valid trigger state. + + Availability depends on oscilloscope options. Please consult + your manual. Only simple types are currently included. + + .. warning:: Some of the trigger types are untested and might + need further parameters in order to be appropriately set. + """ + + dropout = "DROPOUT" + edge = "EDGE" + glitch = "GLIT" + interval = "INTV" + pattern = "PA" + runt = "RUNT" + slew_rate = "SLEW" + width = "WIDTH" + qualified = "TEQ" + tv = "TV" + + class TriggerSource(Enum): + """Enum containing valid trigger sources. + + This is an enum for the default values. + + .. note:: This class is initialized like this for four channels, + which is the default setting. If you change the number of + channels, `TriggerSource` will be recreated using the + routine `_create_trigger_source_enum`. This will make + further channels available to you or remove channels that + are not present in your setup. + """ + + c0 = "C1" + c1 = "C2" + c2 = "C3" + c3 = "C4" + ext = "EX" + ext5 = "EX5" + ext10 = "EX10" + etm10 = "ETM10" + line = "LINE" + + def _create_trigger_source_enum(self): + """Create an Enum for the trigger source class. + + Needs to be dynamically generated, in case channel number + changes! + + .. note:: Not all trigger sources are available on all scopes. + Please consult the manual for your oscilloscope. + """ + names = ["ext", "ext5", "ext10", "etm10", "line"] + values = ["EX", "EX5", "EX10", "ETM10", "LINE"] + # now add the channels + for it in range(self._number_channels): + names.append(f"c{it}") + values.append(f"C{it + 1}") # to send to scope + # create and store the enum + self.TriggerSource = Enum("TriggerSource", zip(names, values)) + + # CLASSES # + + class DataSource(Oscilloscope.DataSource): + + """ + Class representing a data source (channel, math, ref) on a MAUI + oscilloscope. + + .. warning:: This class should NOT be manually created by the + user. It is designed to be initialized by the `MAUI` class. + """ + + # PROPERTIES # + + @property + def name(self): + return self._name + + # METHODS # + + def read_waveform(self, bin_format=False, single=True): + """ + Reads the waveform and returns an array of floats with the + data. + + :param bin_format: Not implemented, always False + :type bin_format: bool + :param single: Run a single trigger? Default True. In case + a waveform from a channel is required, this option + is recommended to be set to True. This means that the + acquisition system is first stopped, a single trigger + is issued, then the waveform is transfered, and the + system is set back into the state it was in before. + If sampling math with multiple samples, set this to + false, otherwise the sweeps are cleared by the + oscilloscope prior when a single trigger command is + issued. + :type single: bool + + :return: Data (time, signal) where time is in seconds and + signal in V + :rtype: `tuple`[`tuple`[`~pint.Quantity`, ...], `tuple`[`~pint.Quantity`, ...]] + or if numpy is installed, `tuple`[`numpy.array`, `numpy.array`] + + :raises NotImplementedError: Bin format was chosen, but + it is not implemented. + + Example usage: + >>> import instruments as ik + >>> import instruments.units as u + >>> inst = ik.teledyne.MAUI.open_visa("TCPIP0::192.168.0.10::INSTR") + >>> channel = inst.channel[0] # set up channel + >>> xdat, ydat = channel.read_waveform() # read waveform + """ + if bin_format: + raise NotImplementedError( + "Bin format reading is currently " + "not implemented for the MAUI " + "routine." + ) + + if single: + # get current trigger state (to reset after read) + trig_state = self._parent.trigger_state + # trigger state to single + self._parent.trigger_state = self._parent.TriggerState.single + + # now read the data + retval = self.query("INSPECT? 'SIMPLE'") # pylint: disable=E1101 + + # read the parameters to create time-base array + horiz_off = self.query("INSPECT? 'HORIZ_OFFSET'") # pylint: disable=E1101 + horiz_int = self.query("INSPECT? 'HORIZ_INTERVAL'") # pylint: disable=E1101 + + if single: + # reset trigger + self._parent.trigger_state = trig_state + + # format the string to appropriate data + retval = retval.replace('"', "").split() + if numpy: + dat_val = numpy.array(retval, dtype=numpy.float) # Convert to ndarray + else: + dat_val = tuple(map(float, retval)) + + # format horizontal data into floats + horiz_off = float(horiz_off.replace('"', "").split(":")[1]) + horiz_int = float(horiz_int.replace('"', "").split(":")[1]) + + # create time base + if numpy: + dat_time = numpy.arange( + horiz_off, horiz_off + horiz_int * (len(dat_val)), horiz_int + ) + else: + dat_time = tuple( + val * horiz_int + horiz_off for val in range(len(dat_val)) + ) + + # fix length bug, sometimes dat_time is longer than dat_signal + if len(dat_time) > len(dat_val): + dat_time = dat_time[0 : len(dat_val)] + else: # in case the opposite is the case + dat_val = dat_val[0 : len(dat_time)] + + if numpy: + return numpy.stack((dat_time, dat_val)) + else: + return dat_time, dat_val + + trace = bool_property( + command="TRA", + doc=""" + Gets/Sets if a given trace is turned on or off. + + Example usage: + + >>> import instruments as ik + >>> address = "TCPIP0::192.168.0.10::INSTR" + >>> inst = inst = ik.teledyne.MAUI.open_visa(address) + >>> channel = inst.channel[0] + >>> channel.trace = False + """, + ) + + class Channel(DataSource, Oscilloscope.Channel): + + """ + Class representing a channel on a MAUI oscilloscope. + + .. warning:: This class should NOT be manually created by the + user. It is designed to be initialized by the `MAUI` class. + """ + + def __init__(self, parent, idx): + self._parent = parent + self._idx = idx + 1 # 1-based + + # Initialize as a data source with name C{}. + super(MAUI.Channel, self).__init__(self._parent, f"C{self._idx}") + + # ENUMS # + + class Coupling(Enum): + """ + Enum containing valid coupling modes for the oscilloscope + channel. 1 MOhm and 50 Ohm are included. + """ + + ac1M = "A1M" + dc1M = "D1M" + dc50 = "D50" + ground = "GND" + + coupling = enum_property( + "CPL", + Coupling, + doc=""" + Gets/sets the coupling for the specified channel. + + Example usage: + + >>> import instruments as ik + >>> address = "TCPIP0::192.168.0.10::INSTR" + >>> inst = inst = ik.teledyne.MAUI.open_visa(address) + >>> channel = inst.channel[0] + >>> channel.coupling = channel.Coupling.dc50 + """, + ) + + # PROPERTIES # + + @property + def offset(self): + """ + Sets/gets the vertical offset of the specified input + channel. + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> inst = ik.teledyne.MAUI.open_visa("TCPIP0::192.168.0.10::INSTR") + >>> channel = inst.channel[0] # set up channel + >>> channel.offset = u.Quantity(-1, u.V) + """ + return u.Quantity(float(self.query("OFST?")), u.V) + + @offset.setter + def offset(self, newval): + newval = assume_units(newval, "V").to(u.V).magnitude + self.sendcmd(f"OFST {newval}") + + @property + def scale(self): + """ + Sets/Gets the vertical scale of the channel. + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> inst = ik.teledyne.MAUI.open_visa("TCPIP0::192.168.0.10::INSTR") + >>> channel = inst.channel[0] # set up channel + >>> channel.scale = u.Quantity(20, u.mV) + """ + return u.Quantity(float(self.query("VDIV?")), u.V) + + @scale.setter + def scale(self, newval): + newval = assume_units(newval, "V").to(u.V).magnitude + self.sendcmd(f"VDIV {newval}") + + # METHODS # + + def sendcmd(self, cmd): + """ + Wraps commands sent from property factories in this class + with identifiers for the specified channel. + + :param str cmd: Command to send to the instrument + """ + self._parent.sendcmd(f"C{self._idx}:{cmd}") + + def query(self, cmd, size=-1): + """ + Executes the given query. Wraps commands sent from property + factories in this class with identifiers for the specified + channel. + + :param str cmd: String containing the query to + execute. + :param int size: Number of bytes to be read. Default is read + until termination character is found. + :return: The result of the query as returned by the + connected instrument. + :rtype: `str` + """ + return self._parent.query(f"C{self._idx}:{cmd}", size=size) + + class Math(DataSource): + + """ + Class representing a function on a MAUI oscilloscope. + + .. warning:: This class should NOT be manually created by the + user. It is designed to be initialized by the `MAUI` class. + """ + + def __init__(self, parent, idx): + self._parent = parent + self._idx = idx + 1 # 1-based + + # Initialize as a data source with name C{}. + super(MAUI.Math, self).__init__(self._parent, f"F{self._idx}") + + # CLASSES # + + class Operators: + """ + Sets the operator for a given channel. + Most operators need a source `src`. If the source is given + as an integer, it is assume that the a signal channel is + requested. If you want to select another math channel for + example, you will need to specify the source as a tuple: + Example: `src=('f', 0)` would represent the first function + channel (called F1 in the MAUI manual). A channel could be + selected by calling `src=('c', 1)`, which would request the + second channel (oscilloscope channel 2). Please consult the + oscilloscope manual / the math setup itself for further + possibilities. + + .. note:: Your oscilloscope might not have all functions + that are described here. Also: Not all possibilities are + currently implemented. However, extension of this + functionality should be simple when following the given + structure + """ + + def __init__(self, parent): + self._parent = parent + + # PROPERTIES # + + @property + def current_setting(self): + """ + Gets the current setting and returns it as the full + command, as sent to the scope when setting an operator. + """ + return self._parent.query("DEF?") + + # METHODS - OPERATORS # + + def absolute(self, src): + """ + Absolute of wave form. + + :param int,tuple src: Source, see info above + """ + src_str = _source(src) + send_str = f"'ABS({src_str})'" + self._send_operator(send_str) + + def average(self, src, average_type="summed", sweeps=1000): + """ + Average of wave form. + + :param int,tuple src: Source, see info above + :param str average_type: `summed` or `continuous` + :param int sweeps: In summed mode, how many sweeps to + collect. In `continuous` mode the weight of each + sweep is equal to 1/`1`sweeps` + """ + src_str = _source(src) + + avgtp_str = "SUMMED" + if average_type == "continuous": + avgtp_str = "CONTINUOUS" + + send_str = "'AVG({})',AVERAGETYPE,{},SWEEPS,{}".format( + src_str, avgtp_str, sweeps + ) + + self._send_operator(send_str) + + def derivative(self, src, vscale=1e6, voffset=0, autoscale=True): + """ + Derivative of waveform using subtraction of adjacent + samples. If vscale and voffset are unitless, V/s are + assumed. + + :param int,tuple src: Source, see info above + :param float vscale: vertical units to display (V/s) + :param float voffset: vertical offset (V/s) + :param bool autoscale: auto scaling of vscale, voffset? + """ + src_str = _source(src) + + vscale = assume_units(vscale, u.V / u.s).to(u.V / u.s).magnitude + + voffset = assume_units(voffset, u.V / u.s).to(u.V / u.s).magnitude + + autoscale_str = "OFF" + if autoscale: + autoscale_str = "ON" + + send_str = ( + "'DERI({})',VERSCALE,{},VEROFFSET,{}," + "ENABLEAUTOSCALE,{}".format(src_str, vscale, voffset, autoscale_str) + ) + + self._send_operator(send_str) + + def difference(self, src1, src2, vscale_variable=False): + """ + Difference between two sources, `src1`-`src2`. + + :param int,tuple src1: Source 1, see info above + :param int,tuple src2: Source 2, see info above + :param bool vscale_variable: Horizontal and vertical + scale for addition and subtraction must be + identical. Allow for variable vertical scale in + result? + """ + src1_str = _source(src1) + src2_str = _source(src2) + + opt_str = "FALSE" + if vscale_variable: + opt_str = "TRUE" + + send_str = "'{}-{}',VERSCALEVARIABLE,{}".format( + src1_str, src2_str, opt_str + ) + + self._send_operator(send_str) + + def envelope(self, src, sweeps=1000, limit_sweeps=True): + """ + Highest and lowest Y values at each X in N sweeps. + + :param int,tuple src: Source, see info above + :param int sweeps: Number of sweeps + :param bool limit_sweeps: Limit the number of sweeps? + """ + src_str = _source(src) + send_str = "'EXTR({})',SWEEPS,{},LIMITNUMSWEEPS,{}".format( + src_str, sweeps, limit_sweeps + ) + self._send_operator(send_str) + + def eres(self, src, bits=0.5): + """ + Smoothing function defined by extra bits of resolution. + + :param int,tuple src: Source, see info above + :param float bits: Number of bits. Possible values are + (0.5, 1.0, 1.5, 2.0, 2.5, 3.0). If not in list, + default to 0.5. + """ + src_str = _source(src) + + bits_possible = (0.5, 1.0, 1.5, 2.0, 2.5, 3.0) + if bits not in bits_possible: + bits = 0.5 + + send_str = f"'ERES({src_str})',BITS,{bits}" + + self._send_operator(send_str) + + def fft( + self, src, type="powerspectrum", window="vonhann", suppress_dc=True + ): + """ + Fast Fourier Transform of signal. + + :param int,tuple src: Source, see info above + :param str type: Type of power spectrum. Possible + options are: ['real', 'imaginary', 'magnitude', + 'phase', 'powerspectrum', 'powerdensity']. + Default: 'powerspectrum' + :param str window: Window. Possible options are: + ['blackmanharris', 'flattop', 'hamming', + 'rectangular', 'vonhann']. Default: 'vonhann' + :param bool suppress_dc: Supress DC? + """ + src_str = _source(src) + + type_possible = [ + "real", + "imaginary", + "magnitude", + "phase", + "powerspectrum", + "powerdensity", + ] + if type not in type_possible: + type = "powerspectrum" + + window_possible = [ + "blackmanharris", + "flattop", + "hamming", + "rectangular", + "vonhann", + ] + if window not in window_possible: + window = "vonhann" + + if suppress_dc: + opt = "ON" + else: + opt = "OFF" + + send_str = "'FFT({})',TYPE,{},WINDOW,{},SUPPRESSDC,{}".format( + src_str, type, window, opt + ) + + self._send_operator(send_str) + + def floor(self, src, sweeps=1000, limit_sweeps=True): + """ + Lowest vertical value at each X value in N sweeps. + + :param int,tuple src: Source, see info above + :param int sweeps: Number of sweeps + :param bool limit_sweeps: Limit the number of sweeps? + """ + src_str = _source(src) + send_str = "'FLOOR({})',SWEEPS,{},LIMITNUMSWEEPS,{}".format( + src_str, sweeps, limit_sweeps + ) + self._send_operator(send_str) + + def integral(self, src, multiplier=1, adder=0, vscale=1e-3, voffset=0): + """ + Integral of waveform. + + :param int,tuple src: Source, see info above + :param float multiplier: 0 to 1e15 + :param float adder: 0 to 1e15 + :param float vscale: vertical units to display (Wb) + :param float voffset: vertical offset (Wb) + """ + src_str = _source(src) + + vscale = assume_units(vscale, u.Wb).to(u.Wb).magnitude + + voffset = assume_units(voffset, u.Wb).to(u.Wb).magnitude + + send_str = ( + "'INTG({}),MULTIPLIER,{},ADDER,{},VERSCALE,{}," + "VEROFFSET,{}".format(src_str, multiplier, adder, vscale, voffset) + ) + + self._send_operator(send_str) + + def invert(self, src): + """ + Inversion of waveform (-waveform). + + :param int,tuple src: Source, see info above + """ + src_str = _source(src) + self._send_operator(f"'-{src_str}'") + + def product(self, src1, src2): + """ + Product of two sources, `src1`*`src2`. + + :param int,tuple src1: Source 1, see info above + :param int,tuple src2: Source 2, see info above + """ + src1_str = _source(src1) + src2_str = _source(src2) + + send_str = f"'{src1_str}*{src2_str}'" + + self._send_operator(send_str) + + def ratio(self, src1, src2): + """ + Ratio of two sources, `src1`/`src2`. + + :param int,tuple src1: Source 1, see info above + :param int,tuple src2: Source 2, see info above + """ + src1_str = _source(src1) + src2_str = _source(src2) + + send_str = f"'{src1_str}/{src2_str}'" + + self._send_operator(send_str) + + def reciprocal(self, src): + """ + Reciprocal of waveform (1/waveform). + + :param int,tuple src: Source, see info above + """ + src_str = _source(src) + self._send_operator(f"'1/{src_str}'") + + def rescale(self, src, multiplier=1, adder=0): + """ + Rescales the waveform (w) in the style. + multiplier * w + adder + + :param int,tuple src: Source, see info above + :param float multiplier: multiplier + :param float adder: addition in V or assuming V + """ + src_str = _source(src) + + adder = assume_units(adder, u.V).to(u.V).magnitude + + send_str = "'RESC({})',MULTIPLIER,{},ADDER,{}".format( + src_str, multiplier, adder + ) + + self._send_operator(send_str) + + def sinx(self, src): + """ + Sin(x)/x interpolation to produce 10x output samples. + + :param int,tuple src: Source, see info above + """ + src_str = _source(src) + self._send_operator(f"'SINX({src_str})'") + + def square(self, src): + """ + Square of the input waveform. + + :param int,tuple src: Source, see info above + """ + src_str = _source(src) + self._send_operator(f"'SQR({src_str})'") + + def square_root(self, src): + """ + Square root of the input waveform. + + :param int,tuple src: Source, see info above + """ + src_str = _source(src) + self._send_operator(f"'SQRT({src_str})'") + + def sum(self, src1, src2): + """ + Product of two sources, `src1`+`src2`. + + :param int,tuple src1: Source 1, see info above + :param int,tuple src2: Source 2, see info above + """ + src1_str = _source(src1) + src2_str = _source(src2) + + send_str = f"'{src1_str}+{src2_str}'" + + self._send_operator(send_str) + + def trend(self, src, vscale=1, center=0, autoscale=True): + """ + Trend of the values of a paramter + + :param float vscale: vertical units to display (V) + :param float center: center (V) + """ + src_str = _source(src) + + vscale = assume_units(vscale, u.V).to(u.V).magnitude + + center = assume_units(center, u.V).to(u.V).magnitude + + if autoscale: + auto_str = "ON" + else: + auto_str = "OFF" + + send_str = ( + "'TREND({})',VERSCALE,{},CENTER,{}," + "AUTOFINDSCALE,{}".format(src_str, vscale, center, auto_str) + ) + + self._send_operator(send_str) + + def roof(self, src, sweeps=1000, limit_sweeps=True): + """ + Highest vertical value at each X value in N sweeps. + + :param int,tuple src: Source, see info above + :param int sweeps: Number of sweeps + :param bool limit_sweeps: Limit the number of sweeps? + """ + src_str = _source(src) + send_str = "'ROOF({})',SWEEPS,{},LIMITNUMSWEEPS,{}".format( + src_str, sweeps, limit_sweeps + ) + self._send_operator(send_str) + + def _send_operator(self, cmd): + """ + Set the operator in the scope. + """ + self._parent.sendcmd("{},{}".format("DEFINE EQN", cmd)) + + # PROPERTIES # + + @property + def operator(self): + """Get an operator object to set use to do math. + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> inst = ik.teledyne.MAUI.open_visa("TCPIP0::192.168.0.10::INSTR") + >>> channel = inst.channel[0] # set up channel + >>> # set up the first math function + >>> function = inst.math[0] + >>> function.trace = True # turn the trace on + >>> # set function to average the first oscilloscope channel + >>> function.operator.average(0) + """ + return self.Operators(self) + + # METHODS # + + def clear_sweeps(self): + """Clear the sweeps in a measurement.""" + self._parent.clear_sweeps() # re-implemented because handy + + def sendcmd(self, cmd): + """ + Wraps commands sent from property factories in this class + with identifiers for the specified channel. + + :param str cmd: Command to send to the instrument + """ + self._parent.sendcmd(f"F{self._idx}:{cmd}") + + def query(self, cmd, size=-1): + """ + Executes the given query. Wraps commands sent from property + factories in this class with identifiers for the specified + channel. + + :param str cmd: String containing the query to + execute. + :param int size: Number of bytes to be read. Default is read + until termination character is found. + :return: The result of the query as returned by the + connected instrument. + :rtype: `str` + """ + return self._parent.query(f"F{self._idx}:{cmd}", size=size) + + class Measurement: + + """ + Class representing a measurement on a MAUI oscilloscope. + + .. warning:: This class should NOT be manually created by the + user. It is designed to be initialized by the `MAUI` class. + """ + + def __init__(self, parent, idx): + self._parent = parent + self._idx = idx + 1 # 1-based + + # CLASSES # + + class State(Enum): + """ + Enum class for Measurement Parameters. Required to turn it + on or off. + """ + + statistics = "CUST,STAT" + histogram_icon = "CUST,HISTICON" + both = "CUST,BOTH" + off = "CUST,OFF" + + # PROPERTIES # + + measurement_state = enum_property( + command="PARM", + enum=State, + doc=""" + Sets / Gets the measurement state. Valid values are + 'statistics', 'histogram_icon', 'both', 'off'. + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> inst = ik.teledyne.MAUI.open_visa("TCPIP0::192.168.0.10::INSTR") + >>> msr1 = inst.measurement[0] # set up first measurement + >>> msr1.measurement_state = msr1.State.both # set to `both` + """, + ) + + @property + def statistics(self): + """ + Gets the statistics for the selected parameter. The scope + must be in `My_Measure` mode. + + :return tuple: (average, low, high, sigma, sweeps) + :return type: (float, float, float, float, float) + """ + ret_str = self.query(f"PAST? CUST, P{self._idx}").rstrip().split(",") + # parse the return string -> put into dictionary: + ret_dict = { + ret_str[it]: ret_str[it + 1] for it in range(0, len(ret_str), 2) + } + try: + stats = ( + float(ret_dict["AVG"]), + float(ret_dict["LOW"]), + float(ret_dict["HIGH"]), + float(ret_dict["SIGMA"]), + float(ret_dict["SWEEPS"]), + ) + except ValueError: # some statistics did not return + raise ValueError( + "Some statistics did not return useful " + "values. The return string is {}. Please " + "ensure that statistics is properly turned " + "on.".format(ret_str) + ) + return stats + + # METHODS # + + def delete(self): + """ + Deletes the given measurement parameter. + """ + self.sendcmd(f"PADL {self._idx}") + + def set_parameter(self, param, src): + """ + Sets a given parameter that should be measured on this + given channel. + + :param `inst.MeasurementParameters` param: The parameter + to set from the given enum list. + :param int,tuple src: Source, either as an integer if a + channel is requested (e.g., src=0 for Channel 1) or as + a tuple in the form, e.g., ('F', 1). Here 'F' refers + to a mathematical function and 1 would take the second + mathematical function `F2`. + + :raises AttributeError: The chosen parameter is invalid. + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> inst = ik.teledyne.MAUI.open_visa("TCPIP0::192.168.0.10::INSTR") + >>> msr1 = inst.measurement[0] # set up first measurement + >>> # setup to measure the 10 - 90% rise time on first channel + >>> msr1.set_parameter(inst.MeasurementParameters.rise_time_10_90, 0) + """ + if not isinstance(param, self._parent.MeasurementParameters): + raise AttributeError( + "Parameter must be selected from {}.".format( + self._parent.MeasurementParameters + ) + ) + + send_str = f"PACU {self._idx},{param.value},{_source(src)}" + + self.sendcmd(send_str) + + def sendcmd(self, cmd): + """ + Wraps commands sent from property factories in this class + with identifiers for the specified channel. + + :param str cmd: Command to send to the instrument + """ + self._parent.sendcmd(cmd) + + def query(self, cmd, size=-1): + """ + Executes the given query. Wraps commands sent from property + factories in this class with identifiers for the specified + channel. + + :param str cmd: String containing the query to + execute. + :param int size: Number of bytes to be read. Default is read + until termination character is found. + :return: The result of the query as returned by the + connected instrument. + :rtype: `str` + """ + return self._parent.query(cmd, size=size) + + # PROPERTIES # + + @property + def channel(self): + """ + Gets an iterator or list for easy Pythonic access to the various + channel objects on the oscilloscope instrument. + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> inst = ik.teledyne.MAUI.open_visa("TCPIP0::192.168.0.10::INSTR") + >>> channel = inst.channel[0] # get first channel + """ + return ProxyList(self, self.Channel, range(self.number_channels)) + + @property + def math(self): + """ + Gets an iterator or list for easy Pythonic access to the various + math data sources objects on the oscilloscope instrument. + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> inst = ik.teledyne.MAUI.open_visa("TCPIP0::192.168.0.10::INSTR") + >>> math = inst.math[0] # get first math function + """ + return ProxyList(self, self.Math, range(self.number_functions)) + + @property + def measurement(self): + """ + Gets an iterator or list for easy Pythonic access to the various + measurement data sources objects on the oscilloscope instrument. + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> inst = ik.teledyne.MAUI.open_visa("TCPIP0::192.168.0.10::INSTR") + >>> msr = inst.measurement[0] # get first measurement parameter + """ + return ProxyList(self, self.Measurement, range(self.number_measurements)) + + @property + def ref(self): + raise NotImplementedError + + # PROPERTIES + + @property + def number_channels(self): + """ + Sets/Gets the number of channels available on the specific + oscilloscope. Defaults to 4. + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> inst = ik.teledyne.MAUI.open_visa("TCPIP0::192.168.0.10::INSTR") + >>> inst.number_channel = 2 # for a oscilloscope with 2 channels + >>> inst.number_channel + 2 + """ + return self._number_channels + + @number_channels.setter + def number_channels(self, newval): + self._number_channels = newval + # create new trigger source enum + self._create_trigger_source_enum() + + @property + def number_functions(self): + """ + Sets/Gets the number of functions available on the specific + oscilloscope. Defaults to 2. + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> inst = ik.teledyne.MAUI.open_visa("TCPIP0::192.168.0.10::INSTR") + >>> inst.number_functions = 4 # for a oscilloscope with 4 math functions + >>> inst.number_functions + 4 + """ + return self._number_functions + + @number_functions.setter + def number_functions(self, newval): + self._number_functions = newval + + @property + def number_measurements(self): + """ + Sets/Gets the number of measurements available on the specific + oscilloscope. Defaults to 6. + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> inst = ik.teledyne.MAUI.open_visa("TCPIP0::192.168.0.10::INSTR") + >>> inst.number_measurements = 4 # for a oscilloscope with 4 measurements + >>> inst.number_measurements + 4 + """ + return self._number_measurements + + @number_measurements.setter + def number_measurements(self, newval): + self._number_measurements = newval + + @property + def self_test(self): + """ + Runs an oscilloscope's internal self test and returns the + result. The self-test includes testing the hardware of all + channels, the timebase and the trigger circuits. + Hardware failures are identified by a unique binary code in the + returned number. A status of 0 indicates that no + failures occurred. + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> inst = ik.teledyne.MAUI.open_visa("TCPIP0::192.168.0.10::INSTR") + >>> inst.self_test() + """ + # increase timeout x 10 to allow for enough time to test + self.timeout *= 10 + retval = self.query("*TST?") + self.timeout /= 10 + return retval + + @property + def show_id(self): + """ + Gets the scope information and returns it. The response + comprises manufacturer, oscilloscope model, serial number, + and firmware revision level. + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> inst = ik.teledyne.MAUI.open_visa("TCPIP0::192.168.0.10::INSTR") + >>> inst.show_id() + """ + return self.query("*IDN?") + + @property + def show_options(self): + """ + Gets and returns oscilloscope options: installed software or + hardware that is additional to the standard instrument + configuration. The response consists of a series of response + fields listing all the installed options. + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> inst = ik.teledyne.MAUI.open_visa("TCPIP0::192.168.0.10::INSTR") + >>> inst.show_options() + """ + return self.query("*OPT?") + + @property + def time_div(self): + """ + Sets/Gets the time per division, modifies the timebase setting. + Unitful. + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> inst = ik.teledyne.MAUI.open_visa("TCPIP0::192.168.0.10::INSTR") + >>> inst.time_div = u.Quantity(200, u.ns) + """ + return u.Quantity(float(self.query("TDIV?")), u.s) + + @time_div.setter + def time_div(self, newval): + newval = assume_units(newval, "s").to(u.s).magnitude + self.sendcmd(f"TDIV {newval}") + + # TRIGGER PROPERTIES + + trigger_state = enum_property( + command="TRMD", + enum=TriggerState, + doc=""" + Sets / Gets the trigger state. Valid values are are defined + in `TriggerState` enum class. + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> inst = ik.teledyne.MAUI.open_visa("TCPIP0::192.168.0.10::INSTR") + >>> inst.trigger_state = inst.TriggerState.normal + """, + ) + + @property + def trigger_delay(self): + """ + Sets/Gets the trigger offset with respect to time zero (i.e., + a horizontal shift). Unitful. + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> inst = ik.teledyne.MAUI.open_visa("TCPIP0::192.168.0.10::INSTR") + >>> inst.trigger_delay = u.Quantity(60, u.ns) + + """ + return u.Quantity(float(self.query("TRDL?")), u.s) + + @trigger_delay.setter + def trigger_delay(self, newval): + newval = assume_units(newval, "s").to(u.s).magnitude + self.sendcmd(f"TRDL {newval}") + + @property + def trigger_source(self): + """Sets / Gets the trigger source. + + .. note:: The `TriggerSource` class is dynamically generated + when the number of channels is switched. The above shown class + is only the default! Channels are added and removed, as + required. + + .. warning:: If a trigger type is currently set on the + oscilloscope that is not implemented in this class, + setting the source will fail. The oscilloscope is set up + such that the the trigger type and source are set at the + same time. However, for convenience, these two properties + are split apart here. + + :return: Trigger source. + :rtype: Member of `TriggerSource` class. + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> inst = ik.teledyne.MAUI.open_visa("TCPIP0::192.168.0.10::INSTR") + >>> inst.trigger_source = inst.TriggerSource.ext # external trigger + """ + retval = self.query("TRIG_SELECT?").split(",")[2] + return self.TriggerSource(retval) + + @trigger_source.setter + def trigger_source(self, newval): + curr_trig_typ = self.trigger_type + cmd = f"TRIG_SELECT {curr_trig_typ.value},SR,{newval.value}" + self.sendcmd(cmd) + + @property + def trigger_type(self): + """Sets / Gets the trigger type. + + .. warning:: If a trigger source is currently set on the + oscilloscope that is not implemented in this class, + setting the source will fail. The oscilloscope is set up + such that the the trigger type and source are set at the + same time. However, for convenience, these two properties + are split apart here. + + :return: Trigger type. + :rtype: Member of `TriggerType` enum class. + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> inst = ik.teledyne.MAUI.open_visa("TCPIP0::192.168.0.10::INSTR") + >>> inst.trigger_type = inst.TriggerType.edge # trigger on edge + """ + retval = self.query("TRIG_SELECT?").split(",")[0] + return self.TriggerType(retval) + + @trigger_type.setter + def trigger_type(self, newval): + curr_trig_src = self.trigger_source + cmd = f"TRIG_SELECT {newval.value},SR,{curr_trig_src.value}" + self.sendcmd(cmd) + + # METHODS # + + def clear_sweeps(self): + """Clears the sweeps in a measurement. + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> inst = ik.teledyne.MAUI.open_visa("TCPIP0::192.168.0.10::INSTR") + >>> inst.clear_sweeps() + """ + self.sendcmd("CLEAR_SWEEPS") + + def force_trigger(self): + """Forces a trigger event to occur on the attached oscilloscope. + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> inst = ik.teledyne.MAUI.open_visa("TCPIP0::192.168.0.10::INSTR") + >>> inst.force_trigger() + """ + self.sendcmd("ARM") + + def run(self): + """Enables the trigger for the oscilloscope and sets it to auto. + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> inst = ik.teledyne.MAUI.open_visa("TCPIP0::192.168.0.10::INSTR") + >>> inst.run() + """ + self.trigger_state = self.TriggerState.auto + + def stop(self): + """Disables the trigger for the oscilloscope. + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> inst = ik.teledyne.MAUI.open_visa("TCPIP0::192.168.0.10::INSTR") + >>> inst.stop() + """ + self.sendcmd("STOP") + + +# STATICS # + + +def _source(src): + """Stich the source together properly and return it.""" + if isinstance(src, int): + return f"C{src + 1}" + elif isinstance(src, tuple) and len(src) == 2: + return f"{src[0].upper()}{int(src[1]) + 1}" + else: + raise ValueError( + "An invalid source was specified. " + "Source must be an integer or a tuple of " + "length 2." + ) diff --git a/instruments/tests/__init__.py b/instruments/tests/__init__.py index 1237d2591..d33a3eb7d 100644 --- a/instruments/tests/__init__.py +++ b/instruments/tests/__init__.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing InstrumentKit unit tests @@ -9,21 +8,21 @@ # IMPORTS #################################################################### -from __future__ import absolute_import -from __future__ import unicode_literals import contextlib from io import BytesIO +from unittest import mock -from builtins import bytes, str +import pytest -from nose.tools import nottest, eq_ +from instruments.optional_dep_finder import numpy +from instruments.units import ureg as u # FUNCTIONS ################################################################## @contextlib.contextmanager -def expected_protocol(ins_class, host_to_ins, ins_to_host, sep="\n"): +def expected_protocol(ins_class, host_to_ins, ins_to_host, sep="\n", repeat=1): """ Given an instrument class, expected output from the host and expected input from the instrument, asserts that the protocol in a context block proceeds @@ -32,7 +31,8 @@ def expected_protocol(ins_class, host_to_ins, ins_to_host, sep="\n"): For an example of how to write tests using this context manager, see the ``make_name_test`` function below. - :param type ins_class: Instrument class to use for the protocol assertion. + :param ins_class: Instrument class to use for the protocol assertion. + :type ins_class: `~instruments.Instrument` :param host_to_ins: Data to be sent by the host to the instrument; this is checked against the actual data sent by the instrument class during the execution of this context manager. @@ -43,9 +43,17 @@ def expected_protocol(ins_class, host_to_ins, ins_to_host, sep="\n"): be used to assert correct behaviour within the context. :type ins_to_host: ``str`` or ``list``; if ``list``, each line is concatenated with the separator given by ``sep``. + :param str sep: Character to be inserted after each string in both + host_to_ins and ins_to_host parameters. This is typically the + termination character you would like to have inserted. + :param int repeat: The number of times the host_to_ins and + ins_to_host data sets should be duplicated. Typically the default + value of 1 is sufficient, but increasing this is useful when + testing multiple calls in the same test that should have the same + command transactions. """ if isinstance(sep, bytes): - sep = sep.encode("utf-8") + sep = sep.decode("utf-8") # Normalize assertion and playback strings. if isinstance(ins_to_host, list): @@ -53,34 +61,41 @@ def expected_protocol(ins_class, host_to_ins, ins_to_host, sep="\n"): item.encode("utf-8") if isinstance(item, str) else item for item in ins_to_host ] - ins_to_host = sep.encode("utf-8").join(ins_to_host) + \ - (sep.encode("utf-8") if ins_to_host else b"") + ins_to_host = sep.encode("utf-8").join(ins_to_host) + ( + sep.encode("utf-8") if ins_to_host else b"" + ) elif isinstance(ins_to_host, str): ins_to_host = ins_to_host.encode("utf-8") + ins_to_host *= repeat if isinstance(host_to_ins, list): host_to_ins = [ item.encode("utf-8") if isinstance(item, str) else item for item in host_to_ins ] - host_to_ins = sep.encode("utf-8").join(host_to_ins) + \ - (sep.encode("utf-8") if host_to_ins else b"") + host_to_ins = sep.encode("utf-8").join(host_to_ins) + ( + sep.encode("utf-8") if host_to_ins else b"" + ) elif isinstance(host_to_ins, str): host_to_ins = host_to_ins.encode("utf-8") + host_to_ins *= repeat stdin = BytesIO(ins_to_host) stdout = BytesIO() yield ins_class.open_test(stdin, stdout) - assert stdout.getvalue() == host_to_ins, \ - """Expected: + assert ( + stdout.getvalue() == host_to_ins + ), """Expected: {} Got: -{}""".format(repr(host_to_ins), repr(stdout.getvalue())) +{}""".format( + repr(host_to_ins), repr(stdout.getvalue()) + ) # current = stdin.tell() # stdin.seek(0, 2) @@ -90,26 +105,42 @@ def expected_protocol(ins_class, host_to_ins, ins_to_host, sep="\n"): # """Only read {} bytes out of {}""".format(current, end) -@nottest -def unit_eq(a, b, msg=None, thresh=1e-5): +def unit_eq(a, b): """ Asserts that two unitful quantites ``a`` and ``b`` are equal up to a small numerical threshold. """ - assert abs((a - b).magnitude) <= thresh, "{} - {} = {}.{}".format( - a, b, a - b, - "\n" + msg if msg is not None else "" - ) - assert a.units == b.units, "{} and {} have different units".format(a, b) + assert a.magnitude == pytest.approx(b.magnitude) + assert a.units == b.units, f"{a} and {b} have different units" -@nottest def make_name_test(ins_class, name_cmd="*IDN?"): """ Given an instrument class, produces a test which asserts that the instrument correctly reports its name in response to a standard command. """ + def test(): with expected_protocol(ins_class, name_cmd + "\n", "NAME\n") as ins: - eq_(ins.name, "NAME") + assert ins.name == "NAME" + return test + + +def iterable_eq(a, b): + """ + Asserts that the contents of two iterables are the same. + """ + if numpy and (isinstance(a, numpy.ndarray) or isinstance(b, numpy.ndarray)): + # pylint: disable=unidiomatic-typecheck + assert type(a) == type( + b + ), f"Expected two numpy arrays, got {type(a)}, {type(b)}" + assert len(a) == len( + b + ), f"Length of iterables is not the same, got {len(a)} and {len(b)}" + assert (a == b).all() + elif isinstance(a, u.Quantity) and isinstance(b, u.Quantity): + unit_eq(a, b) + else: + assert a == b diff --git a/instruments/tests/test_abstract_inst/test_electrometer.py b/instruments/tests/test_abstract_inst/test_electrometer.py new file mode 100644 index 000000000..5201450c9 --- /dev/null +++ b/instruments/tests/test_abstract_inst/test_electrometer.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python +""" +Module containing tests for the abstract electrometer class +""" + +# IMPORTS #################################################################### + + +import pytest + +import instruments as ik +from instruments.tests import expected_protocol + + +# TESTS ###################################################################### + + +@pytest.fixture +def em(monkeypatch): + """Patch and return electrometer class for direct access of metaclass.""" + inst = ik.abstract_instruments.Electrometer + monkeypatch.setattr(inst, "__abstractmethods__", set()) + return inst + + +def test_electrometer_mode(em): + """Get / set mode to ensure the abstract property exists.""" + with expected_protocol(em, [], []) as inst: + _ = inst.mode + inst.mode = 42 + + +def test_electrometer_unit(em): + """Get unit to ensure the abstract property exists.""" + with expected_protocol(em, [], []) as inst: + _ = inst.unit + + +def test_electrometer_trigger_mode(em): + """Get / set trigger mode to ensure the abstract property exists.""" + with expected_protocol(em, [], []) as inst: + _ = inst.trigger_mode + inst.trigger_mode = 42 + + +def test_electrometer_input_range(em): + """Get / set input range to ensure the abstract property exists.""" + with expected_protocol(em, [], []) as inst: + _ = inst.input_range + inst.input_range = 42 + + +def test_electrometer_zero_check(em): + """Get / set zero check to ensure the abstract property exists.""" + with expected_protocol(em, [], []) as inst: + _ = inst.zero_check + inst.zero_check = 42 + + +def test_electrometer_zero_correct(em): + """Get / set zero correct to ensure the abstract property exists.""" + with expected_protocol(em, [], []) as inst: + _ = inst.zero_correct + inst.zero_correct = 42 + + +def test_electrometer_fetch(em): + """Raise NotImplementedError for fetch method.""" + with expected_protocol(em, [], []) as inst: + with pytest.raises(NotImplementedError): + inst.fetch() + + +def test_electrometer_read_measurements(em): + """Raise NotImplementedError for read_measurements method.""" + with expected_protocol(em, [], []) as inst: + with pytest.raises(NotImplementedError): + inst.read_measurements() diff --git a/instruments/tests/test_abstract_inst/test_function_generator.py b/instruments/tests/test_abstract_inst/test_function_generator.py new file mode 100644 index 000000000..83ac8373a --- /dev/null +++ b/instruments/tests/test_abstract_inst/test_function_generator.py @@ -0,0 +1,249 @@ +#!/usr/bin/env python +""" +Module containing tests for the abstract function generator class +""" + +# IMPORTS #################################################################### + + +import pytest +from instruments.units import ureg as u + +import instruments as ik +from instruments.tests import expected_protocol, unit_eq + + +# TESTS ###################################################################### + +# pylint: disable=missing-function-docstring,redefined-outer-name,protected-access + + +@pytest.fixture +def fg(): + return ik.abstract_instruments.FunctionGenerator.open_test() + + +def test_func_gen_default_channel_count(fg): + assert fg._channel_count == 1 + + +def test_func_gen_raises_not_implemented_error_one_channel_getting(fg): + fg._channel_count = 1 + with pytest.raises(NotImplementedError): + _ = fg.amplitude + with pytest.raises(NotImplementedError): + _ = fg.frequency + with pytest.raises(NotImplementedError): + _ = fg.function + with pytest.raises(NotImplementedError): + _ = fg.offset + with pytest.raises(NotImplementedError): + _ = fg.phase + + +def test_func_gen_raises_not_implemented_error_one_channel_setting(fg): + fg._channel_count = 1 + with pytest.raises(NotImplementedError): + fg.amplitude = 1 + with pytest.raises(NotImplementedError): + fg.frequency = 1 + with pytest.raises(NotImplementedError): + fg.function = 1 + with pytest.raises(NotImplementedError): + fg.offset = 1 + with pytest.raises(NotImplementedError): + fg.phase = 1 + + +def test_func_gen_raises_not_implemented_error_two_channel_getting(fg): + fg._channel_count = 2 + with pytest.raises(NotImplementedError): + _ = fg.channel[0].amplitude + with pytest.raises(NotImplementedError): + _ = fg.channel[0].frequency + with pytest.raises(NotImplementedError): + _ = fg.channel[0].function + with pytest.raises(NotImplementedError): + _ = fg.channel[0].offset + with pytest.raises(NotImplementedError): + _ = fg.channel[0].phase + + +def test_func_gen_raises_not_implemented_error_two_channel_setting(fg): + fg._channel_count = 2 + with pytest.raises(NotImplementedError): + fg.channel[0].amplitude = 1 + with pytest.raises(NotImplementedError): + fg.channel[0].frequency = 1 + with pytest.raises(NotImplementedError): + fg.channel[0].function = 1 + with pytest.raises(NotImplementedError): + fg.channel[0].offset = 1 + with pytest.raises(NotImplementedError): + fg.channel[0].phase = 1 + + +def test_func_gen_two_channel_passes_thru_call_getter(fg, mocker): + mock_channel = mocker.MagicMock() + mock_properties = [mocker.PropertyMock(return_value=1) for _ in range(5)] + + mocker.patch( + "instruments.abstract_instruments.FunctionGenerator.Channel", new=mock_channel + ) + type(mock_channel()).amplitude = mock_properties[0] + type(mock_channel()).frequency = mock_properties[1] + type(mock_channel()).function = mock_properties[2] + type(mock_channel()).offset = mock_properties[3] + type(mock_channel()).phase = mock_properties[4] + + fg._channel_count = 2 + _ = fg.amplitude + _ = fg.frequency + _ = fg.function + _ = fg.offset + _ = fg.phase + + for mock_property in mock_properties: + mock_property.assert_called_once_with() + + +def test_func_gen_one_channel_passes_thru_call_getter(fg, mocker): + mock_properties = [mocker.PropertyMock(return_value=1) for _ in range(4)] + mock_method = mocker.MagicMock(return_value=(1, u.V)) + + mocker.patch( + "instruments.abstract_instruments.FunctionGenerator.frequency", + new=mock_properties[0], + ) + mocker.patch( + "instruments.abstract_instruments.FunctionGenerator.function", + new=mock_properties[1], + ) + mocker.patch( + "instruments.abstract_instruments.FunctionGenerator.offset", + new=mock_properties[2], + ) + mocker.patch( + "instruments.abstract_instruments.FunctionGenerator.phase", + new=mock_properties[3], + ) + mocker.patch( + "instruments.abstract_instruments.FunctionGenerator._get_amplitude_", + new=mock_method, + ) + + fg._channel_count = 1 + _ = fg.channel[0].amplitude + _ = fg.channel[0].frequency + _ = fg.channel[0].function + _ = fg.channel[0].offset + _ = fg.channel[0].phase + + for mock_property in mock_properties: + mock_property.assert_called_once_with() + + mock_method.assert_called_once_with() + + +def test_func_gen_two_channel_passes_thru_call_setter(fg, mocker): + mock_channel = mocker.MagicMock() + mock_properties = [mocker.PropertyMock() for _ in range(5)] + + mocker.patch( + "instruments.abstract_instruments.FunctionGenerator.Channel", new=mock_channel + ) + type(mock_channel()).amplitude = mock_properties[0] + type(mock_channel()).frequency = mock_properties[1] + type(mock_channel()).function = mock_properties[2] + type(mock_channel()).offset = mock_properties[3] + type(mock_channel()).phase = mock_properties[4] + + fg._channel_count = 2 + fg.amplitude = 1 + fg.frequency = 1 + fg.function = 1 + fg.offset = 1 + fg.phase = 1 + + for mock_property in mock_properties: + mock_property.assert_called_once_with(1) + + +def test_func_gen_one_channel_passes_thru_call_setter(fg, mocker): + mock_properties = [mocker.PropertyMock() for _ in range(4)] + mock_method = mocker.MagicMock() + + mocker.patch( + "instruments.abstract_instruments.FunctionGenerator.frequency", + new=mock_properties[0], + ) + mocker.patch( + "instruments.abstract_instruments.FunctionGenerator.function", + new=mock_properties[1], + ) + mocker.patch( + "instruments.abstract_instruments.FunctionGenerator.offset", + new=mock_properties[2], + ) + mocker.patch( + "instruments.abstract_instruments.FunctionGenerator.phase", + new=mock_properties[3], + ) + mocker.patch( + "instruments.abstract_instruments.FunctionGenerator._set_amplitude_", + new=mock_method, + ) + + fg._channel_count = 1 + fg.channel[0].amplitude = 1 + fg.channel[0].frequency = 1 + fg.channel[0].function = 1 + fg.channel[0].offset = 1 + fg.channel[0].phase = 1 + + for mock_property in mock_properties: + mock_property.assert_called_once_with(1) + + mock_method.assert_called_once_with(magnitude=1, units=fg.VoltageMode.peak_to_peak) + + +def test_func_gen_channel_set_amplitude_dbm(mocker): + """Get amplitude of channel when units are in dBm.""" + with expected_protocol(ik.abstract_instruments.FunctionGenerator, [], []) as inst: + value = 3.14 + # mock out the _get_amplitude of parent to return value in dBm + mocker.patch.object( + inst, + "_get_amplitude_", + return_value=( + value, + ik.abstract_instruments.FunctionGenerator.VoltageMode.dBm, + ), + ) + + channel = inst.channel[0] + unit_eq(channel.amplitude, u.Quantity(value, u.dBm)) + + +def test_func_gen_channel_sendcmd(mocker): + """Send a command via parent class function.""" + with expected_protocol(ik.abstract_instruments.FunctionGenerator, [], []) as inst: + cmd = "COMMAND" + # mock out parent's send command + mock_sendcmd = mocker.patch.object(inst, "sendcmd") + channel = inst.channel[0] + channel.sendcmd(cmd) + mock_sendcmd.assert_called_with(cmd) + + +def test_func_gen__channel_sendcmd(mocker): + """Send a query via parent class function.""" + with expected_protocol(ik.abstract_instruments.FunctionGenerator, [], []) as inst: + cmd = "QUERY" + size = 13 + retval = "ANSWER" + # mock out parent's query command + mock_query = mocker.patch.object(inst, "query", return_value=retval) + channel = inst.channel[0] + assert channel.query(cmd, size=size) == retval + mock_query.assert_called_with(cmd, size) diff --git a/instruments/tests/test_abstract_inst/test_multimeter.py b/instruments/tests/test_abstract_inst/test_multimeter.py new file mode 100644 index 000000000..3fe6299af --- /dev/null +++ b/instruments/tests/test_abstract_inst/test_multimeter.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python +""" +Module containing tests for the abstract multimeter class +""" + +# IMPORTS #################################################################### + + +import pytest + +import instruments as ik +from instruments.tests import expected_protocol + + +# TESTS ###################################################################### + + +@pytest.fixture +def mul(monkeypatch): + """Patch and return Multimeter class for access.""" + inst = ik.abstract_instruments.Multimeter + monkeypatch.setattr(inst, "__abstractmethods__", set()) + return inst + + +def test_multimeter_mode(mul): + """Get / set mode: ensure existence.""" + with expected_protocol(mul, [], []) as inst: + _ = inst.mode + inst.mode = 42 + + +def test_multimeter_trigger_mode(mul): + """Get / set trigger mode: ensure existence.""" + with expected_protocol(mul, [], []) as inst: + _ = inst.trigger_mode + inst.trigger_mode = 42 + + +def test_multimeter_relative(mul): + """Get / set relative: ensure existence.""" + with expected_protocol(mul, [], []) as inst: + _ = inst.relative + inst.relative = 42 + + +def test_multimeter_input_range(mul): + """Get / set input range: ensure existence.""" + with expected_protocol(mul, [], []) as inst: + _ = inst.input_range + inst.input_range = 42 + + +def test_multimeter_measure(mul): + """Measure: ensure existence.""" + with expected_protocol(mul, [], []) as inst: + inst.measure("mode") diff --git a/instruments/tests/test_abstract_inst/test_optical_spectrum_analyzer.py b/instruments/tests/test_abstract_inst/test_optical_spectrum_analyzer.py new file mode 100644 index 000000000..f953dad8a --- /dev/null +++ b/instruments/tests/test_abstract_inst/test_optical_spectrum_analyzer.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python +""" +Module containing tests for the abstract optical spectrum analyzer class +""" + +# IMPORTS #################################################################### + + +import pytest + +import instruments as ik +from instruments.tests import expected_protocol + + +# TESTS ###################################################################### + + +@pytest.fixture +def osa(monkeypatch): + """Patch and return Optical Spectrum Analyzer class for access.""" + inst = ik.abstract_instruments.OpticalSpectrumAnalyzer + monkeypatch.setattr(inst, "__abstractmethods__", set()) + return inst + + +@pytest.fixture +def osc(monkeypatch): + """Patch and return OSAChannel class for access.""" + inst = ik.abstract_instruments.OpticalSpectrumAnalyzer.Channel + monkeypatch.setattr(inst, "__abstractmethods__", set()) + return inst + + +# OPTICAL SPECTRUM ANALYZER CLASS # + + +def test_osa_channel(osa): + """Get channel: ensure existence.""" + with expected_protocol(osa, [], []) as inst: + with pytest.raises(NotImplementedError): + _ = inst.channel + + +def test_osa_start_wl(osa): + """Get / set start wavelength: ensure existence.""" + with expected_protocol(osa, [], []) as inst: + with pytest.raises(NotImplementedError): + _ = inst.start_wl + with pytest.raises(NotImplementedError): + inst.start_wl = 42 + + +def test_osa_stop_wl(osa): + """Get / set stop wavelength: ensure existence.""" + with expected_protocol(osa, [], []) as inst: + with pytest.raises(NotImplementedError): + _ = inst.stop_wl + with pytest.raises(NotImplementedError): + inst.stop_wl = 42 + + +def test_osa_bandwidth(osa): + """Get / set bandwidth: ensure existence.""" + with expected_protocol(osa, [], []) as inst: + with pytest.raises(NotImplementedError): + _ = inst.bandwidth + with pytest.raises(NotImplementedError): + inst.bandwidth = 42 + + +def test_osa_start_sweep(osa): + """Start sweep: ensure existence.""" + with expected_protocol(osa, [], []) as inst: + with pytest.raises(NotImplementedError): + inst.start_sweep() + + +# OSAChannel # + + +def test_osa_channel_wavelength(osc): + """Channel wavelength method: ensure existence.""" + inst = osc() + with pytest.raises(NotImplementedError): + inst.wavelength() + + +def test_osa_channel_data(osc): + """Channel data method: ensure existence.""" + inst = osc() + with pytest.raises(NotImplementedError): + inst.data() diff --git a/instruments/tests/test_abstract_inst/test_oscilloscope.py b/instruments/tests/test_abstract_inst/test_oscilloscope.py new file mode 100644 index 000000000..22c0a8c2d --- /dev/null +++ b/instruments/tests/test_abstract_inst/test_oscilloscope.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python +""" +Module containing tests for the abstract oscilloscope class +""" + +# IMPORTS #################################################################### + + +import pytest + +import instruments as ik +from instruments.tests import expected_protocol + + +# TESTS ###################################################################### + + +@pytest.fixture +def osc(monkeypatch): + """Patch and return Oscilloscope class for access.""" + inst = ik.abstract_instruments.Oscilloscope + monkeypatch.setattr(inst, "__abstractmethods__", set()) + return inst + + +@pytest.fixture +def osc_ch(monkeypatch): + """Patch and return OscilloscopeChannel class for access.""" + inst = ik.abstract_instruments.Oscilloscope.Channel + monkeypatch.setattr(inst, "__abstractmethods__", set()) + return inst + + +@pytest.fixture +def osc_ds(monkeypatch): + """Patch and return OscilloscopeDataSource class for access.""" + inst = ik.abstract_instruments.Oscilloscope.DataSource + monkeypatch.setattr(inst, "__abstractmethods__", set()) + return inst + + +# OSCILLOSCOPE # + + +def test_oscilloscope_channel(osc): + """Get channel: ensure existence.""" + with expected_protocol(osc, [], []) as inst: + with pytest.raises(NotImplementedError): + _ = inst.channel + + +def test_oscilloscope_ref(osc): + """Get ref: ensure existence.""" + with expected_protocol(osc, [], []) as inst: + with pytest.raises(NotImplementedError): + _ = inst.ref + + +def test_oscilloscope_math(osc): + """Get math: ensure existence.""" + with expected_protocol(osc, [], []) as inst: + with pytest.raises(NotImplementedError): + _ = inst.math + + +def test_oscilloscope_force_trigger(osc): + """Force a trigger: ensure existence.""" + with expected_protocol(osc, [], []) as inst: + with pytest.raises(NotImplementedError): + inst.force_trigger() + + +# OSCILLOSCOPE CHANNEL # + + +def test_oscilloscope_channel_coupling(osc_ch): + """Get / set channel coupling: ensure existence.""" + inst = osc_ch() + with pytest.raises(NotImplementedError): + _ = inst.coupling + with pytest.raises(NotImplementedError): + inst.coupling = 42 + + +# OSCILLOSCOPE DATA SOURCE # + + +def test_oscilloscope_data_source_init(osc_ds): + """Initialize Oscilloscope Data Source.""" + parent = "parent" + name = "name" + inst = osc_ds(parent, name) + assert inst._parent == parent + assert inst._name == name + assert inst._old_dsrc is None + + +def test_oscilloscope_data_source_name(osc_ds): + """Get data source name: ensure existence.""" + parent = "parent" + name = "name" + inst = osc_ds(parent, name) + with pytest.raises(NotImplementedError): + _ = inst.name + + +def test_oscilloscope_data_source_read_waveform(osc_ds): + """Read data source waveform: ensure existence.""" + parent = "parent" + name = "name" + inst = osc_ds(parent, name) + with pytest.raises(NotImplementedError): + inst.read_waveform() diff --git a/instruments/tests/test_abstract_inst/test_power_supply.py b/instruments/tests/test_abstract_inst/test_power_supply.py new file mode 100644 index 000000000..e2c9446db --- /dev/null +++ b/instruments/tests/test_abstract_inst/test_power_supply.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python +""" +Module containing tests for the abstract power supply class +""" + +# IMPORTS #################################################################### + + +import pytest + +import instruments as ik +from instruments.tests import expected_protocol + + +# TESTS ###################################################################### + + +@pytest.fixture +def ps(monkeypatch): + """Patch and return Power Supply class for access.""" + inst = ik.abstract_instruments.PowerSupply + monkeypatch.setattr(inst, "__abstractmethods__", set()) + return inst + + +@pytest.fixture +def ps_ch(monkeypatch): + """Patch and return Power Supply Channel class for access.""" + inst = ik.abstract_instruments.PowerSupply.Channel + monkeypatch.setattr(inst, "__abstractmethods__", set()) + return inst + + +# POWER SUPPLY # + + +def test_power_supply_channel(ps): + """Get channel: ensure existence.""" + with expected_protocol(ps, [], []) as inst: + with pytest.raises(NotImplementedError): + _ = inst.channel + + +def test_power_supply_voltage(ps): + """Get / set voltage: ensure existence.""" + with expected_protocol(ps, [], []) as inst: + _ = inst.voltage + inst.voltage = 42 + + +def test_power_supply_current(ps): + """Get / set current: ensure existence.""" + with expected_protocol(ps, [], []) as inst: + _ = inst.current + inst.current = 42 + + +# POWER SUPPLY CHANNEL # + + +def test_power_supply_channel_mode(ps_ch): + """Get / set channel mode: ensure existence.""" + inst = ps_ch() + _ = inst.mode + inst.mode = 42 + + +def test_power_supply_channel_voltage(ps_ch): + """Get / set channel voltage: ensure existence.""" + inst = ps_ch() + _ = inst.voltage + inst.voltage = 42 + + +def test_power_supply_channel_current(ps_ch): + """Get / set channel current: ensure existence.""" + inst = ps_ch() + _ = inst.current + inst.current = 42 + + +def test_power_supply_channel_output(ps_ch): + """Get / set channel output: ensure existence.""" + inst = ps_ch() + _ = inst.output + inst.output = 42 diff --git a/instruments/tests/test_abstract_inst/test_signal_generator/test_channel.py b/instruments/tests/test_abstract_inst/test_signal_generator/test_channel.py new file mode 100644 index 000000000..d348b03e7 --- /dev/null +++ b/instruments/tests/test_abstract_inst/test_signal_generator/test_channel.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python +""" +Module containing tests for the abstract signal generator channel class +""" + +# IMPORTS #################################################################### + + +import pytest + +import instruments as ik + + +# TESTS ###################################################################### + + +@pytest.fixture +def sgc(monkeypatch): + """Patch and return SGChannel for direct access of metaclass.""" + inst = ik.abstract_instruments.signal_generator.SGChannel + monkeypatch.setattr(inst, "__abstractmethods__", set()) + return inst + + +def test_sg_channel_frequency(sgc): + """Get / set frequency: Ensure existence.""" + inst = sgc() + _ = inst.frequency + inst.frequency = 42 + + +def test_sg_channel_power(sgc): + """Get / set power: Ensure existence.""" + inst = sgc() + _ = inst.power + inst.power = 42 + + +def test_sg_channel_phase(sgc): + """Get / set phase: Ensure existence.""" + inst = sgc() + _ = inst.phase + inst.phase = 42 + + +def test_sg_channel_output(sgc): + """Get / set output: Ensure existence.""" + inst = sgc() + _ = inst.output + inst.output = 4 diff --git a/instruments/tests/test_abstract_inst/test_signal_generator/test_signal_generator.py b/instruments/tests/test_abstract_inst/test_signal_generator/test_signal_generator.py new file mode 100644 index 000000000..fd25fca12 --- /dev/null +++ b/instruments/tests/test_abstract_inst/test_signal_generator/test_signal_generator.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python +""" +Module containing tests for the abstract signal generator class +""" + +# IMPORTS #################################################################### + + +import pytest + +import instruments as ik +from instruments.tests import expected_protocol + + +# TESTS ###################################################################### + + +@pytest.fixture +def sg(monkeypatch): + """Patch and return signal generator for direct access of metaclass.""" + inst = ik.abstract_instruments.signal_generator.SignalGenerator + monkeypatch.setattr(inst, "__abstractmethods__", set()) + return inst + + +def test_signal_generator_channel(sg): + """Get channel: Ensure existence.""" + with expected_protocol(sg, [], []) as inst: + with pytest.raises(NotImplementedError): + _ = inst.channel diff --git a/instruments/tests/test_abstract_inst/test_signal_generator/test_single_channel_sg.py b/instruments/tests/test_abstract_inst/test_signal_generator/test_single_channel_sg.py new file mode 100644 index 000000000..6f180ffa4 --- /dev/null +++ b/instruments/tests/test_abstract_inst/test_signal_generator/test_single_channel_sg.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +""" +Module containing tests for the abstract signal generator class +""" + +# IMPORTS #################################################################### + + +import pytest + +import instruments as ik +from instruments.tests import expected_protocol + + +# TESTS ###################################################################### + + +@pytest.fixture +def scsg(monkeypatch): + """Patch and return signal generator for direct access of metaclass.""" + inst = ik.abstract_instruments.signal_generator.SingleChannelSG + monkeypatch.setattr(inst, "__abstractmethods__", set()) + return inst + + +def test_signal_generator_channel(scsg): + """Get channel: Ensure existence.""" + with expected_protocol(scsg, [], []) as inst: + assert inst.channel[0] == inst diff --git a/instruments/tests/test_agilent/test_agilent_33220a.py b/instruments/tests/test_agilent/test_agilent_33220a.py new file mode 100644 index 000000000..d042d4585 --- /dev/null +++ b/instruments/tests/test_agilent/test_agilent_33220a.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python +""" +Module containing tests for generic SCPI function generator instruments +""" + +# IMPORTS #################################################################### + +from hypothesis import given, strategies as st +import pytest + +from instruments.units import ureg as u + +import instruments as ik +from instruments.tests import expected_protocol, make_name_test + +# TESTS ###################################################################### + +test_scpi_func_gen_name = make_name_test(ik.agilent.Agilent33220a) + + +def test_agilent33220a_amplitude(): + with expected_protocol( + ik.agilent.Agilent33220a, + [ + "VOLT:UNIT?", + "VOLT?", + "VOLT:UNIT VPP", + "VOLT 2.0", + "VOLT:UNIT DBM", + "VOLT 1.5", + ], + ["VPP", "+1.000000E+00"], + ) as fg: + assert fg.amplitude == (1 * u.V, fg.VoltageMode.peak_to_peak) + fg.amplitude = 2 * u.V + fg.amplitude = (1.5 * u.V, fg.VoltageMode.dBm) + + +def test_agilent33220a_frequency(): + with expected_protocol( + ik.agilent.Agilent33220a, ["FREQ?", "FREQ 1.005000e+02"], ["+1.234000E+03"] + ) as fg: + assert fg.frequency == 1234 * u.Hz + fg.frequency = 100.5 * u.Hz + + +def test_agilent33220a_function(): + with expected_protocol( + ik.agilent.Agilent33220a, ["FUNC?", "FUNC:SQU"], ["SIN"] + ) as fg: + assert fg.function == fg.Function.sinusoid + fg.function = fg.Function.square + + +def test_agilent33220a_offset(): + with expected_protocol( + ik.agilent.Agilent33220a, + ["VOLT:OFFS?", "VOLT:OFFS 4.321000e-01"], + [ + "+1.234000E+01", + ], + ) as fg: + assert fg.offset == 12.34 * u.V + fg.offset = 0.4321 * u.V + + +def test_agilent33220a_duty_cycle(): + with expected_protocol( + ik.agilent.Agilent33220a, + ["FUNC:SQU:DCYC?", "FUNC:SQU:DCYC 75"], + [ + "53", + ], + ) as fg: + assert fg.duty_cycle == 53 + fg.duty_cycle = 75 + + +def test_agilent33220a_ramp_symmetry(): + with expected_protocol( + ik.agilent.Agilent33220a, + ["FUNC:RAMP:SYMM?", "FUNC:RAMP:SYMM 75"], + [ + "53", + ], + ) as fg: + assert fg.ramp_symmetry == 53 + fg.ramp_symmetry = 75 + + +def test_agilent33220a_output(): + with expected_protocol( + ik.agilent.Agilent33220a, + ["OUTP?", "OUTP OFF"], + [ + "ON", + ], + ) as fg: + assert fg.output is True + fg.output = False + + +def test_agilent33220a_output_sync(): + with expected_protocol( + ik.agilent.Agilent33220a, + ["OUTP:SYNC?", "OUTP:SYNC OFF"], + [ + "ON", + ], + ) as fg: + assert fg.output_sync is True + fg.output_sync = False + + +def test_agilent33220a_output_polarity(): + with expected_protocol( + ik.agilent.Agilent33220a, + ["OUTP:POL?", "OUTP:POL NORM"], + [ + "INV", + ], + ) as fg: + assert fg.output_polarity == fg.OutputPolarity.inverted + fg.output_polarity = fg.OutputPolarity.normal + + +def test_agilent33220a_load_resistance(): + with expected_protocol( + ik.agilent.Agilent33220a, + ["OUTP:LOAD?", "OUTP:LOAD?", "OUTP:LOAD 100", "OUTP:LOAD MAX"], + ["50", "INF"], + ) as fg: + assert fg.load_resistance == 50 * u.ohm + assert fg.load_resistance == fg.LoadResistance.high_impedance + fg.load_resistance = 100 * u.ohm + fg.load_resistance = fg.LoadResistance.maximum + + +@given(value=st.floats().filter(lambda x: x < 0 or x > 10000)) +def test_agilent33220a_load_resistance_value_invalid(value): + """Raise ValueError when resistance value loaded is out of range.""" + with expected_protocol(ik.agilent.Agilent33220a, [], []) as fg: + with pytest.raises(ValueError) as err_info: + fg.load_resistance = value + err_msg = err_info.value.args[0] + assert err_msg == "Load resistance must be between 0 and 10,000" + + +def test_phase_not_implemented_error(): + """Raise a NotImplementedError when getting / setting the phase.""" + with expected_protocol(ik.agilent.Agilent33220a, [], []) as fg: + with pytest.raises(NotImplementedError): + _ = fg.phase() + with pytest.raises(NotImplementedError): + fg.phase = 42 diff --git a/instruments/tests/test_agilent/test_agilent_34410a.py b/instruments/tests/test_agilent/test_agilent_34410a.py index 301e657db..4c7b1f205 100644 --- a/instruments/tests/test_agilent/test_agilent_34410a.py +++ b/instruments/tests/test_agilent/test_agilent_34410a.py @@ -1,19 +1,16 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing tests for Agilent 34410a """ # IMPORTS #################################################################### -from __future__ import absolute_import -from builtins import bytes - -import quantities as pq -import numpy as np +import pytest import instruments as ik -from instruments.tests import expected_protocol, make_name_test, unit_eq +from instruments.optional_dep_finder import numpy +from instruments.tests import expected_protocol, iterable_eq, make_name_test, unit_eq +from instruments.units import ureg as u # TESTS ###################################################################### @@ -23,15 +20,10 @@ def test_agilent34410a_read(): with expected_protocol( ik.agilent.Agilent34410a, - [ - "CONF?", - "READ?" - ], [ - "VOLT +1.000000E+01,+3.000000E-06", - "+1.86850000E-03" - ] + ["CONF?", "READ?"], + ["VOLT +1.000000E+01,+3.000000E-06", "+1.86850000E-03"], ) as dmm: - unit_eq(dmm.read_meter(), +1.86850000E-03 * pq.volt) + unit_eq(dmm.read_meter(), +1.86850000e-03 * u.volt) def test_agilent34410a_data_point_count(): @@ -39,60 +31,134 @@ def test_agilent34410a_data_point_count(): ik.agilent.Agilent34410a, [ "DATA:POIN?", - ], [ + ], + [ "+215", - ] + ], ) as dmm: assert dmm.data_point_count == 215 +def test_agilent34410a_init(): + """Switch device from `idle` to `wait-for-trigger state`.""" + with expected_protocol(ik.agilent.Agilent34410a, ["INIT"], []) as dmm: + dmm.init() + + +def test_agilent34410a_abort(): + """Abort all current measurements.""" + with expected_protocol(ik.agilent.Agilent34410a, ["ABOR"], []) as dmm: + dmm.abort() + + +def test_agilent34410a_clear_memory(): + """Clear non-volatile memory.""" + with expected_protocol(ik.agilent.Agilent34410a, ["DATA:DEL NVMEM"], []) as dmm: + dmm.clear_memory() + + def test_agilent34410a_r(): with expected_protocol( ik.agilent.Agilent34410a, + ["CONF?", "FORM:DATA REAL,64", "R? 1"], [ - "CONF?", - "FORM:DATA REAL,64", - "R? 1" - ], [ "VOLT +1.000000E+01,+3.000000E-06", # pylint: disable=no-member - b"#18" + bytes.fromhex("3FF0000000000000") - ] + b"#18" + bytes.fromhex("3FF0000000000000"), + ], ) as dmm: - unit_eq(dmm.r(1), np.array([1]) * pq.volt) + expected = (u.Quantity(1, u.volt),) + if numpy: + expected = numpy.array([1]) * u.volt + actual = dmm.r(1) + iterable_eq(actual, expected) -def test_agilent34410a_fetch(): +def test_agilent34410a_r_count_zero(): + """Read measurements with count set to zero.""" + with expected_protocol( + ik.agilent.Agilent34410a, + ["CONF?", "FORM:DATA REAL,64", "R?"], + [ + "VOLT +1.000000E+01,+3.000000E-06", + # pylint: disable=no-member + b"#18" + bytes.fromhex("3FF0000000000000"), + ], + ) as dmm: + expected = (u.Quantity(1, u.volt),) + if numpy: + expected = numpy.array([1]) * u.volt + actual = dmm.r(0) + iterable_eq(actual, expected) + + +def test_agilent34410a_r_type_error(): + """Raise TypeError if count is not a integer.""" + wrong_type = "42" with expected_protocol( ik.agilent.Agilent34410a, [ "CONF?", - "FETC?" - ], [ + ], + [ "VOLT +1.000000E+01,+3.000000E-06", - "+4.27150000E-03,5.27150000E-03" - ] + ], + ) as dmm: + with pytest.raises(TypeError) as err_info: + dmm.r(wrong_type) + err_msg = err_info.value.args[0] + assert err_msg == 'Parameter "count" must be an integer' + + +def test_agilent34410a_fetch(): + with expected_protocol( + ik.agilent.Agilent34410a, + ["CONF?", "FETC?"], + ["VOLT +1.000000E+01,+3.000000E-06", "+4.27150000E-03,5.27150000E-03"], ) as dmm: data = dmm.fetch() - unit_eq(data[0], 4.27150000E-03 * pq.volt) - unit_eq(data[1], 5.27150000E-03 * pq.volt) + expected = (4.27150000e-03 * u.volt, 5.27150000e-03 * u.volt) + if numpy: + expected = (4.27150000e-03, 5.27150000e-03) * u.volt + iterable_eq(data, expected) def test_agilent34410a_read_data(): with expected_protocol( ik.agilent.Agilent34410a, + ["CONF?", "FORM:DATA ASC", "DATA:REM? 2"], + ["VOLT +1.000000E+01,+3.000000E-06", "+4.27150000E-03,5.27150000E-03"], + ) as dmm: + data = dmm.read_data(2) + unit_eq(data[0], 4.27150000e-03 * u.volt) + unit_eq(data[1], 5.27150000e-03 * u.volt) + + +def test_agilent34410a_read_data_count_minus_one(): + """Read data for all data points available.""" + sample_count = 100 + with expected_protocol( + ik.agilent.Agilent34410a, + ["DATA:POIN?", "CONF?", "FORM:DATA ASC", f"DATA:REM? {sample_count}"], [ - "CONF?", - "FORM:DATA ASC", - "DATA:REM? 2" - ], [ + f"{sample_count}", "VOLT +1.000000E+01,+3.000000E-06", - "+4.27150000E-03,5.27150000E-03" - ] + "+4.27150000E-03,5.27150000E-03", + ], ) as dmm: - data = dmm.read_data(2) - unit_eq(data[0], 4.27150000E-03 * pq.volt) - unit_eq(data[1], 5.27150000E-03 * pq.volt) + data = dmm.read_data(-1) + unit_eq(data[0], 4.27150000e-03 * u.volt) + unit_eq(data[1], 5.27150000e-03 * u.volt) + + +def test_agilent34410a_read_data_type_error(): + """Raise Type error if count is not an integer.""" + wrong_type = "42" + with expected_protocol(ik.agilent.Agilent34410a, [], []) as dmm: + with pytest.raises(TypeError) as err_info: + dmm.read_data(wrong_type) + err_msg = err_info.value.args[0] + assert err_msg == 'Parameter "sample_count" must be an integer.' def test_agilent34410a_read_data_nvmem(): @@ -101,14 +167,12 @@ def test_agilent34410a_read_data_nvmem(): [ "CONF?", "DATA:DATA? NVMEM", - ], [ - "VOLT +1.000000E+01,+3.000000E-06", - "+4.27150000E-03,5.27150000E-03" - ] + ], + ["VOLT +1.000000E+01,+3.000000E-06", "+4.27150000E-03,5.27150000E-03"], ) as dmm: data = dmm.read_data_nvmem() - unit_eq(data[0], 4.27150000E-03 * pq.volt) - unit_eq(data[1], 5.27150000E-03 * pq.volt) + unit_eq(data[0], 4.27150000e-03 * u.volt) + unit_eq(data[1], 5.27150000e-03 * u.volt) def test_agilent34410a_read_last_data(): @@ -116,8 +180,18 @@ def test_agilent34410a_read_last_data(): ik.agilent.Agilent34410a, [ "DATA:LAST?", - ], [ + ], + [ "+1.73730000E-03 VDC", - ] + ], + ) as dmm: + unit_eq(dmm.read_last_data(), 1.73730000e-03 * u.volt) + + +def test_agilent34410a_read_last_data_na(): + """Return 9.91e37 if no data are available to read.""" + na_value_str = "9.91000000E+37" + with expected_protocol( + ik.agilent.Agilent34410a, ["DATA:LAST?"], [na_value_str] ) as dmm: - unit_eq(dmm.read_last_data(), 1.73730000E-03 * pq.volt) + assert dmm.read_last_data() == float(na_value_str) diff --git a/instruments/tests/test_base_instrument.py b/instruments/tests/test_base_instrument.py index 46b1d323a..c3ddbafa2 100644 --- a/instruments/tests/test_base_instrument.py +++ b/instruments/tests/test_base_instrument.py @@ -1,34 +1,41 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing tests for the base Instrument class """ # IMPORTS #################################################################### -from __future__ import absolute_import import socket import io - -from builtins import bytes import serial +import usb.core from serial.tools.list_ports_common import ListPortInfo -from nose.tools import raises -import mock - -import numpy as np +import pytest import instruments as ik +from instruments.optional_dep_finder import numpy from instruments.tests import expected_protocol + # pylint: disable=unused-import from instruments.abstract_instruments.comm import ( - SocketCommunicator, USBCommunicator, VisaCommunicator, FileCommunicator, - LoopbackCommunicator, GPIBCommunicator, AbstractCommunicator, - USBTMCCommunicator, VXI11Communicator, serial_manager, SerialCommunicator + SocketCommunicator, + USBCommunicator, + VisaCommunicator, + FileCommunicator, + LoopbackCommunicator, + GPIBCommunicator, + AbstractCommunicator, + USBTMCCommunicator, + VXI11Communicator, + serial_manager, + SerialCommunicator, ) from instruments.errors import AcknowledgementError, PromptError +from instruments.tests import iterable_eq + +from . import mock # TESTS ###################################################################### @@ -37,6 +44,7 @@ # BINBLOCKREAD TESTS + def test_instrument_binblockread(): with expected_protocol( ik.Instrument, @@ -44,9 +52,13 @@ def test_instrument_binblockread(): [ b"#210" + bytes.fromhex("00000001000200030004") + b"0", ], - sep="\n" + sep="\n", ) as inst: - np.testing.assert_array_equal(inst.binblockread(2), [0, 1, 2, 3, 4]) + actual_data = inst.binblockread(2) + expected = (0, 1, 2, 3, 4) + if numpy: + expected = numpy.array(expected) + iterable_eq(actual_data, expected) def test_instrument_binblockread_two_reads(): @@ -56,34 +68,38 @@ def test_instrument_binblockread_two_reads(): side_effect=[b"#", b"2", b"10", data[:6], data[6:]] ) - np.testing.assert_array_equal(inst.binblockread(2), [0, 1, 2, 3, 4]) + expected = (0, 1, 2, 3, 4) + if numpy: + expected = numpy.array((0, 1, 2, 3, 4)) + iterable_eq(inst.binblockread(2), expected) calls_expected = [1, 1, 2, 10, 4] calls_actual = [call[0][0] for call in inst._file.read_raw.call_args_list] - np.testing.assert_array_equal(calls_expected, calls_actual) + iterable_eq(calls_actual, calls_expected) -@raises(IOError) def test_instrument_binblockread_too_many_reads(): - inst = ik.Instrument.open_test() - data = bytes.fromhex("00000001000200030004") - inst._file.read_raw = mock.MagicMock( - side_effect=[b"#", b"2", b"10", data[:6], b"", b"", b""] - ) + with pytest.raises(IOError): + inst = ik.Instrument.open_test() + data = bytes.fromhex("00000001000200030004") + inst._file.read_raw = mock.MagicMock( + side_effect=[b"#", b"2", b"10", data[:6], b"", b"", b""] + ) - _ = inst.binblockread(2) + _ = inst.binblockread(2) -@raises(IOError) def test_instrument_binblockread_bad_block_start(): - inst = ik.Instrument.open_test() - inst._file.read_raw = mock.MagicMock(return_value=b"@") + with pytest.raises(IOError): + inst = ik.Instrument.open_test() + inst._file.read_raw = mock.MagicMock(return_value=b"@") - _ = inst.binblockread(2) + _ = inst.binblockread(2) # OPEN CONNECTION TESTS + @mock.patch("instruments.abstract_instruments.instrument.SocketCommunicator") @mock.patch("instruments.abstract_instruments.instrument.socket") def test_instrument_open_tcpip(mock_socket, mock_socket_comm): @@ -100,25 +116,25 @@ def test_instrument_open_tcpip(mock_socket, mock_socket_comm): @mock.patch("instruments.abstract_instruments.instrument.serial_manager") def test_instrument_open_serial(mock_serial_manager): - mock_serial_manager.new_serial_connection.return_value.__class__ = SerialCommunicator + mock_serial_manager.new_serial_connection.return_value.__class__ = ( + SerialCommunicator + ) inst = ik.Instrument.open_serial("/dev/port", baud=1234) assert isinstance(inst._file, SerialCommunicator) is True mock_serial_manager.new_serial_connection.assert_called_with( - "/dev/port", - baud=1234, - timeout=3, - write_timeout=3 + "/dev/port", baud=1234, timeout=3, write_timeout=3 ) -class fake_serial(object): +class fake_serial: """ Create a fake serial.Serial() object so that tests can be run without accessing a non-existant port. """ + # pylint: disable=unused-variable, unused-argument, no-self-use def __init__(self, device, baudrate=None, timeout=None, writeTimeout=None): self.device = device @@ -132,156 +148,171 @@ def isOpen(self): # TEST OPEN_SERIAL WITH USB IDENTIFIERS ###################################### + def fake_comports(): """ Generate a fake list of comports to compare against. """ - fake_device = ListPortInfo() + fake_device = ListPortInfo(device="COM1") fake_device.vid = 0 fake_device.pid = 1000 - fake_device.serial_number = 'a1' - fake_device.device = 'COM1' + fake_device.serial_number = "a1" - fake_device2 = ListPortInfo() + fake_device2 = ListPortInfo(device="COM2") fake_device2.vid = 1 fake_device2.pid = 1010 - fake_device2.serial_number = 'c0' - fake_device2.device = 'COM2' + fake_device2.serial_number = "c0" return [fake_device, fake_device2] @mock.patch("instruments.abstract_instruments.instrument.comports", new=fake_comports) @mock.patch("instruments.abstract_instruments.instrument.serial_manager") def test_instrument_open_serial_by_usb_ids(mock_serial_manager): - mock_serial_manager.new_serial_connection.return_value.__class__ = SerialCommunicator + mock_serial_manager.new_serial_connection.return_value.__class__ = ( + SerialCommunicator + ) inst = ik.Instrument.open_serial(baud=1234, vid=1, pid=1010) assert isinstance(inst._file, SerialCommunicator) is True mock_serial_manager.new_serial_connection.assert_called_with( - "COM2", - baud=1234, - timeout=3, - write_timeout=3 + "COM2", baud=1234, timeout=3, write_timeout=3 ) @mock.patch("instruments.abstract_instruments.instrument.comports", new=fake_comports) @mock.patch("instruments.abstract_instruments.instrument.serial_manager") def test_instrument_open_serial_by_usb_ids_and_serial_number(mock_serial_manager): - mock_serial_manager.new_serial_connection.return_value.__class__ = SerialCommunicator + mock_serial_manager.new_serial_connection.return_value.__class__ = ( + SerialCommunicator + ) inst = ik.Instrument.open_serial(baud=1234, vid=0, pid=1000, serial_number="a1") assert isinstance(inst._file, SerialCommunicator) is True mock_serial_manager.new_serial_connection.assert_called_with( - "COM1", - baud=1234, - timeout=3, - write_timeout=3 + "COM1", baud=1234, timeout=3, write_timeout=3 ) -@raises(serial.SerialException) @mock.patch("instruments.abstract_instruments.instrument.comports") @mock.patch("instruments.abstract_instruments.instrument.serial_manager") def test_instrument_open_serial_by_usb_ids_multiple_matches(_, mock_comports): - fake_device = ListPortInfo() - fake_device.vid = 0 - fake_device.pid = 1000 - fake_device.serial_number = 'a1' - fake_device.device = 'COM1' + with pytest.raises(serial.SerialException): + fake_device = ListPortInfo(device="COM1") + fake_device.vid = 0 + fake_device.pid = 1000 + fake_device.serial_number = "a1" - fake_device2 = ListPortInfo() - fake_device2.vid = 0 - fake_device2.pid = 1000 - fake_device2.serial_number = 'b2' - fake_device2.device = 'COM2' + fake_device2 = ListPortInfo(device="COM2") + fake_device2.vid = 0 + fake_device2.pid = 1000 + fake_device2.serial_number = "b2" - mock_comports.return_value = [fake_device, fake_device2] + mock_comports.return_value = [fake_device, fake_device2] - _ = ik.Instrument.open_serial(baud=1234, vid=0, pid=1000) + _ = ik.Instrument.open_serial(baud=1234, vid=0, pid=1000) -@raises(ValueError) @mock.patch("instruments.abstract_instruments.instrument.comports", new=fake_comports) @mock.patch("instruments.abstract_instruments.instrument.serial_manager") def test_instrument_open_serial_by_usb_ids_incorrect_serial_num(mock_serial_manager): - mock_serial_manager.new_serial_connection.return_value.__class__ = SerialCommunicator - _ = ik.Instrument.open_serial(baud=1234, vid=0, pid=1000, serial_number="xyz") + with pytest.raises(ValueError): + mock_serial_manager.new_serial_connection.return_value.__class__ = ( + SerialCommunicator + ) + _ = ik.Instrument.open_serial(baud=1234, vid=0, pid=1000, serial_number="xyz") -@raises(ValueError) @mock.patch("instruments.abstract_instruments.instrument.comports", new=fake_comports) @mock.patch("instruments.abstract_instruments.instrument.serial_manager") def test_instrument_open_serial_by_usb_ids_cant_find(mock_serial_manager): - mock_serial_manager.new_serial_connection.return_value.__class__ = SerialCommunicator - _ = ik.Instrument.open_serial(baud=1234, vid=1234, pid=1000) + with pytest.raises(ValueError): + mock_serial_manager.new_serial_connection.return_value.__class__ = ( + SerialCommunicator + ) + _ = ik.Instrument.open_serial(baud=1234, vid=1234, pid=1000) -@raises(ValueError) @mock.patch("instruments.abstract_instruments.instrument.comports", new=fake_comports) @mock.patch("instruments.abstract_instruments.instrument.serial_manager") def test_instrument_open_serial_no_port(mock_serial_manager): - mock_serial_manager.new_serial_connection.return_value.__class__ = SerialCommunicator - _ = ik.Instrument.open_serial(baud=1234) + with pytest.raises(ValueError): + mock_serial_manager.new_serial_connection.return_value.__class__ = ( + SerialCommunicator + ) + _ = ik.Instrument.open_serial(baud=1234) -@raises(ValueError) @mock.patch("instruments.abstract_instruments.instrument.comports", new=fake_comports) @mock.patch("instruments.abstract_instruments.instrument.serial_manager") def test_instrument_open_serial_by_usb_ids_and_port(mock_serial_manager): - mock_serial_manager.new_serial_connection.return_value.__class__ = SerialCommunicator - _ = ik.Instrument.open_serial(port="COM1", baud=1234, vid=1234, pid=1000) + with pytest.raises(ValueError): + mock_serial_manager.new_serial_connection.return_value.__class__ = ( + SerialCommunicator + ) + _ = ik.Instrument.open_serial(port="COM1", baud=1234, vid=1234, pid=1000) -@raises(ValueError) @mock.patch("instruments.abstract_instruments.instrument.comports", new=fake_comports) @mock.patch("instruments.abstract_instruments.instrument.serial_manager") def test_instrument_open_serial_by_usb_vid_no_pid(mock_serial_manager): - mock_serial_manager.new_serial_connection.return_value.__class__ = SerialCommunicator - _ = ik.Instrument.open_serial(baud=1234, vid=1234) + with pytest.raises(ValueError): + mock_serial_manager.new_serial_connection.return_value.__class__ = ( + SerialCommunicator + ) + _ = ik.Instrument.open_serial(baud=1234, vid=1234) -@raises(ValueError) @mock.patch("instruments.abstract_instruments.instrument.comports", new=fake_comports) @mock.patch("instruments.abstract_instruments.instrument.serial_manager") def test_instrument_open_serial_by_usb_pid_no_vid(mock_serial_manager): - mock_serial_manager.new_serial_connection.return_value.__class__ = SerialCommunicator - _ = ik.Instrument.open_serial(baud=1234, pid=1234) + with pytest.raises(ValueError): + mock_serial_manager.new_serial_connection.return_value.__class__ = ( + SerialCommunicator + ) + _ = ik.Instrument.open_serial(baud=1234, pid=1234) # TEST OPEN_GPIBUSB ########################################################## + @mock.patch("instruments.abstract_instruments.instrument.GPIBCommunicator") @mock.patch("instruments.abstract_instruments.instrument.serial_manager") def test_instrument_open_gpibusb(mock_serial_manager, mock_gpib_comm): - mock_serial_manager.new_serial_connection.return_value.__class__ = SerialCommunicator + mock_serial_manager.new_serial_connection.return_value.__class__ = ( + SerialCommunicator + ) mock_gpib_comm.return_value.__class__ = GPIBCommunicator - inst = ik.Instrument.open_gpibusb("/dev/port", gpib_address=1) + inst = ik.Instrument.open_gpibusb("/dev/port", gpib_address=1, model="gi") assert isinstance(inst._file, GPIBCommunicator) is True mock_serial_manager.new_serial_connection.assert_called_with( - "/dev/port", - baud=460800, - timeout=3, - write_timeout=3 + "/dev/port", baud=460800, timeout=3, write_timeout=3 ) mock_gpib_comm.assert_called_with( - mock_serial_manager.new_serial_connection.return_value, - 1 + mock_serial_manager.new_serial_connection.return_value, 1, "gi" ) -@raises(ImportError) -@mock.patch("instruments.abstract_instruments.instrument.visa", new=None) -def test_instrument_open_visa_import_error(): - _ = ik.Instrument.open_visa("abc123") +@mock.patch("instruments.abstract_instruments.instrument.GPIBCommunicator") +@mock.patch("instruments.abstract_instruments.instrument.socket") +def test_instrument_open_gpibethernet(mock_socket_manager, mock_gpib_comm): + mock_gpib_comm.return_value.__class__ = GPIBCommunicator + + host = "192.168.1.13" + port = 1818 + + inst = ik.Instrument.open_gpibethernet(host, port, gpib_address=1, model="pl") + + mock_socket_manager.socket.assert_called() + mock_socket_manager.socket().connect.assert_called_with((host, port)) + assert isinstance(inst._file, GPIBCommunicator) is True @mock.patch("instruments.abstract_instruments.instrument.VisaCommunicator") -@mock.patch("instruments.abstract_instruments.instrument.visa") +@mock.patch("instruments.abstract_instruments.instrument.pyvisa") def test_instrument_open_visa_new_version(mock_visa, mock_visa_comm): mock_visa_comm.return_value.__class__ = VisaCommunicator mock_visa.__version__ = "1.8" @@ -296,7 +327,7 @@ def test_instrument_open_visa_new_version(mock_visa, mock_visa_comm): @mock.patch("instruments.abstract_instruments.instrument.VisaCommunicator") -@mock.patch("instruments.abstract_instruments.instrument.visa") +@mock.patch("instruments.abstract_instruments.instrument.pyvisa") def test_instrument_open_visa_old_version(mock_visa, mock_visa_comm): mock_visa_comm.return_value.__class__ = VisaCommunicator mock_visa.__version__ = "1.5" @@ -332,6 +363,35 @@ def test_instrument_open_vxi11(mock_vxi11_comm): mock_vxi11_comm.assert_called_with("string", 1, key1="value") +@mock.patch("instruments.abstract_instruments.instrument.USBCommunicator") +@mock.patch("instruments.abstract_instruments.instrument.usb") +def test_instrument_open_usb(mock_usb, mock_usb_comm): + """Open USB device.""" + mock_usb.core.find.return_value.__class__ = usb.core.Device + mock_usb_comm.return_value.__class__ = USBCommunicator + + # fake instrument + vid = "0x1000" + pid = "0x1000" + dev = mock_usb.core.find(idVendor=vid, idProduct=pid) + + # call instrument + inst = ik.Instrument.open_usb(vid, pid) + + assert isinstance(inst._file, USBCommunicator) + mock_usb_comm.assert_called_with(dev) + + +@mock.patch("instruments.abstract_instruments.instrument.usb") +def test_instrument_open_usb_no_device(mock_usb): + """Open USB, no device found.""" + mock_usb.core.find.return_value = None # mock no instrument found + with pytest.raises(IOError) as err: + _ = ik.Instrument.open_usb(0x1000, 0x1000) + err_msg = err.value.args[0] + assert err_msg == "No such device found." + + @mock.patch("instruments.abstract_instruments.instrument.USBTMCCommunicator") def test_instrument_open_usbtmc(mock_usbtmc_comm): mock_usbtmc_comm.return_value.__class__ = USBTMCCommunicator @@ -356,6 +416,7 @@ def test_instrument_open_file(mock_file_comm): # OPEN URI TESTS + @mock.patch("instruments.abstract_instruments.instrument.Instrument.open_serial") def test_instrument_open_from_uri_serial(mock_open_conn): _ = ik.Instrument.open_from_uri("serial:///dev/foobar") @@ -419,16 +480,17 @@ def test_instrument_open_from_uri_vxi11(mock_open_conn): mock_open_conn.assert_called_with("TCPIP::192.168.1.105::gpib,5::INSTR") -@raises(NotImplementedError) def test_instrument_open_from_uri_invalid_scheme(): - _ = ik.Instrument.open_from_uri("foo://bar") + with pytest.raises(NotImplementedError): + _ = ik.Instrument.open_from_uri("foo://bar") # INIT TESTS -@raises(TypeError) + def test_instrument_init_bad_filelike(): - _ = ik.Instrument(mock.MagicMock()) + with pytest.raises(TypeError): + _ = ik.Instrument(mock.MagicMock()) def test_instrument_init(): @@ -452,6 +514,7 @@ def test_instrument_init_loopbackcomm(): # COMM TESTS + def test_instrument_default_ack_expected(): mock_filelike = mock.MagicMock() mock_filelike.__class__ = AbstractCommunicator @@ -503,19 +566,19 @@ def new_ack(msg): inst._file.sendcmd.assert_called_with("foobar") -@raises(AcknowledgementError) def test_instrument_sendcmd_bad_ack(): - mock_filelike = mock.MagicMock() - mock_filelike.__class__ = AbstractCommunicator - inst = ik.Instrument(mock_filelike) + with pytest.raises(AcknowledgementError): + mock_filelike = mock.MagicMock() + mock_filelike.__class__ = AbstractCommunicator + inst = ik.Instrument(mock_filelike) - def new_ack(msg): - return msg + def new_ack(msg): + return msg - inst._ack_expected = new_ack - inst.read = mock.MagicMock(return_value="derp") + inst._ack_expected = new_ack + inst.read = mock.MagicMock(return_value="derp") - inst.sendcmd("foobar") + inst.sendcmd("foobar") def test_instrument_sendcmd_noack(): @@ -531,16 +594,16 @@ def test_instrument_sendcmd_noack(): inst._file.sendcmd.assert_called_with("foobar") -@raises(PromptError) def test_instrument_sendcmd_noack_bad_prompt(): - mock_filelike = mock.MagicMock() - mock_filelike.__class__ = AbstractCommunicator - inst = ik.Instrument(mock_filelike) + with pytest.raises(PromptError): + mock_filelike = mock.MagicMock() + mock_filelike.__class__ = AbstractCommunicator + inst = ik.Instrument(mock_filelike) - inst.prompt = "> " - inst.read = mock.MagicMock(return_value="* ") + inst.prompt = "> " + inst.read = mock.MagicMock(return_value="* ") - inst.sendcmd("foobar") + inst.sendcmd("foobar") def test_instrument_sendcmd(): @@ -622,19 +685,19 @@ def new_ack(msg): inst.read.assert_called_with(-1) -@raises(AcknowledgementError) def test_instrument_query_bad_ack(): - mock_filelike = mock.MagicMock() - mock_filelike.__class__ = AbstractCommunicator - inst = ik.Instrument(mock_filelike) - inst.read = mock.MagicMock(return_value="derp") + with pytest.raises(AcknowledgementError): + mock_filelike = mock.MagicMock() + mock_filelike.__class__ = AbstractCommunicator + inst = ik.Instrument(mock_filelike) + inst.read = mock.MagicMock(return_value="derp") - def new_ack(msg): - return msg + def new_ack(msg): + return msg - inst._ack_expected = new_ack + inst._ack_expected = new_ack - _ = inst.query("foobar?") + _ = inst.query("foobar?") def test_instrument_query_noack(): @@ -659,17 +722,17 @@ def test_instrument_query_noack(): inst.read.assert_called_with(2) -@raises(PromptError) def test_instrument_query_noack_bad_prompt(): - mock_filelike = mock.MagicMock() - mock_filelike.__class__ = AbstractCommunicator - inst = ik.Instrument(mock_filelike) - inst._file.query.return_value = "datas" + with pytest.raises(PromptError): + mock_filelike = mock.MagicMock() + mock_filelike.__class__ = AbstractCommunicator + inst = ik.Instrument(mock_filelike) + inst._file.query.return_value = "datas" - inst.prompt = "> " - inst.read = mock.MagicMock(return_value="* ") + inst.prompt = "> " + inst.read = mock.MagicMock(return_value="* ") - _ = inst.query("foobar?") + _ = inst.query("foobar?") def test_instrument_query(): @@ -710,13 +773,13 @@ def test_instrument_read(): inst._file.read.return_value = "foobar" assert inst.read() == "foobar" - inst._file.read.assert_called_with(-1) + inst._file.read.assert_called_with(-1, "utf-8") inst._file = mock.MagicMock() inst._file.read.return_value = "foobar" assert inst.read(6) == "foobar" - inst._file.read.assert_called_with(6) + inst._file.read.assert_called_with(6, "utf-8") def test_instrument_write(): @@ -730,6 +793,7 @@ def test_instrument_write(): # PROPERTIES # + def test_instrument_timeout(): mock_filelike = mock.MagicMock() mock_filelike.__class__ = AbstractCommunicator diff --git a/instruments/tests/test_comm/test_file.py b/instruments/tests/test_comm/test_file.py index fcd242077..d35d8012b 100644 --- a/instruments/tests/test_comm/test_file.py +++ b/instruments/tests/test_comm/test_file.py @@ -1,17 +1,15 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Unit tests for the file communication layer """ # IMPORTS #################################################################### -from __future__ import absolute_import -from nose.tools import raises, eq_ -import mock +import pytest from instruments.abstract_instruments.comm import FileCommunicator +from .. import mock # TEST CASES ################################################################# @@ -33,7 +31,7 @@ def test_filecomm_address_getter(): mock_name = mock.PropertyMock(return_value="/home/user/file") type(comm._filelike).name = mock_name - eq_(comm.address, "/home/user/file") + assert comm.address == "/home/user/file" mock_name.assert_called_with() @@ -43,37 +41,37 @@ def test_filecomm_address_getter_no_name(): del comm._filelike.name - eq_(comm.address, None) + assert comm.address is None -@raises(NotImplementedError) def test_filecomm_address_setter(): - comm = FileCommunicator(mock.MagicMock()) - comm.address = "abc123" + with pytest.raises(NotImplementedError): + comm = FileCommunicator(mock.MagicMock()) + comm.address = "abc123" def test_filecomm_terminator(): comm = FileCommunicator(mock.MagicMock()) - eq_(comm.terminator, "\n") + assert comm.terminator == "\n" comm.terminator = "*" - eq_(comm._terminator, "*") + assert comm._terminator == "*" - comm.terminator = b"*" # pylint: disable=redefined-variable-type - eq_(comm._terminator, "*") + comm.terminator = b"*" + assert comm._terminator == "*" -@raises(NotImplementedError) def test_filecomm_timeout_getter(): - comm = FileCommunicator(mock.MagicMock()) - _ = comm.timeout + with pytest.raises(NotImplementedError): + comm = FileCommunicator(mock.MagicMock()) + _ = comm.timeout -@raises(NotImplementedError) def test_filecomm_timeout_setter(): - comm = FileCommunicator(mock.MagicMock()) - comm.timeout = 1 + with pytest.raises(NotImplementedError): + comm = FileCommunicator(mock.MagicMock()) + comm.timeout = 1 def test_filecomm_close(): @@ -87,7 +85,7 @@ def test_filecomm_read_raw(): comm = FileCommunicator(mock.MagicMock()) comm._filelike.read = mock.MagicMock(side_effect=[b"a", b"b", b"c", b"\n"]) - eq_(comm.read_raw(), b"abc") + assert comm.read_raw() == b"abc" comm._filelike.read.assert_has_calls([mock.call(1)] * 4) assert comm._filelike.read.call_count == 4 @@ -115,7 +113,7 @@ def test_filecomm_query(): comm._testing = True # to disable the delay in the _query function comm._filelike.read = mock.MagicMock(side_effect=[b"a", b"b", b"c", b"\n"]) - eq_(comm._query("mock"), "abc") + assert comm._query("mock") == "abc" def test_filecomm_seek(): @@ -128,7 +126,7 @@ def test_filecomm_tell(): comm = FileCommunicator(mock.MagicMock()) comm._filelike.tell.return_value = 5 - eq_(comm.tell(), 5) + assert comm.tell() == 5 comm._filelike.tell.assert_called_with() diff --git a/instruments/tests/test_comm/test_gi_gpibusb.py b/instruments/tests/test_comm/test_gpibusb.py similarity index 72% rename from instruments/tests/test_comm/test_gi_gpibusb.py rename to instruments/tests/test_comm/test_gpibusb.py index bee107953..e699b6bba 100644 --- a/instruments/tests/test_comm/test_gi_gpibusb.py +++ b/instruments/tests/test_comm/test_gpibusb.py @@ -1,20 +1,18 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ -Unit tests for the GI GPIBUSB communication layer +Unit tests for the GPIBUSB communication layer """ # IMPORTS #################################################################### -from __future__ import absolute_import -from nose.tools import raises, eq_ -import mock +import pytest import serial -import quantities as pq +from instruments.units import ureg as u from instruments.abstract_instruments.comm import GPIBCommunicator, SerialCommunicator from instruments.tests import unit_eq +from .. import mock # TEST CASES ################################################################# @@ -34,11 +32,11 @@ def test_gpibusbcomm_init_correct_values_new_firmware(): mock_gpib.query.return_value = "5" comm = GPIBCommunicator(mock_gpib, 1) - eq_(comm._terminator, "\n") - eq_(comm._version, 5) - eq_(comm._eos, "\n") - eq_(comm._eoi, True) - unit_eq(comm._timeout, 1000 * pq.millisecond) + assert comm._terminator == "\n" + assert comm._version == 5 + assert comm._eos == "\n" + assert comm._eoi is True + unit_eq(comm._timeout, 1000 * u.millisecond) def test_gpibusbcomm_init_correct_values_old_firmware(): @@ -47,7 +45,7 @@ def test_gpibusbcomm_init_correct_values_old_firmware(): mock_gpib.query.return_value = "4" comm = GPIBCommunicator(mock_gpib, 1) - eq_(comm._eos, 10) + assert comm._eos == 10 def test_gpibusbcomm_address(): @@ -58,30 +56,30 @@ def test_gpibusbcomm_address(): type(comm._file).address = port_name # Check that our address function is working - eq_(comm.address, (1, "/dev/address")) + assert comm.address == (1, "/dev/address") port_name.assert_called_with() # Able to set GPIB address comm.address = 5 - eq_(comm._gpib_address, 5) + assert comm._gpib_address == 5 # Able to set address with a list - comm.address = [6, "/dev/foobar"] # pylint: disable=redefined-variable-type - eq_(comm._gpib_address, 6) + comm.address = [6, "/dev/foobar"] + assert comm._gpib_address == 6 port_name.assert_called_with("/dev/foobar") -@raises(ValueError) def test_gpibusbcomm_address_out_of_range(): - comm = GPIBCommunicator(mock.MagicMock(), 1) + with pytest.raises(ValueError): + comm = GPIBCommunicator(mock.MagicMock(), 1) - comm.address = 31 + comm.address = 31 -@raises(TypeError) def test_gpibusbcomm_address_wrong_type(): - comm = GPIBCommunicator(mock.MagicMock(), 1) - comm.address = "derp" + with pytest.raises(TypeError): + comm = GPIBCommunicator(mock.MagicMock(), 1) + comm.address = "derp" def test_gpibusbcomm_eoi(): @@ -90,14 +88,14 @@ def test_gpibusbcomm_eoi(): comm._file.sendcmd = mock.MagicMock() comm.eoi = True - eq_(comm.eoi, True) - eq_(comm._eoi, True) + assert comm.eoi is True + assert comm._eoi is True comm._file.sendcmd.assert_called_with("++eoi 1") comm._file.sendcmd = mock.MagicMock() comm.eoi = False - eq_(comm.eoi, False) - eq_(comm._eoi, False) + assert comm.eoi is False + assert comm._eoi is False comm._file.sendcmd.assert_called_with("++eoi 0") @@ -107,22 +105,22 @@ def test_gpibusbcomm_eoi_old_firmware(): comm._file.sendcmd = mock.MagicMock() comm.eoi = True - eq_(comm.eoi, True) - eq_(comm._eoi, True) + assert comm.eoi is True + assert comm._eoi is True comm._file.sendcmd.assert_called_with("+eoi:1") comm._file.sendcmd = mock.MagicMock() comm.eoi = False - eq_(comm.eoi, False) - eq_(comm._eoi, False) + assert comm.eoi is False + assert comm._eoi is False comm._file.sendcmd.assert_called_with("+eoi:0") -@raises(TypeError) def test_gpibusbcomm_eoi_bad_type(): - comm = GPIBCommunicator(mock.MagicMock(), 1) - comm._version = 5 - comm.eoi = "abc" + with pytest.raises(TypeError): + comm = GPIBCommunicator(mock.MagicMock(), 1) + comm._version = 5 + comm.eoi = "abc" def test_gpibusbcomm_eos_rn(): @@ -131,8 +129,8 @@ def test_gpibusbcomm_eos_rn(): comm._file.sendcmd = mock.MagicMock() comm.eos = "\r\n" - eq_(comm.eos, "\r\n") - eq_(comm._eos, "\r\n") + assert comm.eos == "\r\n" + assert comm._eos == "\r\n" comm._file.sendcmd.assert_called_with("++eos 0") @@ -142,8 +140,8 @@ def test_gpibusbcomm_eos_r(): comm._file.sendcmd = mock.MagicMock() comm.eos = "\r" - eq_(comm.eos, "\r") - eq_(comm._eos, "\r") + assert comm.eos == "\r" + assert comm._eos == "\r" comm._file.sendcmd.assert_called_with("++eos 1") @@ -153,16 +151,16 @@ def test_gpibusbcomm_eos_n(): comm._file.sendcmd = mock.MagicMock() comm.eos = "\n" - eq_(comm.eos, "\n") - eq_(comm._eos, "\n") + assert comm.eos == "\n" + assert comm._eos == "\n" comm._file.sendcmd.assert_called_with("++eos 2") -@raises(ValueError) def test_gpibusbcomm_eos_invalid(): - comm = GPIBCommunicator(mock.MagicMock(), 1) - comm._version = 5 - comm.eos = "*" + with pytest.raises(ValueError): + comm = GPIBCommunicator(mock.MagicMock(), 1) + comm._version = 5 + comm.eos = "*" def test_gpibusbcomm_eos_old_firmware(): @@ -171,7 +169,7 @@ def test_gpibusbcomm_eos_old_firmware(): comm._file.sendcmd = mock.MagicMock() comm.eos = "\n" - eq_(comm._eos, 10) + assert comm._eos == 10 comm._file.sendcmd.assert_called_with("+eos:10") @@ -180,26 +178,26 @@ def test_gpibusbcomm_terminator(): comm._version = 5 # Default terminator should be eoi - eq_(comm.terminator, "eoi") - eq_(comm._eoi, True) + assert comm.terminator == "eoi" + assert comm._eoi is True comm.terminator = "\n" - eq_(comm.terminator, "\n") - eq_(comm._eoi, False) + assert comm.terminator == "\n" + assert comm._eoi is False comm.terminator = "eoi" - eq_(comm.terminator, "eoi") - eq_(comm._eoi, True) + assert comm.terminator == "eoi" + assert comm._eoi is True def test_gpibusbcomm_timeout(): comm = GPIBCommunicator(mock.MagicMock(), 1) comm._version = 5 - unit_eq(comm.timeout, 1000 * pq.millisecond) + unit_eq(comm.timeout, 1000 * u.millisecond) - comm.timeout = 5000 * pq.millisecond - comm._file.sendcmd.assert_called_with("++read_tmo_ms 5000.0") + comm.timeout = 5000 * u.millisecond + comm._file.sendcmd.assert_called_with("++read_tmo_ms 5000") def test_gpibusbcomm_close(): @@ -215,7 +213,7 @@ def test_gpibusbcomm_read_raw(): comm._version = 5 comm._file.read_raw = mock.MagicMock(return_value=b"abc") - eq_(comm.read_raw(3), b"abc") + assert comm.read_raw(3) == b"abc" comm._file.read_raw.assert_called_with(3) @@ -232,13 +230,15 @@ def test_gpibusbcomm_sendcmd(): comm._version = 5 comm._sendcmd("mock") - comm._file.sendcmd.assert_has_calls([ - mock.call("+a:1"), - mock.call("++eoi 1"), - mock.call("++read_tmo_ms 1000.0"), - mock.call("++eos 2"), - mock.call("mock") - ]) + comm._file.sendcmd.assert_has_calls( + [ + mock.call("+a:1"), + mock.call("++eoi 1"), + mock.call("++read_tmo_ms 1000"), + mock.call("++eos 2"), + mock.call("mock"), + ] + ) def test_gpibusbcomm_sendcmd_empty_string(): @@ -257,7 +257,7 @@ def test_gpibusbcomm_query(): comm._file.read = mock.MagicMock(return_value="answer") comm.sendcmd = mock.MagicMock() - eq_(comm._query("mock?"), "answer") + assert comm._query("mock?") == "answer" comm.sendcmd.assert_called_with("mock?") comm._file.read.assert_called_with(-1) @@ -273,7 +273,7 @@ def test_gpibusbcomm_query_no_question_mark(): comm._file.read = mock.MagicMock(return_value="answer") comm.sendcmd = mock.MagicMock() - eq_(comm._query("mock"), "answer") + assert comm._query("mock") == "answer" comm.sendcmd.assert_called_with("mock") comm._file.read.assert_called_with(-1) comm._file.sendcmd.assert_has_calls([mock.call("+read")]) diff --git a/instruments/tests/test_comm/test_loopback.py b/instruments/tests/test_comm/test_loopback.py index 66e5aae5d..0aa9ab46f 100644 --- a/instruments/tests/test_comm/test_loopback.py +++ b/instruments/tests/test_comm/test_loopback.py @@ -1,17 +1,15 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Unit tests for the loopback communication layer """ # IMPORTS #################################################################### -from __future__ import absolute_import -from nose.tools import raises, eq_ -import mock +import pytest from instruments.abstract_instruments.comm import LoopbackCommunicator +from .. import mock # TEST CASES ################################################################# @@ -34,7 +32,7 @@ def test_loopbackcomm_address(mock_sys): comm._conn = mock.MagicMock() # Check that our address function is working - eq_(comm.address, "address") + assert comm.address == "address" mock_name.assert_called_with() @@ -42,28 +40,28 @@ def test_loopbackcomm_terminator(): comm = LoopbackCommunicator() # Default terminator should be \n - eq_(comm.terminator, "\n") + assert comm.terminator == "\n" comm.terminator = b"*" - eq_(comm.terminator, "*") - eq_(comm._terminator, "*") + assert comm.terminator == "*" + assert comm._terminator == "*" - comm.terminator = u"\r" # pylint: disable=redefined-variable-type - eq_(comm.terminator, u"\r") - eq_(comm._terminator, u"\r") + comm.terminator = "\r" + assert comm.terminator == "\r" + assert comm._terminator == "\r" comm.terminator = "\r\n" - eq_(comm.terminator, "\r\n") - eq_(comm._terminator, "\r\n") + assert comm.terminator == "\r\n" + assert comm._terminator == "\r\n" def test_loopbackcomm_timeout(): comm = LoopbackCommunicator() - eq_(comm.timeout, 0) + assert comm.timeout == 0 comm.timeout = 10 - eq_(comm.timeout, 0) # setting should be ignored + assert comm.timeout == 0 # setting should be ignored def test_loopbackcomm_close(): @@ -79,8 +77,8 @@ def test_loopbackcomm_read_raw(): mock_stdin.read.side_effect = [b"a", b"b", b"c", b"\n"] comm = LoopbackCommunicator(stdin=mock_stdin) - eq_(comm.read_raw(), b"abc") - mock_stdin.read.assert_has_calls([mock.call(1)]*4) + assert comm.read_raw() == b"abc" + mock_stdin.read.assert_has_calls([mock.call(1)] * 4) assert mock_stdin.read.call_count == 4 mock_stdin.read = mock.MagicMock() @@ -94,11 +92,30 @@ def test_loopbackcomm_read_raw_2char_terminator(): comm = LoopbackCommunicator(stdin=mock_stdin) comm._terminator = "\r\n" - eq_(comm.read_raw(), b"abc") - mock_stdin.read.assert_has_calls([mock.call(1)]*5) + assert comm.read_raw() == b"abc" + mock_stdin.read.assert_has_calls([mock.call(1)] * 5) assert mock_stdin.read.call_count == 5 +def test_loopbackcomm_read_raw_terminator_is_empty_string(): + mock_stdin = mock.MagicMock() + mock_stdin.read.side_effect = [b"abc"] + comm = LoopbackCommunicator(stdin=mock_stdin) + comm._terminator = "" + + assert comm.read_raw() == b"abc" + mock_stdin.read.assert_has_calls([mock.call(-1)]) + assert mock_stdin.read.call_count == 1 + + +def test_loopbackcomm_read_raw_size_invalid(): + with pytest.raises(ValueError): + mock_stdin = mock.MagicMock() + mock_stdin.read.side_effect = [b"abc"] + comm = LoopbackCommunicator(stdin=mock_stdin) + comm.read_raw(size=-2) + + def test_loopbackcomm_write_raw(): mock_stdout = mock.MagicMock() comm = LoopbackCommunicator(stdout=mock_stdout) @@ -124,7 +141,7 @@ def test_loopbackcomm_query(): comm.read = mock.MagicMock(return_value="answer") comm.sendcmd = mock.MagicMock() - eq_(comm._query("mock"), "answer") + assert comm._query("mock") == "answer" comm.sendcmd.assert_called_with("mock") comm.read.assert_called_with(-1) @@ -132,16 +149,16 @@ def test_loopbackcomm_query(): comm.read.assert_called_with(10) -@raises(NotImplementedError) def test_loopbackcomm_seek(): - comm = LoopbackCommunicator() - comm.seek(1) + with pytest.raises(NotImplementedError): + comm = LoopbackCommunicator() + comm.seek(1) -@raises(NotImplementedError) def test_loopbackcomm_tell(): - comm = LoopbackCommunicator() - comm.tell() + with pytest.raises(NotImplementedError): + comm = LoopbackCommunicator() + comm.tell() def test_loopbackcomm_flush_input(): diff --git a/instruments/tests/test_comm/test_serial.py b/instruments/tests/test_comm/test_serial.py index d12c2f150..ab7335b72 100644 --- a/instruments/tests/test_comm/test_serial.py +++ b/instruments/tests/test_comm/test_serial.py @@ -1,20 +1,18 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Unit tests for the serial communication layer """ # IMPORTS #################################################################### -from __future__ import absolute_import -from nose.tools import raises, eq_ -import mock +import pytest import serial -import quantities as pq +from instruments.units import ureg as u from instruments.abstract_instruments.comm import SerialCommunicator from instruments.tests import unit_eq +from .. import mock # TEST CASES ################################################################# @@ -26,9 +24,9 @@ def test_serialcomm_init(): assert isinstance(comm._conn, serial.Serial) is True -@raises(TypeError) def test_serialcomm_init_wrong_filelike(): - _ = SerialCommunicator("derp") + with pytest.raises(TypeError): + _ = SerialCommunicator("derp") def test_serialcomm_address(): @@ -40,7 +38,7 @@ def test_serialcomm_address(): type(comm._conn).port = port_name # Check that our address function is working - eq_(comm.address, "/dev/address") + assert comm.address == "/dev/address" port_name.assert_called_with() @@ -48,14 +46,14 @@ def test_serialcomm_terminator(): comm = SerialCommunicator(serial.Serial()) # Default terminator should be \n - eq_(comm.terminator, "\n") + assert comm.terminator == "\n" comm.terminator = "*" - eq_(comm.terminator, "*") + assert comm.terminator == "*" comm.terminator = "\r\n" - eq_(comm.terminator, "\r\n") - eq_(comm._terminator, "\r\n") + assert comm.terminator == "\r\n" + assert comm._terminator == "\r\n" def test_serialcomm_timeout(): @@ -65,13 +63,13 @@ def test_serialcomm_timeout(): timeout = mock.PropertyMock(return_value=30) type(comm._conn).timeout = timeout - unit_eq(comm.timeout, 30 * pq.second) + unit_eq(comm.timeout, 30 * u.second) timeout.assert_called_with() comm.timeout = 10 timeout.assert_called_with(10) - comm.timeout = 1000 * pq.millisecond + comm.timeout = 1000 * u.millisecond timeout.assert_called_with(1) @@ -89,8 +87,8 @@ def test_serialcomm_read_raw(): comm._conn = mock.MagicMock() comm._conn.read = mock.MagicMock(side_effect=[b"a", b"b", b"c", b"\n"]) - eq_(comm.read_raw(), b"abc") - comm._conn.read.assert_has_calls([mock.call(1)]*4) + assert comm.read_raw() == b"abc" + comm._conn.read.assert_has_calls([mock.call(1)] * 4) assert comm._conn.read.call_count == 4 comm._conn.read = mock.MagicMock() @@ -104,18 +102,18 @@ def test_loopbackcomm_read_raw_2char_terminator(): comm._conn.read = mock.MagicMock(side_effect=[b"a", b"b", b"c", b"\r", b"\n"]) comm._terminator = "\r\n" - eq_(comm.read_raw(), b"abc") + assert comm.read_raw() == b"abc" comm._conn.read.assert_has_calls([mock.call(1)] * 5) assert comm._conn.read.call_count == 5 -@raises(IOError) def test_serialcomm_read_raw_timeout(): - comm = SerialCommunicator(serial.Serial()) - comm._conn = mock.MagicMock() - comm._conn.read = mock.MagicMock(side_effect=[b"a", b"b", b""]) + with pytest.raises(IOError): + comm = SerialCommunicator(serial.Serial()) + comm._conn = mock.MagicMock() + comm._conn.read = mock.MagicMock(side_effect=[b"a", b"b", b""]) - _ = comm.read_raw(-1) + _ = comm.read_raw(-1) def test_serialcomm_write_raw(): @@ -140,7 +138,7 @@ def test_serialcomm_query(): comm.read = mock.MagicMock(return_value="answer") comm.sendcmd = mock.MagicMock() - eq_(comm._query("mock"), "answer") + assert comm._query("mock") == "answer" comm.sendcmd.assert_called_with("mock") comm.read.assert_called_with(-1) @@ -148,16 +146,16 @@ def test_serialcomm_query(): comm.read.assert_called_with(10) -@raises(NotImplementedError) def test_serialcomm_seek(): - comm = SerialCommunicator(serial.Serial()) - comm.seek(1) + with pytest.raises(NotImplementedError): + comm = SerialCommunicator(serial.Serial()) + comm.seek(1) -@raises(NotImplementedError) def test_serialcomm_tell(): - comm = SerialCommunicator(serial.Serial()) - comm.tell() + with pytest.raises(NotImplementedError): + comm = SerialCommunicator(serial.Serial()) + comm.tell() def test_serialcomm_flush_input(): diff --git a/instruments/tests/test_comm/test_socket.py b/instruments/tests/test_comm/test_socket.py index a08c39660..ba03da89a 100644 --- a/instruments/tests/test_comm/test_socket.py +++ b/instruments/tests/test_comm/test_socket.py @@ -1,21 +1,19 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Unit tests for the socket communication layer """ # IMPORTS #################################################################### -from __future__ import absolute_import import socket -from nose.tools import raises, eq_ -import mock -import quantities as pq +import pytest +from instruments.units import ureg as u from instruments.abstract_instruments.comm import SocketCommunicator from instruments.tests import unit_eq +from .. import mock # TEST CASES ################################################################# @@ -29,9 +27,9 @@ def test_socketcomm_init(): assert comm._conn == socket_object -@raises(TypeError) def test_socketcomm_init_wrong_filelike(): - _ = SocketCommunicator("derp") + with pytest.raises(TypeError): + _ = SocketCommunicator("derp") def test_socketcomm_address(): @@ -39,33 +37,33 @@ def test_socketcomm_address(): comm._conn = mock.MagicMock() comm._conn.getpeername.return_value = "127.0.0.1", 1234 - eq_(comm.address, ("127.0.0.1", 1234)) + assert comm.address == ("127.0.0.1", 1234) comm._conn.getpeername.assert_called_with() -@raises(NotImplementedError) def test_socketcomm_address_setting(): - comm = SocketCommunicator(socket.socket()) - comm.address = "foobar" + with pytest.raises(NotImplementedError): + comm = SocketCommunicator(socket.socket()) + comm.address = "foobar" def test_socketcomm_terminator(): comm = SocketCommunicator(socket.socket()) # Default terminator should be \n - eq_(comm.terminator, "\n") + assert comm.terminator == "\n" comm.terminator = b"*" - eq_(comm.terminator, "*") - eq_(comm._terminator, "*") + assert comm.terminator == "*" + assert comm._terminator == "*" - comm.terminator = u"\r" # pylint: disable=redefined-variable-type - eq_(comm.terminator, u"\r") - eq_(comm._terminator, u"\r") + comm.terminator = "\r" + assert comm.terminator == "\r" + assert comm._terminator == "\r" comm.terminator = "\r\n" - eq_(comm.terminator, "\r\n") - eq_(comm._terminator, "\r\n") + assert comm.terminator == "\r\n" + assert comm._terminator == "\r\n" def test_socketcomm_timeout(): @@ -73,13 +71,13 @@ def test_socketcomm_timeout(): comm._conn = mock.MagicMock() comm._conn.gettimeout.return_value = 1.234 - unit_eq(comm.timeout, 1.234 * pq.second) + unit_eq(comm.timeout, 1.234 * u.second) comm._conn.gettimeout.assert_called_with() comm.timeout = 10 comm._conn.settimeout.assert_called_with(10) - comm.timeout = 1000 * pq.millisecond + comm.timeout = 1000 * u.millisecond comm._conn.settimeout.assert_called_with(1) @@ -88,7 +86,7 @@ def test_socketcomm_close(): comm._conn = mock.MagicMock() comm.close() - comm._conn.shutdown.assert_called_with() + comm._conn.shutdown.assert_called_with(socket.SHUT_RDWR) comm._conn.close.assert_called_with() @@ -97,8 +95,8 @@ def test_socketcomm_read_raw(): comm._conn = mock.MagicMock() comm._conn.recv = mock.MagicMock(side_effect=[b"a", b"b", b"c", b"\n"]) - eq_(comm.read_raw(), b"abc") - comm._conn.recv.assert_has_calls([mock.call(1)]*4) + assert comm.read_raw() == b"abc" + comm._conn.recv.assert_has_calls([mock.call(1)] * 4) assert comm._conn.recv.call_count == 4 comm._conn.recv = mock.MagicMock() @@ -112,18 +110,18 @@ def test_loopbackcomm_read_raw_2char_terminator(): comm._conn.recv = mock.MagicMock(side_effect=[b"a", b"b", b"c", b"\r", b"\n"]) comm._terminator = "\r\n" - eq_(comm.read_raw(), b"abc") + assert comm.read_raw() == b"abc" comm._conn.recv.assert_has_calls([mock.call(1)] * 5) assert comm._conn.recv.call_count == 5 -@raises(IOError) def test_serialcomm_read_raw_timeout(): - comm = SocketCommunicator(socket.socket()) - comm._conn = mock.MagicMock() - comm._conn.recv = mock.MagicMock(side_effect=[b"a", b"b", b""]) + with pytest.raises(IOError): + comm = SocketCommunicator(socket.socket()) + comm._conn = mock.MagicMock() + comm._conn.recv = mock.MagicMock(side_effect=[b"a", b"b", b""]) - _ = comm.read_raw(-1) + _ = comm.read_raw(-1) def test_socketcomm_write_raw(): @@ -148,7 +146,7 @@ def test_socketcomm_query(): comm.read = mock.MagicMock(return_value="answer") comm.sendcmd = mock.MagicMock() - eq_(comm._query("mock"), "answer") + assert comm._query("mock") == "answer" comm.sendcmd.assert_called_with("mock") comm.read.assert_called_with(-1) @@ -156,16 +154,16 @@ def test_socketcomm_query(): comm.read.assert_called_with(10) -@raises(NotImplementedError) def test_socketcomm_seek(): - comm = SocketCommunicator(socket.socket()) - comm.seek(1) + with pytest.raises(NotImplementedError): + comm = SocketCommunicator(socket.socket()) + comm.seek(1) -@raises(NotImplementedError) def test_socketcomm_tell(): - comm = SocketCommunicator(socket.socket()) - comm.tell() + with pytest.raises(NotImplementedError): + comm = SocketCommunicator(socket.socket()) + comm.tell() def test_socketcomm_flush_input(): diff --git a/instruments/tests/test_comm/test_usb_communicator.py b/instruments/tests/test_comm/test_usb_communicator.py new file mode 100644 index 000000000..1c962f4f9 --- /dev/null +++ b/instruments/tests/test_comm/test_usb_communicator.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python +""" +Unit tests for the USB communicator. +""" + +# IMPORTS #################################################################### + +from hypothesis import given, strategies as st +import pytest + +import usb.core +import usb.util + +from instruments.abstract_instruments.comm import USBCommunicator +from instruments.units import ureg as u +from .. import mock + +# TEST CASES ################################################################# + +# pylint: disable=protected-access,unused-argument, redefined-outer-name + +patch_util = "instruments.abstract_instruments.comm.usb_communicator.usb.util" + + +@pytest.fixture() +def dev(): + """Return a usb core device for initialization.""" + dev = mock.MagicMock() + dev.__class__ = usb.core.Device + return dev + + +@pytest.fixture() +@mock.patch(patch_util) +def inst(patch_util, dev): + """Return a USB Communicator instrument.""" + return USBCommunicator(dev) + + +@mock.patch(patch_util) +def test_init(usb_util, dev): + """Initialize usb communicator.""" + # mock some behavior of the device required for initializing + dev.find.return_value.__class__ = usb.core.Device # dev + # shortcuts for asserting calls + cfg = dev.get_active_configuration() + interface_number = cfg[(0, 0)].bInterfaceNumber + _ = dev.control.get_interface(dev, cfg[(0, 0)].bInterfaceNumber) + + inst = USBCommunicator(dev) + + # # assert calls according to manual + dev.set_configuration.assert_called() # check default configuration + dev.get_active_configuration.assert_called() # get active configuration + dev.control.get_interface.assert_called_with(dev, interface_number) + usb_util.find_descriptor.assert_has_calls(cfg) + + assert isinstance(inst, USBCommunicator) + + assert inst._dev == dev + + +def test_init_wrong_type(): + """Raise TypeError if initialized with wrong device.""" + with pytest.raises(TypeError) as err: + _ = USBCommunicator(42) + err_msg = err.value.args[0] + assert err_msg == "USBCommunicator must wrap a usb.core.Device object." + + +def test_init_no_endpoints(dev): + """Initialize usb communicator without endpoints.""" + # mock some behavior of the device required for initializing + dev.find.return_value.__class__ = usb.core.Device # dev + + with pytest.raises(IOError) as err: + _ = USBCommunicator(dev) + err_msg = err.value.args[0] + assert err_msg == "USB endpoint not found." + + +def test_address(inst): + """Address of device can not be read, nor written.""" + with pytest.raises(NotImplementedError): + _ = inst.address + + with pytest.raises(ValueError) as err: + inst.address = 42 + + msg = err.value.args[0] + assert msg == "Unable to change USB target address." + + +def test_terminator(inst): + """Get / set terminator of instrument.""" + assert inst.terminator == "\n" + inst.terminator = "\r\n" + assert inst.terminator == "\r\n" + + +def test_terminator_wrong_type(inst): + """Raise TypeError when setting bad terminator.""" + with pytest.raises(TypeError) as err: + inst.terminator = 42 + msg = err.value.args[0] + assert ( + msg == "Terminator for USBCommunicator must be specified as a " + "character string." + ) + + +@given(val=st.integers(min_value=1)) +def test_timeout_get(val, inst): + """Get a timeout from device (ms) and turn into s.""" + # mock timeout value of device + inst._dev.default_timeout = val + + ret_val = inst.timeout + assert ret_val == u.Quantity(val, u.ms).to(u.s) + + +def test_timeout_set_unitless(inst): + """Set a timeout value from device unitless (s).""" + val = 1000 + inst.timeout = val + set_val = inst._dev.default_timeout + exp_val = 1000 * val + assert set_val == exp_val + + +def test_timeout_set_minutes(inst): + """Set a timeout value from device in minutes.""" + val = 10 + val_to_set = u.Quantity(val, u.min) + inst.timeout = val_to_set + set_val = inst._dev.default_timeout + exp_val = 1000 * 60 * val + assert set_val == exp_val + + +@mock.patch(patch_util) +def test_close(usb_util, inst): + """Close the connection, release instrument.""" + inst.close() + inst._dev.reset.assert_called() + usb_util.dispose_resources.assert_called_with(inst._dev) + + +def test_read_raw(inst): + """Read raw information from instrument.""" + msg = b"message\n" + msg_exp = b"message" + + inst._ep_in.read.return_value = msg + + assert inst.read_raw() == msg_exp + + +def test_read_raw_size(inst): + """If size is -1, read 1000 bytes.""" + msg = b"message\n" + inst._ep_in.read.return_value = msg + + # set max package size + max_size = 256 + inst._max_packet_size = max_size + + _ = inst.read_raw(size=-1) + inst._ep_in.read.assert_called_with(max_size) + + +def test_read_raw_termination_char_not_found(inst): + """Raise IOError if termination character not found.""" + msg = b"message" + inst._ep_in.read.return_value = msg + default_read_size = 1000 + + inst._max_packet_size = default_read_size + + with pytest.raises(IOError) as err: + _ = inst.read_raw() + err_msg = err.value.args[0] + assert ( + err_msg == f"Did not find the terminator in the returned " + f"string. Total size of {default_read_size} might " + f"not be enough." + ) + + +def test_write_raw(inst): + """Write a message to the instrument.""" + msg = b"message\n" + inst.write_raw(msg) + inst._ep_out.write.assert_called_with(msg) + + +def test_seek(inst): + """Raise NotImplementedError if `seek` is called.""" + with pytest.raises(NotImplementedError): + inst.seek(42) + + +def test_tell(inst): + """Raise NotImplementedError if `tell` is called.""" + with pytest.raises(NotImplementedError): + inst.tell() + + +def test_flush_input(inst): + """Flush the input out by trying to read until no more available.""" + inst._ep_in.read.side_effect = [b"message\n", usb.core.USBTimeoutError] + inst.flush_input() + inst._ep_in.read.assert_called() + + +def test_sendcmd(inst): + """Send a command.""" + msg = "msg" + msg_to_send = f"msg{inst._terminator}" + + inst.write = mock.MagicMock() + + inst._sendcmd(msg) + inst.write.assert_called_with(msg_to_send) + + +def test_query(inst): + """Query the instrument.""" + msg = "msg" + size = 1000 + + inst.sendcmd = mock.MagicMock() + inst.read = mock.MagicMock() + + inst._query(msg, size=size) + inst.sendcmd.assert_called_with(msg) + inst.read.assert_called_with(size) diff --git a/instruments/tests/test_comm/test_usbtmc.py b/instruments/tests/test_comm/test_usbtmc.py index 826ccecec..5a5533b3a 100644 --- a/instruments/tests/test_comm/test_usbtmc.py +++ b/instruments/tests/test_comm/test_usbtmc.py @@ -1,21 +1,17 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Unit tests for the USBTMC communication layer """ # IMPORTS #################################################################### -from __future__ import absolute_import -from nose.tools import raises, eq_ -import mock - -import quantities as pq -from numpy import array +import pytest from instruments.abstract_instruments.comm import USBTMCCommunicator from instruments.tests import unit_eq +from instruments.units import ureg as u +from .. import mock # TEST CASES ################################################################# @@ -30,20 +26,20 @@ def test_usbtmccomm_init(mock_usbtmc): mock_usbtmc.Instrument.assert_called_with("foobar", var1=123) -@raises(ImportError) @mock.patch(patch_path, new=None) def test_usbtmccomm_init_missing_module(): - _ = USBTMCCommunicator() + with pytest.raises(ImportError): + _ = USBTMCCommunicator() @mock.patch(patch_path) def test_usbtmccomm_terminator_getter(mock_usbtmc): comm = USBTMCCommunicator() - term_char = mock.PropertyMock(return_value="\n") + term_char = mock.PropertyMock(return_value=10) type(comm._filelike).term_char = term_char - eq_(comm.terminator, "\n") + assert comm.terminator == "\n" term_char.assert_called_with() @@ -55,11 +51,11 @@ def test_usbtmccomm_terminator_setter(mock_usbtmc): type(comm._filelike).term_char = term_char comm.terminator = "*" - eq_(comm._terminator, "*") + assert comm._terminator == "*" term_char.assert_called_with(42) - comm.terminator = b"*" # pylint: disable=redefined-variable-type - eq_(comm._terminator, "*") + comm.terminator = b"*" + assert comm._terminator == "*" term_char.assert_called_with(42) @@ -70,14 +66,14 @@ def test_usbtmccomm_timeout(mock_usbtmc): timeout = mock.PropertyMock(return_value=1) type(comm._filelike).timeout = timeout - unit_eq(comm.timeout, 1 * pq.second) + unit_eq(comm.timeout, 1 * u.second) timeout.assert_called_with() comm.timeout = 10 - timeout.assert_called_with(array(10000.0)) + timeout.assert_called_with(10.0) - comm.timeout = 1000 * pq.millisecond - timeout.assert_called_with(array(1000.0)) + comm.timeout = 1000 * u.millisecond + timeout.assert_called_with(1.0) @mock.patch(patch_path) @@ -93,7 +89,7 @@ def test_usbtmccomm_read_raw(mock_usbtmc): comm = USBTMCCommunicator() comm._filelike.read_raw = mock.MagicMock(return_value=b"abc") - eq_(comm.read_raw(), b"abc") + assert comm.read_raw() == b"abc" comm._filelike.read_raw.assert_called_with(num=-1) assert comm._filelike.read_raw.call_count == 1 @@ -124,25 +120,25 @@ def test_usbtmccomm_query(mock_usbtmc): comm = USBTMCCommunicator() comm._filelike.ask = mock.MagicMock(return_value="answer") - eq_(comm._query("mock"), "answer") + assert comm._query("mock") == "answer" comm._filelike.ask.assert_called_with("mock", num=-1, encoding="utf-8") comm._query("mock", size=10) comm._filelike.ask.assert_called_with("mock", num=10, encoding="utf-8") -@raises(NotImplementedError) @mock.patch(patch_path) def test_usbtmccomm_seek(mock_usbtmc): - comm = USBTMCCommunicator() - comm.seek(1) + with pytest.raises(NotImplementedError): + comm = USBTMCCommunicator() + comm.seek(1) -@raises(NotImplementedError) @mock.patch(patch_path) def test_usbtmccomm_tell(mock_usbtmc): - comm = USBTMCCommunicator() - comm.tell() + with pytest.raises(NotImplementedError): + comm = USBTMCCommunicator() + comm.tell() @mock.patch(patch_path) diff --git a/instruments/tests/test_comm/test_visa_communicator.py b/instruments/tests/test_comm/test_visa_communicator.py new file mode 100644 index 000000000..94a13faab --- /dev/null +++ b/instruments/tests/test_comm/test_visa_communicator.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python +""" +Unit tests for the VISA communication layer +""" + +# IMPORTS #################################################################### + + +import pytest +import pyvisa + +from instruments.units import ureg as u +from instruments.abstract_instruments.comm import VisaCommunicator + + +# TEST CASES ################################################################# + +# pylint: disable=protected-access,redefined-outer-name + +# create a visa instrument +@pytest.fixture() +def visa_inst(): + """Create a default visa-sim instrument and return it.""" + inst = pyvisa.ResourceManager("@sim").open_resource("ASRL1::INSTR") + return inst + + +def test_visacomm_init(visa_inst): + """Initialize visa communicator.""" + comm = VisaCommunicator(visa_inst) + assert comm._conn == visa_inst + assert comm._terminator == "\n" + assert comm._buf == bytearray() + + +def test_visacomm_init_wrong_type(): + """Raise TypeError if not a VISA instrument.""" + with pytest.raises(TypeError) as err: + VisaCommunicator(42) + err_msg = err.value.args[0] + assert err_msg == "VisaCommunicator must wrap a VISA Instrument." + + +def test_visacomm_address(visa_inst): + """Get / Set instrument address.""" + comm = VisaCommunicator(visa_inst) + assert comm.address == visa_inst.resource_name + with pytest.raises(NotImplementedError) as err: + comm.address = "new address" + err_msg = err.value.args[0] + assert err_msg == ("Changing addresses of a VISA Instrument is not supported.") + + +def test_visacomm_terminator(visa_inst): + """Get / Set terminator.""" + comm = VisaCommunicator(visa_inst) + comm.terminator = "\r" + assert comm.terminator == "\r" + + +def test_visacomm_terminator_not_string(visa_inst): + """Raise TypeError if terminator is set with non-string character.""" + comm = VisaCommunicator(visa_inst) + with pytest.raises(TypeError) as err: + comm.terminator = 42 + err_msg = err.value.args[0] + assert err_msg == ( + "Terminator for VisaCommunicator must be specified as a single " + "character string." + ) + + +def test_visacomm_terminator_not_single_char(visa_inst): + """Raise ValueError if terminator longer than one character.""" + comm = VisaCommunicator(visa_inst) + with pytest.raises(ValueError) as err: + comm.terminator = "\r\n" + err_msg = err.value.args[0] + assert err_msg == ("Terminator for VisaCommunicator must only be 1 character long.") + + +def test_visacomm_timeout(visa_inst): + """Set / Get timeout of VISA communicator.""" + comm = VisaCommunicator(visa_inst) + comm.timeout = 3 + assert comm.timeout == u.Quantity(3, u.s) + comm.timeout = u.Quantity(40000, u.ms) + assert comm.timeout == u.Quantity(40, u.s) + + +def test_visacomm_close(visa_inst, mocker): + """Raise an IOError if comms cannot be closed.""" + io_error_mock = mocker.Mock() + io_error_mock.side_effect = IOError + mock_close = mocker.patch.object(visa_inst, "close", io_error_mock) + comm = VisaCommunicator(visa_inst) + comm.close() + mock_close.assert_called() # but error will just pass! + + +def test_visacomm_read_raw(visa_inst, mocker): + """Read raw data from instrument without size specification.""" + comm = VisaCommunicator(visa_inst) + mock_read_raw = mocker.patch.object(visa_inst, "read_raw", return_value=b"asdf") + comm.read_raw() + mock_read_raw.assert_called() + assert comm._buf == bytearray() + + +def test_visacomm_read_raw_size(visa_inst, mocker): + """Read raw data from instrument with size specification.""" + comm = VisaCommunicator(visa_inst) + size = 3 + mock_read_bytes = mocker.patch.object(visa_inst, "read_bytes", return_value=b"123") + ret_val = comm.read_raw(size=size) + assert ret_val == b"123" + mock_read_bytes.assert_called() + assert comm._buf == bytearray() + + +def test_visacomm_read_raw_wrong_size(visa_inst): + """Raise ValueError if size is invalid.""" + comm = VisaCommunicator(visa_inst) + with pytest.raises(ValueError) as err: + comm.read_raw(size=-3) + err_msg = err.value.args[0] + assert err_msg == ( + "Must read a positive value of characters, or -1 for all characters." + ) + + +def test_visacomm_write_raw(visa_inst, mocker): + """Write raw message to instrument.""" + mock_write = mocker.patch.object(visa_inst, "write_raw") + comm = VisaCommunicator(visa_inst) + msg = b"12345" + comm.write_raw(msg) + mock_write.assert_called_with(msg) + + +def test_visacomm_seek_not_implemented(visa_inst): + """Raise NotImplementedError when calling seek.""" + comm = VisaCommunicator(visa_inst) + with pytest.raises(NotImplementedError): + comm.seek(42) + + +def test_visacomm_tell_not_implemented(visa_inst): + """Raise NotImplementedError when calling tell.""" + comm = VisaCommunicator(visa_inst) + with pytest.raises(NotImplementedError): + comm.tell() + + +def test_visacomm_sendcmd(visa_inst, mocker): + """Write to device.""" + mock_write = mocker.patch.object(VisaCommunicator, "write") + comm = VisaCommunicator(visa_inst) + msg = "asdf" + comm._sendcmd(msg) + mock_write.assert_called_with(msg + comm.terminator) + + +def test_visacomm_query(visa_inst, mocker): + """Query device.""" + mock_query = mocker.patch.object(visa_inst, "query") + comm = VisaCommunicator(visa_inst) + msg = "asdf" + comm._query(msg) + mock_query.assert_called_with(msg + comm.terminator) diff --git a/instruments/tests/test_comm/test_vxi11.py b/instruments/tests/test_comm/test_vxi11.py index 60dd4a899..c73bb37ab 100644 --- a/instruments/tests/test_comm/test_vxi11.py +++ b/instruments/tests/test_comm/test_vxi11.py @@ -1,17 +1,15 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Unit tests for the VXI11 communication layer """ # IMPORTS #################################################################### -from __future__ import absolute_import -from nose.tools import raises, eq_ -import mock +import pytest from instruments.abstract_instruments.comm import VXI11Communicator +from .. import mock # TEST CASES ################################################################# @@ -26,10 +24,10 @@ def test_vxi11comm_init(mock_vxi11): mock_vxi11.Instrument.assert_called_with("host") -@raises(ImportError) @mock.patch(import_base, new=None) def test_vxi11comm_init_no_vxi11(): - _ = VXI11Communicator("host") + with pytest.raises(ImportError): + _ = VXI11Communicator("host") @mock.patch(import_base) @@ -45,7 +43,7 @@ def test_vxi11comm_address(mock_vxi11): type(comm._inst).name = name # Check that our address function is working - eq_(comm.address, ["host", "name"]) + assert comm.address == ["host", "name"] host.assert_called_with() name.assert_called_with() @@ -57,7 +55,7 @@ def test_vxi11comm_terminator(mock_vxi11): term_char = mock.PropertyMock(return_value="\n") type(comm._inst).term_char = term_char - eq_(comm.terminator, "\n") + assert comm.terminator == "\n" term_char.assert_called_with() comm.terminator = "*" @@ -71,7 +69,7 @@ def test_vxi11comm_timeout(mock_vxi11): timeout = mock.PropertyMock(return_value=30) type(comm._inst).timeout = timeout - eq_(comm.timeout, 30) + assert comm.timeout == 30 timeout.assert_called_with() comm.timeout = 10 @@ -100,7 +98,7 @@ def test_vxi11comm_read(mock_vxi11): comm = VXI11Communicator() comm._inst.read_raw.return_value = b"mock" - eq_(comm.read_raw(), b"mock") + assert comm.read_raw() == b"mock" comm._inst.read_raw.assert_called_with(num=-1) comm.read(10) @@ -128,29 +126,29 @@ def test_vxi11comm_query(mock_vxi11): comm = VXI11Communicator() comm._inst.ask.return_value = "answer" - eq_(comm._query("mock"), "answer") + assert comm._query("mock") == "answer" comm._inst.ask.assert_called_with("mock", num=-1) comm._query("mock", size=10) comm._inst.ask.assert_called_with("mock", num=10) -@raises(NotImplementedError) @mock.patch(import_base) def test_vxi11comm_seek(mock_vxi11): - comm = VXI11Communicator() - comm.seek(1) + with pytest.raises(NotImplementedError): + comm = VXI11Communicator() + comm.seek(1) -@raises(NotImplementedError) @mock.patch(import_base) def test_vxi11comm_tell(mock_vxi11): - comm = VXI11Communicator() - comm.tell() + with pytest.raises(NotImplementedError): + comm = VXI11Communicator() + comm.tell() -@raises(NotImplementedError) @mock.patch(import_base) def test_vxi11comm_flush(mock_vxi11): - comm = VXI11Communicator() - comm.flush_input() + with pytest.raises(NotImplementedError): + comm = VXI11Communicator() + comm.flush_input() diff --git a/instruments/tests/test_config.py b/instruments/tests/test_config.py new file mode 100644 index 000000000..a4f0c7be0 --- /dev/null +++ b/instruments/tests/test_config.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python +""" +Module containing tests for util_fns.py +""" + +# IMPORTS #################################################################### + + +from io import StringIO + +from instruments.units import ureg as u + +from instruments import Instrument +from instruments.config import load_instruments, yaml + +# TEST CASES ################################################################# + +# pylint: disable=protected-access,missing-docstring + + +def test_load_test_instrument(): + config_data = StringIO( + """ +test: + class: !!python/name:instruments.Instrument + uri: test:// +""" + ) + insts = load_instruments(config_data) + assert isinstance(insts["test"], Instrument) + + +def test_load_test_instrument_subtree(): + config_data = StringIO( + """ +instruments: + test: + class: !!python/name:instruments.Instrument + uri: test:// +""" + ) + insts = load_instruments(config_data, conf_path="/instruments") + assert isinstance(insts["test"], Instrument) + + +def test_yaml_quantity_tag(): + yaml_data = StringIO( + """ +a: + b: !Q 37 tesla + c: !Q 41.2 inches + d: !Q 98 +""" + ) + data = yaml.load(yaml_data, Loader=yaml.Loader) + assert data["a"]["b"] == u.Quantity(37, "tesla") + assert data["a"]["c"] == u.Quantity(41.2, "inches") + assert data["a"]["d"] == 98 + + +def test_load_test_instrument_setattr(): + config_data = StringIO( + """ +test: + class: !!python/name:instruments.Instrument + uri: test:// + attrs: + foo: !Q 111 GHz +""" + ) + insts = load_instruments(config_data) + assert insts["test"].foo == u.Quantity(111, "GHz") diff --git a/instruments/tests/test_fluke/__init__.py b/instruments/tests/test_fluke/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/instruments/tests/test_fluke/test_fluke3000.py b/instruments/tests/test_fluke/test_fluke3000.py new file mode 100644 index 000000000..bf656ebe2 --- /dev/null +++ b/instruments/tests/test_fluke/test_fluke3000.py @@ -0,0 +1,382 @@ +#!/usr/bin/env python +""" +Module containing tests for the Fluke 3000 FC multimeter +""" + +# IMPORTS #################################################################### + +import pytest + +from instruments.units import ureg as u + +import instruments as ik +from instruments.tests import expected_protocol + +# TESTS ###################################################################### + + +# pylint: disable=protected-access + + +# Empty initialization sequence (scan function) that does not uncover +# any available Fluke 3000 FC device. +none_sequence = [ + "rfebd 01 0", + "rfebd 02 0", + "rfebd 03 0", + "rfebd 04 0", + "rfebd 05 0", + "rfebd 06 0", +] +none_response = ["CR:Ack=2", "CR:Ack=2", "CR:Ack=2", "CR:Ack=2", "CR:Ack=2", "CR:Ack=2"] + +# Default initialization sequence (scan function) that binds a multimeter +# to port 1 and a temperature module to port 2. +init_sequence = [ + "rfebd 01 0", # 1 + "rfgus 01", # 2 + "rfebd 02 0", # 3 + "rfgus 02", # 4 + "rfebd 03 0", # 5 + "rfebd 04 0", # 6 + "rfebd 05 0", # 7 + "rfebd 06 0", # 8 +] +init_response = [ + "CR:Ack=0:RFEBD", # 1.1 + "ME:R:S#=01:DCC=012:PH=64", # 1.2 + "CR:Ack=0:RFGUS", # 2.1 + "ME:R:S#=01:DCC=004:PH=46333030304643", # 2.2 + "CR:Ack=0:RFEBD", # 3.1 + "ME:R:S#=01:DCC=012:PH=64", # 3.2 + "CR:Ack=0:RFGUS", # 4.1 + "ME:R:S#=02:DCC=004:PH=54333030304643", # 4.2 + "CR:Ack=2", # 5 + "CR:Ack=2", # 6 + "CR:Ack=2", # 7 + "CR:Ack=2", # 8 +] + + +# Default initialization sequence (scan function) that binds a multimeter +# to port 1. Adopted from `init_sequence` and `init_response`, thus +# counting does not contain 4. +init_sequence_mm_only = [ + "rfebd 01 0", # 1 + "rfgus 01", # 2 + "rfebd 02 0", # 3 + "rfebd 03 0", # 5 + "rfebd 04 0", # 6 + "rfebd 05 0", # 7 + "rfebd 06 0", # 8 +] +init_response_mm_only = [ + "CR:Ack=0:RFEBD", # 1.1 + "ME:R:S#=01:DCC=012:PH=64", # 1.2 + "CR:Ack=0:RFGUS", # 2.1 + "ME:R:S#=01:DCC=004:PH=46333030304643", # 2.2 + "CR:Ack=2", # 3 + "CR:Ack=2", # 5 + "CR:Ack=2", # 6 + "CR:Ack=2", # 7 + "CR:Ack=2", # 8 +] + + +def test_mode(): + with expected_protocol( + ik.fluke.Fluke3000, + init_sequence + ["rfemd 01 1", "rfemd 01 2"], # 1 # 2 + init_response + + [ + "CR:Ack=0:RFEMD", # 1.1 + "ME:R:S#=01:DCC=010:PH=00000006020C0600", # 1.2 + "CR:Ack=0:RFEMD", # 2 + ], + "\r", + ) as inst: + assert inst.mode == inst.Mode.voltage_dc + + +def test_mode_key_error(): + """Raise KeyError if the Module is not available.""" + with expected_protocol( + ik.fluke.Fluke3000, init_sequence, init_response, "\r" + ) as inst: + # kill positions to trigger error + inst.positions = {} + with pytest.raises(KeyError) as err_info: + _ = inst.mode + err_msg = err_info.value.args[0] + assert err_msg == "No `Fluke3000` FC multimeter is bound" + + +def test_trigger_mode_attribute_error(): + """Raise AttributeError since trigger mode not supported.""" + with expected_protocol( + ik.fluke.Fluke3000, init_sequence, init_response, "\r" + ) as inst: + with pytest.raises(AttributeError) as err_info: + _ = inst.trigger_mode + err_msg = err_info.value.args[0] + assert err_msg == "The `Fluke3000` only supports single trigger when " "queried" + + +def test_relative_attribute_error(): + """Raise AttributeError since relative measurement mode not supported.""" + with expected_protocol( + ik.fluke.Fluke3000, init_sequence, init_response, "\r" + ) as inst: + with pytest.raises(AttributeError) as err_info: + _ = inst.relative + err_msg = err_info.value.args[0] + assert err_msg == "The `Fluke3000` FC does not support relative " "measurements" + + +def test_input_range_attribute_error(): + """ + Raise AttributeError since instrument is an auto ranging only + multimeter. + """ + with expected_protocol( + ik.fluke.Fluke3000, init_sequence, init_response, "\r" + ) as inst: + with pytest.raises(AttributeError) as err_info: + _ = inst.input_range + err_msg = err_info.value.args[0] + assert err_msg == "The `Fluke3000` FC is an autoranging only " "multimeter" + + +def test_connect(): + with expected_protocol( + ik.fluke.Fluke3000, + none_sequence + + [ + "ri", # 1 + "rfsm 1", # 2 + "rfdis", # 3 + ] + + init_sequence, + none_response + + [ + "CR:Ack=0:RI", # 1.1 + "SI:PON=Power On", # 1.2 + "RE:O", # 1.3 + "CR:Ack=0:RFSM:Radio On Master", # 2.1 + "RE:M", # 2.2 + "CR:Ack=0:RFDIS", # 3.1 + "ME:S", # 3.2 + "ME:D:010200000000", # 3.3 + ] + + init_response, + "\r", + ) as inst: + assert inst.positions[ik.fluke.Fluke3000.Module.m3000] == 1 + assert inst.positions[ik.fluke.Fluke3000.Module.t3000] == 2 + + +def test_connect_no_modules_available(): + """Raise ValueError if no modules are avilable.""" + with pytest.raises(ValueError) as err_info: + with expected_protocol( + ik.fluke.Fluke3000, none_sequence, none_response, "\r" + ) as inst: + _ = inst + err_msg = err_info.value.args[0] + assert err_msg == "No `Fluke3000` modules available" + + +def test_scan(): + with expected_protocol( + ik.fluke.Fluke3000, init_sequence, init_response, "\r" + ) as inst: + assert inst.positions[ik.fluke.Fluke3000.Module.m3000] == 1 + assert inst.positions[ik.fluke.Fluke3000.Module.t3000] == 2 + + +def test_scan_module_not_implemented(): + """Raise NotImplementedError if a module with wrong ID is found.""" + # modify response to contain unknown module + module_id = 42 + mod_response = list(init_response) + mod_response[3] = f"ME:R:S#=01:DCC=004:PH={module_id}" # new module id + with pytest.raises(NotImplementedError) as err_info: + with expected_protocol( + ik.fluke.Fluke3000, init_sequence, mod_response, "\r" + ) as inst: + _ = inst + err_msg = err_info.value.args[0] + assert err_msg == f"Module ID {module_id} not implemented" + + +def test_reset(): + with expected_protocol( + ik.fluke.Fluke3000, + init_sequence + ["ri", "rfsm 1"], # 1 # 2 + init_response + + [ + "CR:Ack=0:RI", # 1.1 + "SI:PON=Power On", # 1.2 + "RE:O", # 1.3 + "CR:Ack=0:RFSM:Radio On Master", # 2.1 + "RE:M", # 2.2 + ], + "\r", + ) as inst: + inst.reset() + + +def test_flush(mocker): + """Test flushing the reads, which raises an OSError here. + + Mocking `read()` to generate the error. + """ + with expected_protocol( + ik.fluke.Fluke3000, init_sequence, init_response, "\r" + ) as inst: + # mock read to raise OSError + os_error_mock = mocker.Mock() + os_error_mock.side_effect = OSError + read_mock = mocker.patch.object(inst, "read", os_error_mock) + # now flush + inst.flush() + read_mock.assert_called() + + +def test_measure(): + with expected_protocol( + ik.fluke.Fluke3000, + init_sequence + ["rfemd 01 1", "rfemd 01 2", "rfemd 02 0"], # 1 # 2 # 3 + init_response + + [ + "CR:Ack=0:RFEMD", # 1.1 + "ME:R:S#=01:DCC=010:PH=FD010006020C0600", # 1.2 + "CR:Ack=0:RFEMD", # 2 + "CR:Ack=0:RFEMD", # 3.1 + "ME:R:S#=02:DCC=010:PH=FD00C08207220000", # 3.2 + ], + "\r", + ) as inst: + assert inst.measure(inst.Mode.voltage_dc) == 0.509 * u.volt + assert inst.measure(inst.Mode.temperature) == u.Quantity(-25.3, u.degC) + + +def test_measure_invalid_mode(): + """Raise ValueError if measurement mode is not supported.""" + with expected_protocol( + ik.fluke.Fluke3000, init_sequence, init_response, "\r" + ) as inst: + wrong_mode = 42 + with pytest.raises(ValueError) as err_info: + inst.measure(wrong_mode) + err_msg = err_info.value.args[0] + assert err_msg == f"Mode {wrong_mode} is not supported" + + +def test_measure_no_module_with_mode(): + """ + Raise ValueError if not sensor that supports the requested mode is + connected. + """ + mode_not_available = ik.fluke.Fluke3000.Mode.temperature + with expected_protocol( + ik.fluke.Fluke3000, init_sequence_mm_only, init_response_mm_only, "\r" + ) as inst: + with pytest.raises(ValueError) as err_info: + inst.measure(mode=mode_not_available) + err_msg = err_info.value.args[0] + assert ( + err_msg == f"Device necessary to measure {mode_not_available} " + f"is not available" + ) + + +def test_measure_inconsistent_answer(mocker): + """Measurement test with inconsistent answer. + + The first time around in this measurement an inconsistent answer is + returend. This would usually call a `flush` routine, which reads + until no more terminators are found. Here, `flush` is mocked out + such that the `expected_protocol` can actually be used. + """ + mode_issue = 42 # expect 02, answer something different - unexpected + with expected_protocol( + ik.fluke.Fluke3000, + init_sequence + + [ + # bad query + "rfemd 01 1", # 1 + "rfemd 01 2", # 2 + "rfemd 01 2", # 2 + # try again + "rfemd 01 1", # 1 + "rfemd 01 2", # 2 + ], + init_response + + [ + # bad response + "CR:Ack=0:RFEMD", # 1.1 + f"ME:R:S#=01:DCC=010:PH=FD010006{mode_issue}0C0600", # 1.2 + "CR:Ack=0:RFEMD", # 2 + "CR:Ack=0:RFEMD", # 2 + # "", # something to flush + # try again + "CR:Ack=0:RFEMD", # 1.1 + "ME:R:S#=01:DCC=010:PH=FD010006020C0600", # 1.2 + "CR:Ack=0:RFEMD", # 2 + ], + "\r", + ) as inst: + # mock out flush + flush_mock = mocker.patch.object(inst, "flush", return_value=None) + assert inst.measure(inst.Mode.voltage_dc) == 0.509 * u.volt + # assert that flush was called once + flush_mock.assert_called_once() + + +def test_parse_ph_not_in_result(): + """Raise ValueError if 'PH' is not in `result`.""" + with expected_protocol( + ik.fluke.Fluke3000, init_sequence, init_response, "\r" + ) as inst: + mode = inst.Mode.temperature + bad_result = "42" + with pytest.raises(ValueError) as err_info: + inst._parse(bad_result, mode) + err_msg = err_info.value.args[0] + assert ( + err_msg == "Cannot parse a string that does not contain a " "return value" + ) + + +def test_parse_wrong_mode(): + """Raise ValueError if multimeter not in the right mode.""" + with expected_protocol( + ik.fluke.Fluke3000, init_sequence, init_response, "\r" + ) as inst: + mode_requested = inst.Mode.temperature + result = "ME:R:S#=01:DCC=010:PH=FD010006020C0600" + mode_result = inst.Mode(result.split("PH=")[-1][8:10]) + with pytest.raises(ValueError) as err_info: + inst._parse(result, mode_requested) + err_msg = err_info.value.args[0] + assert ( + err_msg == f"Mode {mode_requested.name} was requested but " + f"the Fluke 3000FC Multimeter is in mode " + f"{mode_result.name} instead. Could not read the " + f"requested quantity." + ) + + +def test_parse_factor_wrong_code(): + """Raise ValueError if code not in prefixes.""" + data = "00000012" + byte = format(int(data[6:8], 16), "08b") + code = int(byte[1:4], 2) + with expected_protocol( + ik.fluke.Fluke3000, init_sequence, init_response, "\r" + ) as inst: + with pytest.raises(ValueError) as err_info: + inst._parse_factor(data) + err_msg = err_info.value.args[0] + assert err_msg == f"Metric prefix not recognized: {code}" diff --git a/instruments/tests/test_generic_scpi/test_scpi_function_generator.py b/instruments/tests/test_generic_scpi/test_scpi_function_generator.py new file mode 100644 index 000000000..dc9fecd15 --- /dev/null +++ b/instruments/tests/test_generic_scpi/test_scpi_function_generator.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python +""" +Module containing tests for generic SCPI function generator instruments +""" + +# IMPORTS #################################################################### + +import pytest + +from instruments.units import ureg as u + +import instruments as ik +from instruments.tests import expected_protocol, make_name_test + +# TESTS ###################################################################### + +test_scpi_func_gen_name = make_name_test(ik.generic_scpi.SCPIFunctionGenerator) + + +def test_scpi_func_gen_amplitude(): + with expected_protocol( + ik.generic_scpi.SCPIFunctionGenerator, + [ + "VOLT:UNIT?", + "VOLT?", + "VOLT:UNIT VPP", + "VOLT 2.0", + "VOLT:UNIT DBM", + "VOLT 1.5", + ], + ["VPP", "+1.000000E+00"], + repeat=2, + ) as fg: + assert fg.amplitude == (1 * u.V, fg.VoltageMode.peak_to_peak) + fg.amplitude = 2 * u.V + fg.amplitude = (1.5 * u.V, fg.VoltageMode.dBm) + + assert fg.channel[0].amplitude == (1 * u.V, fg.VoltageMode.peak_to_peak) + fg.channel[0].amplitude = 2 * u.V + fg.channel[0].amplitude = (1.5 * u.V, fg.VoltageMode.dBm) + + +def test_scpi_func_gen_frequency(): + with expected_protocol( + ik.generic_scpi.SCPIFunctionGenerator, + ["FREQ?", "FREQ 1.005000e+02"], + ["+1.234000E+03"], + repeat=2, + ) as fg: + assert fg.frequency == 1234 * u.Hz + fg.frequency = 100.5 * u.Hz + + assert fg.channel[0].frequency == 1234 * u.Hz + fg.channel[0].frequency = 100.5 * u.Hz + + +def test_scpi_func_gen_function(): + with expected_protocol( + ik.generic_scpi.SCPIFunctionGenerator, ["FUNC?", "FUNC SQU"], ["SIN"], repeat=2 + ) as fg: + assert fg.function == fg.Function.sinusoid + fg.function = fg.Function.square + + assert fg.channel[0].function == fg.Function.sinusoid + fg.channel[0].function = fg.Function.square + + +def test_scpi_func_gen_offset(): + with expected_protocol( + ik.generic_scpi.SCPIFunctionGenerator, + ["VOLT:OFFS?", "VOLT:OFFS 4.321000e-01"], + [ + "+1.234000E+01", + ], + repeat=2, + ) as fg: + assert fg.offset == 12.34 * u.V + fg.offset = 0.4321 * u.V + + assert fg.channel[0].offset == 12.34 * u.V + fg.channel[0].offset = 0.4321 * u.V + + +def test_scpi_func_gen_phase(): + """Raise NotImplementedError when set / get phase.""" + with expected_protocol( + ik.generic_scpi.SCPIFunctionGenerator, + [], + [], + ) as fg: + with pytest.raises(NotImplementedError): + _ = fg.phase + with pytest.raises(NotImplementedError): + fg.phase = 42 diff --git a/instruments/tests/test_generic_scpi/test_scpi_instrument.py b/instruments/tests/test_generic_scpi/test_scpi_instrument.py new file mode 100644 index 000000000..78a0df00e --- /dev/null +++ b/instruments/tests/test_generic_scpi/test_scpi_instrument.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python +""" +Module containing tests for generic SCPI instruments +""" + +# IMPORTS #################################################################### + +from hypothesis import given, strategies as st +import pytest + +from instruments.units import ureg as u + +import instruments as ik +from instruments.tests import expected_protocol, make_name_test, unit_eq + +# TESTS ###################################################################### + + +test_scpi_multimeter_name = make_name_test(ik.generic_scpi.SCPIInstrument) + + +def test_scpi_instrument_scpi_version(): + """Get name of instrument.""" + retval = "12345" + with expected_protocol( + ik.generic_scpi.SCPIInstrument, ["SYST:VERS?"], [f"{retval}"] + ) as inst: + assert inst.scpi_version == retval + + +@pytest.mark.parametrize("retval", ("0", "1")) +def test_scpi_instrument_op_complete(retval): + """Check if operation is completed.""" + with expected_protocol( + ik.generic_scpi.SCPIInstrument, ["*OPC?"], [f"{retval}"] + ) as inst: + assert inst.op_complete == bool(int(retval)) + + +@pytest.mark.parametrize("retval", ("off", "0", 0, False)) +def test_scpi_instrument_power_on_status_off(retval): + """Get / set power on status for instrument to on.""" + with expected_protocol( + ik.generic_scpi.SCPIInstrument, ["*PSC 0", "*PSC?"], ["0"] + ) as inst: + inst.power_on_status = retval + assert not inst.power_on_status + + +@pytest.mark.parametrize("retval", ("on", "1", 1, True)) +def test_scpi_instrument_power_on_status_on(retval): + """Get / set power on status for instrument to on.""" + with expected_protocol( + ik.generic_scpi.SCPIInstrument, ["*PSC 1", "*PSC?"], ["1"] + ) as inst: + inst.power_on_status = retval + assert inst.power_on_status + + +def test_scpi_instrument_power_on_status_value_error(): + """Raise ValueError if power on status set with invalid value.""" + with expected_protocol(ik.generic_scpi.SCPIInstrument, [], []) as inst: + with pytest.raises(ValueError): + inst.power_on_status = 42 + + +def test_scpi_instrument_self_test_ok(): + """Check if self test returns okay.""" + with expected_protocol( + ik.generic_scpi.SCPIInstrument, ["*TST?", "*TST?"], ["0", "not ok"] # ok + ) as inst: + assert inst.self_test_ok + assert not inst.self_test_ok + + +def test_scpi_instrument_reset(): + """Reset the instrument.""" + with expected_protocol(ik.generic_scpi.SCPIInstrument, ["*RST"], []) as inst: + inst.reset() + + +def test_scpi_instrument_clear(): + """Clear the instrument.""" + with expected_protocol(ik.generic_scpi.SCPIInstrument, ["*CLS"], []) as inst: + inst.clear() + + +def test_scpi_instrument_trigger(): + """Trigger the instrument.""" + with expected_protocol(ik.generic_scpi.SCPIInstrument, ["*TRG"], []) as inst: + inst.trigger() + + +def test_scpi_instrument_wait_to_continue(): + """Wait to continue the instrument.""" + with expected_protocol(ik.generic_scpi.SCPIInstrument, ["*WAI"], []) as inst: + inst.wait_to_continue() + + +def test_scpi_instrument_line_frequency(): + """Get / set line frequency.""" + freq_hz = 100 + freq_mhz = u.Quantity(100000, u.mHz) + with expected_protocol( + ik.generic_scpi.SCPIInstrument, + [ + f"SYST:LFR {freq_hz}", + "SYST:LFR?", + f"SYST:LFR {freq_mhz.to('Hz').magnitude}", + ], + [ + f"{freq_hz}", + ], + ) as inst: + inst.line_frequency = freq_hz + unit_eq(inst.line_frequency, freq_hz * u.hertz) + # send a value as mHz + inst.line_frequency = freq_mhz + + +def test_scpi_instrument_check_error_queue(): + """Check and clear error queue.""" + ErrorCodes = ik.generic_scpi.SCPIInstrument.ErrorCodes + err1 = ErrorCodes.no_error # is skipped + err2 = ErrorCodes.invalid_separator + err3 = 13 # invalid error number + with expected_protocol( + ik.generic_scpi.SCPIInstrument, + [f"SYST:ERR:CODE:ALL?"], + [ + f"{err1.value},{err2.value},{err3}", + ], + ) as inst: + assert inst.check_error_queue() == [err2, err3] + + +@given(val=st.floats(min_value=0, max_value=1)) +def test_scpi_instrument_display_brightness(val): + """Get / set display brightness.""" + with expected_protocol( + ik.generic_scpi.SCPIInstrument, + [f"DISP:BRIG {val}", f"DISP:BRIG?"], + [ + f"{val}", + ], + ) as inst: + inst.display_brightness = val + assert inst.display_brightness == val + + +@given( + val=st.floats(allow_nan=False, allow_infinity=False).filter( + lambda x: x < 0 or x > 1 + ) +) +def test_scpi_instrument_display_brightness_invalid_value(val): + """Raise ValueError if display brightness set with invalid value.""" + with expected_protocol(ik.generic_scpi.SCPIInstrument, [], []) as inst: + with pytest.raises(ValueError) as err_info: + inst.display_brightness = val + err_msg = err_info.value.args[0] + assert err_msg == "Display brightness must be a number between 0 " "and 1." + + +@given(val=st.floats(min_value=0, max_value=1)) +def test_scpi_instrument_display_contrast(val): + """Get / set display contrast.""" + with expected_protocol( + ik.generic_scpi.SCPIInstrument, + [f"DISP:CONT {val}", f"DISP:CONT?"], + [ + f"{val}", + ], + ) as inst: + inst.display_contrast = val + assert inst.display_contrast == val + + +@given( + val=st.floats(allow_nan=False, allow_infinity=False).filter( + lambda x: x < 0 or x > 1 + ) +) +def test_scpi_instrument_display_contrast_invalid_value(val): + """Raise ValueError if display contrast set with invalid value.""" + with expected_protocol(ik.generic_scpi.SCPIInstrument, [], []) as inst: + with pytest.raises(ValueError) as err_info: + inst.display_contrast = val + err_msg = err_info.value.args[0] + assert err_msg == "Display contrast must be a number between 0 " "and 1." diff --git a/instruments/tests/test_generic_scpi/test_scpi_multimeter.py b/instruments/tests/test_generic_scpi/test_scpi_multimeter.py index a65a55cd9..85bc1d023 100644 --- a/instruments/tests/test_generic_scpi/test_scpi_multimeter.py +++ b/instruments/tests/test_generic_scpi/test_scpi_multimeter.py @@ -1,14 +1,13 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing tests for generic SCPI multimeter instruments """ # IMPORTS #################################################################### -from __future__ import absolute_import +import pytest -import quantities as pq +from instruments.units import ureg as u import instruments as ik from instruments.tests import expected_protocol, make_name_test, unit_eq @@ -21,12 +20,8 @@ def test_scpi_multimeter_mode(): with expected_protocol( ik.generic_scpi.SCPIMultimeter, - [ - "CONF?", - "CONF:CURR:AC" - ], [ - "FRES +1.000000E+01,+3.000000E-06" - ] + ["CONF?", "CONF:CURR:AC"], + ["FRES +1.000000E+01,+3.000000E-06"], ) as dmm: assert dmm.mode == dmm.Mode.fourpt_resistance dmm.mode = dmm.Mode.current_ac @@ -34,13 +29,7 @@ def test_scpi_multimeter_mode(): def test_scpi_multimeter_trigger_mode(): with expected_protocol( - ik.generic_scpi.SCPIMultimeter, - [ - "TRIG:SOUR?", - "TRIG:SOUR EXT" - ], [ - "BUS" - ] + ik.generic_scpi.SCPIMultimeter, ["TRIG:SOUR?", "TRIG:SOUR EXT"], ["BUS"] ) as dmm: assert dmm.trigger_mode == dmm.TriggerMode.bus dmm.trigger_mode = dmm.TriggerMode.external @@ -55,18 +44,19 @@ def test_scpi_multimeter_input_range(): "CONF?", # 3.1 "CONF:FRES MIN", # 3.2 "CONF?", # 4.1 - "CONF:CURR:DC 1.0" # 4.2 - ], [ + "CONF:CURR:DC 1", # 4.2 + ], + [ "CURR:AC +1.000000E+01,+3.000000E-06", # 1 "CURR:AC AUTO,+3.000000E-06", # 2 "FRES +1.000000E+01,+3.000000E-06", # 3 - "CURR:DC +1.000000E+01,+3.000000E-06" # 4 - ] + "CURR:DC +1.000000E+01,+3.000000E-06", # 4 + ], ) as dmm: - unit_eq(dmm.input_range, 1e1 * pq.amp) + unit_eq(dmm.input_range, 1e1 * u.amp) assert dmm.input_range == dmm.InputRange.automatic dmm.input_range = dmm.InputRange.minimum - dmm.input_range = 1 * pq.amp + dmm.input_range = 1 * u.amp def test_scpi_multimeter_resolution(): @@ -78,13 +68,14 @@ def test_scpi_multimeter_resolution(): "CONF?", # 3.1 "CONF:FRES +1.000000E+01,MIN", # 3.2 "CONF?", # 4.1 - "CONF:CURR:DC +1.000000E+01,3e-06" # 4.2 - ], [ + "CONF:CURR:DC +1.000000E+01,3e-06", # 4.2 + ], + [ "VOLT +1.000000E+01,+3.000000E-06", # 1 "VOLT +1.000000E+01,MAX", # 2 "FRES +1.000000E+01,+3.000000E-06", # 3 - "CURR:DC +1.000000E+01,+3.000000E-06" # 4 - ] + "CURR:DC +1.000000E+01,+3.000000E-06", # 4 + ], ) as dmm: assert dmm.resolution == 3e-06 assert dmm.resolution == dmm.Resolution.maximum @@ -92,18 +83,29 @@ def test_scpi_multimeter_resolution(): dmm.resolution = 3e-06 +def test_scpi_multimeter_resolution_type_error(): + """Raise TypeError if resolution value has the wrong type.""" + with expected_protocol( + ik.generic_scpi.SCPIMultimeter, ["CONF?"], ["VOLT +1.000000E+01,+3.000000E-06"] + ) as dmm: + wrong_type = "42" + with pytest.raises(TypeError) as err_info: + dmm.resolution = wrong_type + err_msg = err_info.value.args[0] + assert err_msg == ( + "Resolution must be specified as an int, float, " + "or SCPIMultimeter.Resolution value." + ) + + def test_scpi_multimeter_trigger_count(): with expected_protocol( ik.generic_scpi.SCPIMultimeter, + ["TRIG:COUN?", "TRIG:COUN?", "TRIG:COUN MIN", "TRIG:COUN 10"], [ - "TRIG:COUN?", - "TRIG:COUN?", - "TRIG:COUN MIN", - "TRIG:COUN 10" - ], [ "+10", "INF", - ] + ], ) as dmm: assert dmm.trigger_count == 10 assert dmm.trigger_count == dmm.TriggerCount.infinity @@ -111,18 +113,27 @@ def test_scpi_multimeter_trigger_count(): dmm.trigger_count = 10 +def test_scpi_multimeter_trigger_count_type_error(): + """Raise TypeError if trigger count value has the wrong type.""" + with expected_protocol(ik.generic_scpi.SCPIMultimeter, [], []) as dmm: + wrong_type = "42" + with pytest.raises(TypeError) as err_info: + dmm.trigger_count = wrong_type + err_msg = err_info.value.args[0] + assert err_msg == ( + "Trigger count must be specified as an int " + "or SCPIMultimeter.TriggerCount value." + ) + + def test_scpi_multimeter_sample_count(): with expected_protocol( ik.generic_scpi.SCPIMultimeter, + ["SAMP:COUN?", "SAMP:COUN?", "SAMP:COUN MIN", "SAMP:COUN 10"], [ - "SAMP:COUN?", - "SAMP:COUN?", - "SAMP:COUN MIN", - "SAMP:COUN 10" - ], [ "+10", "MAX", - ] + ], ) as dmm: assert dmm.sample_count == 10 assert dmm.sample_count == dmm.SampleCount.maximum @@ -130,18 +141,32 @@ def test_scpi_multimeter_sample_count(): dmm.sample_count = 10 +def test_scpi_multimeter_sample_count_type_error(): + """Raise TypeError if sample count is of invalid type.""" + with expected_protocol(ik.generic_scpi.SCPIMultimeter, [], []) as dmm: + wrong_type = "42" + with pytest.raises(TypeError) as err_info: + dmm.sample_count = wrong_type + err_msg = err_info.value.args[0] + assert err_msg == ( + "Sample count must be specified as an int " + "or SCPIMultimeter.SampleCount value." + ) + + def test_scpi_multimeter_trigger_delay(): with expected_protocol( ik.generic_scpi.SCPIMultimeter, [ "TRIG:DEL?", - "TRIG:DEL {:e}".format(1), - ], [ + f"TRIG:DEL {1:e}", + ], + [ "+1", - ] + ], ) as dmm: - unit_eq(dmm.trigger_delay, 1 * pq.second) - dmm.trigger_delay = 1000 * pq.millisecond + unit_eq(dmm.trigger_delay, 1 * u.second) + dmm.trigger_delay = 1000 * u.millisecond def test_scpi_multimeter_sample_source(): @@ -150,9 +175,10 @@ def test_scpi_multimeter_sample_source(): [ "SAMP:SOUR?", "SAMP:SOUR TIM", - ], [ + ], + [ "IMM", - ] + ], ) as dmm: assert dmm.sample_source == dmm.SampleSource.immediate dmm.sample_source = dmm.SampleSource.timer @@ -163,13 +189,23 @@ def test_scpi_multimeter_sample_timer(): ik.generic_scpi.SCPIMultimeter, [ "SAMP:TIM?", - "SAMP:TIM {:e}".format(1), - ], [ + f"SAMP:TIM {1:e}", + ], + [ "+1", - ] + ], ) as dmm: - unit_eq(dmm.sample_timer, 1 * pq.second) - dmm.sample_timer = 1000 * pq.millisecond + unit_eq(dmm.sample_timer, 1 * u.second) + dmm.sample_timer = 1000 * u.millisecond + + +def test_scpi_multimeter_relative_not_implemented(): + """Raise NotImplementedError when set / get relative.""" + with expected_protocol(ik.generic_scpi.SCPIMultimeter, [], []) as dmm: + with pytest.raises(NotImplementedError): + _ = dmm.relative + with pytest.raises(NotImplementedError): + dmm.relative = 42 def test_scpi_multimeter_measure(): @@ -177,8 +213,38 @@ def test_scpi_multimeter_measure(): ik.generic_scpi.SCPIMultimeter, [ "MEAS:VOLT:DC?", - ], [ + ], + [ + "+4.23450000E-03", + ], + ) as dmm: + unit_eq(dmm.measure(dmm.Mode.voltage_dc), 4.2345e-03 * u.volt) + + +def test_scpi_multimeter_measure_mode_none(): + """Read current mode if not specified, test with volt, DC mode.""" + with expected_protocol( + ik.generic_scpi.SCPIMultimeter, + [ + "CONF?", + "MEAS:VOLT:DC?", + ], + [ + "VOLT:DC", "+4.23450000E-03", - ] + ], ) as dmm: - unit_eq(dmm.measure(dmm.Mode.voltage_dc), 4.2345e-03 * pq.volt) + unit_eq(dmm.measure(), 4.2345e-03 * u.volt) + + +def test_scpi_multimeter_measure_invalid_mode(): + """Raise TypeError if mode is not of type SCPIMultimeter.Mode.""" + with expected_protocol(ik.generic_scpi.SCPIMultimeter, [], []) as dmm: + wrong_type = 42 + with pytest.raises(TypeError) as err_info: + dmm.measure(mode=wrong_type) + err_msg = err_info.value.args[0] + assert ( + err_msg == f"Mode must be specified as a SCPIMultimeter.Mode " + f"value, got {type(wrong_type)} instead." + ) diff --git a/instruments/tests/test_gentec_eo/__init__.py b/instruments/tests/test_gentec_eo/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/instruments/tests/test_gentec_eo/test_blu.py b/instruments/tests/test_gentec_eo/test_blu.py new file mode 100644 index 000000000..04d6e5bc7 --- /dev/null +++ b/instruments/tests/test_gentec_eo/test_blu.py @@ -0,0 +1,471 @@ +#!/usr/bin/env python +""" +Module containing tests for the Gentec-eo Blu +""" + +# IMPORTS #################################################################### + +from hypothesis import given, strategies as st +import pytest + +import instruments as ik +from instruments.tests import expected_protocol +from instruments.units import ureg as u + +# TESTS ###################################################################### + +# pylint: disable=protected-access + + +# TESTS FOR Blu # + + +def test_blu_initialization(): + """Initialize the device.""" + with expected_protocol( + ik.gentec_eo.Blu, + [], + [], + sep="\r\n", + ) as blu: + assert blu.terminator == "\r\n" + assert blu._power_mode is None + + +# TEST PROPERTIES # + + +def test_blu_anticipation(): + """Get / Set the instrument into anticipation mode.""" + with expected_protocol( + ik.gentec_eo.Blu, + ["*GAN", "*ANT0"], + ["Anticipation: 1", "ACK"], + sep="\r\n", + ) as blu: + assert blu.anticipation + blu.anticipation = False + + +def test_blu_auto_scale(): + """Get / Set the instrument into automatic scaling mode.""" + with expected_protocol( + ik.gentec_eo.Blu, + ["*GAS", "*SAS0"], + ["Autoscale: 1", "ACK"], + sep="\r\n", + ) as blu: + assert blu.auto_scale + blu.auto_scale = False + + +def test_blu_available_scales(): + """Get the available scales that are on teh blue device. + + Note that the routine tested here will temporarily overwrite the + terminator and the timeout. The function here is special in the + sense that it returns a list of parameters, all individual entries + are separated by the terminator. There is no clear end to when this + should be finished. It is assumed that 1 second is enough time to + send all the data. + """ + with expected_protocol( + ik.gentec_eo.Blu, + ["*DVS"], + [ + "[22]: 100.0 m\r\n" + "[23]: 300.0 m\r\n" + "[24]: 1.000\r\n" + "[25]: 3.000\r\n" + "[26]: 10.00\r\n" + "[27]: 30.00\r\n" + "[28]: 100.0\r\n" + ], + sep="", + ) as blu: + ret_scale = [ + blu.Scale.max100milli, + blu.Scale.max300milli, + blu.Scale.max1, + blu.Scale.max3, + blu.Scale.max10, + blu.Scale.max30, + blu.Scale.max100, + ] + assert blu.available_scales == ret_scale + + +def test_blu_available_scales_error(): + """Ensure that temporary variables are reset if read errors. + + Return a `bogus` value, which is not an available scale, and ensure + that the temporary variables are reset afterwards. This specific + case raises a ValueError. + """ + with expected_protocol( + ik.gentec_eo.Blu, + ["*DVS"], + ["bogus"], + sep="", + ) as blu: + _terminator = blu.terminator + _timeout = blu.timeout + with pytest.raises(ValueError): + _ = blu.available_scales + assert blu.terminator == _terminator + assert blu.timeout == _timeout + + +def test_blu_battery_state(): + """Get the battery state of the instrument in percent.""" + with expected_protocol( + ik.gentec_eo.Blu, + ["*QSO"], + ["98"], + sep="\r\n", + ) as blu: + assert blu.battery_state == u.Quantity(98, u.percent) + + +def test_blu_current_value_watts(): + """Get the current value in Watt mode.""" + with expected_protocol( + ik.gentec_eo.Blu, + ["*GMD", "*CVU"], + ["Mode: 0", "42"], + sep="\r\n", + ) as blu: + assert blu.current_value == u.Quantity(42, u.W) + + +def test_blu_current_value_joules(): + """Get the current value in Watt mode.""" + with expected_protocol( + ik.gentec_eo.Blu, + ["*GMD", "*CVU"], + ["Mode: 2", "42"], + sep="\r\n", + ) as blu: + assert blu.current_value == u.Quantity(42, u.J) + + +def test_blu_head_type(): + """Get information on the connected power meter head. + + Here, an example head is returned. + """ + with expected_protocol( + ik.gentec_eo.Blu, + ["*GFW"], + ["NIG : 104552, Wattmeter, V1.95"], + sep="\r\n", + ) as blu: + example_head = "NIG : 104552, Wattmeter, V1.95" + assert blu.head_type == example_head + + +def test_blu_measure_mode(): + """Get the measure mode the head is in. + + This routine is also run when a unitful response is returned from + another routine and the measurement mode has not been determined + before. + """ + with expected_protocol( + ik.gentec_eo.Blu, + ["*GMD", "*GMD"], + ["Mode: 0", "Mode: 2"], + sep="\r\n", + ) as blu: + # power mode + assert blu.measure_mode == "power" + assert blu._power_mode + + # single shot energy mode (J) + assert blu.measure_mode == "sse" + assert not blu._power_mode + + +def test_blu_new_value_ready(): + """Query if a new value is ready for reading.""" + with expected_protocol( + ik.gentec_eo.Blu, + ["*NVU", "*NVU"], + ["New Data Not Available", "New Data Available"], + sep="\r\n", + ) as blu: + assert not blu.new_value_ready + assert blu.new_value_ready + + +@pytest.mark.parametrize("scale", ik.gentec_eo.Blu.Scale) +def test_blu_scale(scale): + """Get / set the instrument scale manually.""" + with expected_protocol( + ik.gentec_eo.Blu, + [f"*SCS{scale.value}", "*GCR"], + ["ACK", f"Range: {scale.value}"], + sep="\r\n", + ) as blu: + blu.scale = scale + assert blu.scale == scale + + +def test_blu_single_shot_energy_mode(): + """Get / set the single shot energy mode.""" + with expected_protocol( + ik.gentec_eo.Blu, + ["*GSE", "*SSE1"], + ["SSE: 0", "ACK"], + sep="\r\n", + ) as blu: + assert not blu.single_shot_energy_mode + assert blu._power_mode + blu.single_shot_energy_mode = True + assert not blu._power_mode + + +def test_blu_trigger_level(): + """Get / set the trigger level.""" + with expected_protocol( + ik.gentec_eo.Blu, + ["*GTL", "*STL53.4", "*STL01.2", "*STL1.23"], + [ + "Trigger level: 15.4% (4.6 Watts) of max power: 30 Watts", + "ACK", + "ACK", + "ACK", + ], + sep="\r\n", + ) as blu: + assert blu.trigger_level == 0.154 + blu.trigger_level = 0.534 + blu.trigger_level = 0.012 + blu.trigger_level = 0.0123 + + +def test_blu_trigger_level_invalid_value(): + """Raise error when trigger level value set is out of bound.""" + with expected_protocol( + ik.gentec_eo.Blu, + [], + [], + sep="\r\n", + ) as blu: + with pytest.raises(ValueError): + blu.trigger_level = -0.3 + with pytest.raises(ValueError): + blu.trigger_level = 1.1 + + +def test_blu_usb_state(): + """Get the status if USB cable is plugged in.""" + with expected_protocol( + ik.gentec_eo.Blu, + ["*USB"], + ["USB: 1"], + sep="\r\n", + ) as blu: + assert blu.usb_state + + +def test_blu_user_multiplier(): + """Get / set user multiplier.""" + with expected_protocol( + ik.gentec_eo.Blu, + ["*GUM", "*MUL435.6666"], + ["User Multiplier: 3.3000000e+01", "ACK"], + sep="\r\n", + ) as blu: + assert blu.user_multiplier == 33.0 + blu.user_multiplier = 435.6666 + + +def test_blu_user_offset_watts(): + """Get / set user offset in watts.""" + with expected_protocol( + ik.gentec_eo.Blu, + ["*GMD", "*GUO", "*OFF000042.0"], # get power mode + ["Mode: 0", "User Offset : 1.500e-3", "ACK"], # power mode watts + sep="\r\n", + ) as blu: + assert blu.user_offset == u.Quantity(1.5, u.mW) + blu.user_offset = u.Quantity(42.0, u.W) + + +def test_blu_user_offset_joules(): + """Get / set user offset in joules.""" + with expected_protocol( + ik.gentec_eo.Blu, + ["*GMD", "*GUO", "*OFF000042.0"], # get power mode + ["Mode: 2", "User Offset : 1.500e-3", "ACK"], # power mode joules + sep="\r\n", + ) as blu: + assert blu.user_offset == u.Quantity(0.0015, u.J) + blu.user_offset = u.Quantity(42.0, u.J) + + +def test_blu_user_offset_unitless(): + """Set user offset unitless.""" + with expected_protocol( + ik.gentec_eo.Blu, + ["*OFF000042.0"], + ["ACK"], + sep="\r\n", + ) as blu: + blu.user_offset = 42.0 + + +def test_blu_user_offset_unit_error(): + """Raise ValueError if unit is invalid.""" + with expected_protocol( + ik.gentec_eo.Blu, + [], + [], + sep="\r\n", + ) as blu: + with pytest.raises(ValueError): + blu.user_offset = u.Quantity(42, u.mm) + + +def test_blu_version(): + """Query version of device.""" + with expected_protocol( + ik.gentec_eo.Blu, + ["*VER"], + ["Blu firmware Version 1.95"], + sep="\r\n", + ) as blu: + version = "Blu firmware Version 1.95" + assert blu.version == version + + +def test_blu_wavelength(): + """Get / set the wavelength.""" + with expected_protocol( + ik.gentec_eo.Blu, + ["*GWL", "*PWC00527", "*PWC00527"], + ["PWC: 1064", "ACK", "ACK"], + sep="\r\n", + ) as blu: + assert blu.wavelength == u.Quantity(1064, u.nm) + blu.wavelength = u.Quantity(0.527, u.um) + blu.wavelength = 527 + + +def test_blu_wavelength_out_of_bound(): + """Get / set the wavelength when value is out of bound.""" + with expected_protocol( + ik.gentec_eo.Blu, + ["*PWC00000", "*PWC00000"], + ["ACK", "ACK"], + sep="\r\n", + ) as blu: + blu.wavelength = u.Quantity(1000, u.um) + blu.wavelength = -3 + + +def test_blu_zero_offset(): + """Get / set the zero offset.""" + with expected_protocol( + ik.gentec_eo.Blu, + ["*GZO", "*SOU", "*COU"], + ["Zero: 1", "ACK", "ACK"], + sep="\r\n", + ) as blu: + assert blu.zero_offset + blu.zero_offset = True + blu.zero_offset = False + + +# TEST METHODS # + + +def test_blu_confirm_connection(): + """Confirm a bluetooth connection.""" + with expected_protocol( + ik.gentec_eo.Blu, + ["*RDY"], + ["ACK"], + sep="\r\n", + ) as blu: + blu.confirm_connection() + + +def test_blu_disconnect(): + """Disconnect bluetooth connection.""" + with expected_protocol( + ik.gentec_eo.Blu, + ["*BTD"], + ["ACK"], + sep="\r\n", + ) as blu: + blu.disconnect() + + +def test_blu_scale_down(): + """Set the scale one level lower.""" + with expected_protocol( + ik.gentec_eo.Blu, + ["*SSD"], + ["ACK"], + sep="\r\n", + ) as blu: + blu.scale_down() + + +def test_blu_scale_up(): + """Set the scale one level higher.""" + with expected_protocol( + ik.gentec_eo.Blu, + ["*SSU"], + ["ACK"], + sep="\r\n", + ) as blu: + blu.scale_up() + + +def test_no_ack_query_error(mocker): + """Ensure temporary variables reset if `_no_ack_query` errors. + + Mocking query here in order to raise an error on query. + """ + with expected_protocol( + ik.gentec_eo.Blu, + [], + [], + sep="\r\n", + ) as blu: + # mock query w/ IOError + io_error_mock = mocker.Mock() + io_error_mock.side_effect = IOError + mocker.patch.object(blu, "query", io_error_mock) + # do the query + with pytest.raises(IOError): + _ = blu._no_ack_query("QUERY") + assert blu._ack_message == "ACK" + + +# NON-Blu ROUTINES # + + +def test_format_eight_type(): + """Ensure type returned is string.""" + assert isinstance(ik.gentec_eo.blu._format_eight(3.0), str) + + +@given( + value=st.floats( + min_value=-1e100, max_value=1e100, exclude_min=True, exclude_max=True + ) +) +def test_format_eight_length_values(value): + """Ensure format eight routine works. + + This is a helper routine for the blu device to cut any number to + eight characters. Make sure this is the case with various numbers + and that it is correct to 1% with given number. + """ + value_read = ik.gentec_eo.blu._format_eight(value) + assert value == pytest.approx(float(value_read), abs(value) / 100.0) + assert len(value_read) == 8 diff --git a/instruments/tests/test_glassman/__init__.py b/instruments/tests/test_glassman/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/instruments/tests/test_glassman/test_glassmanfr.py b/instruments/tests/test_glassman/test_glassmanfr.py new file mode 100644 index 000000000..9d7e60e91 --- /dev/null +++ b/instruments/tests/test_glassman/test_glassmanfr.py @@ -0,0 +1,230 @@ +#!/usr/bin/env python +""" +Module containing tests for the Glassman FR power supply +""" + +# IMPORTS #################################################################### + +import pytest + +import instruments as ik +from instruments.tests import expected_protocol +from instruments.units import ureg as u + + +# TESTS ###################################################################### + +# pylint: disable=protected-access + + +def set_defaults(inst): + """ + Sets default values for the voltage and current range of the Glassman FR + to be used to test the voltage and current property getters/setters. + """ + inst.voltage_max = 50.0 * u.kilovolt + inst.current_max = 6.0 * u.milliamp + inst.polarity = +1 + + +def test_channel(): + with expected_protocol(ik.glassman.GlassmanFR, [], [], "\r") as inst: + assert len(inst.channel) == 1 + assert inst.channel[0] == inst + + +def test_voltage(): + with expected_protocol( + ik.glassman.GlassmanFR, + ["\x01Q51", "\x01S3330000000001CD"], + ["R00000000000040", "A"], + "\r", + ) as inst: + set_defaults(inst) + inst.voltage = 10.0 * u.kilovolt + assert inst.voltage == 10.0 * u.kilovolt + + +def test_current(): + with expected_protocol( + ik.glassman.GlassmanFR, + ["\x01Q51", "\x01S0003330000001CD"], + ["R00000000000040", "A"], + "\r", + ) as inst: + set_defaults(inst) + inst.current = 1.2 * u.milliamp + assert inst.current == 1.2 * u.milliamp + + +def test_voltage_sense(): + with expected_protocol( + ik.glassman.GlassmanFR, ["\x01Q51"], ["R10A00000010053"], "\r" + ) as inst: + set_defaults(inst) + assert round(inst.voltage_sense) == 13.0 * u.kilovolt + + +def test_current_sense(): + with expected_protocol( + ik.glassman.GlassmanFR, ["\x01Q51"], ["R0001550001004C"], "\r" + ) as inst: + set_defaults(inst) + assert inst.current_sense == 2.0 * u.milliamp + + +def test_mode(): + with expected_protocol( + ik.glassman.GlassmanFR, + ["\x01Q51", "\x01Q51"], + ["R00000000000040", "R00000000010041"], + "\r", + ) as inst: + assert inst.mode == inst.Mode.voltage + assert inst.mode == inst.Mode.current + + +def test_output(): + with expected_protocol( + ik.glassman.GlassmanFR, + ["\x01S0000000000001C4", "\x01Q51", "\x01S0000000000002C5", "\x01Q51"], + ["A", "R00000000000040", "A", "R00000000040044"], + "\r", + ) as inst: + inst.output = False + assert not inst.output + inst.output = True + assert inst.output + + +def test_output_type_error(): + """Raise TypeError when setting output w non-boolean value.""" + with expected_protocol(ik.glassman.GlassmanFR, [], [], "\r") as inst: + with pytest.raises(TypeError) as err_info: + inst.output = 42 + err_msg = err_info.value.args[0] + assert err_msg == "Output status mode must be a boolean." + + +@pytest.mark.parametrize("value", [0, 2]) +def test_fault(value): + """Get the instrument status: True if fault.""" + with expected_protocol( + ik.glassman.GlassmanFR, + ["\x01Q51"], + [ + f"R000000000{value}004{value}", + ], + "\r", + ) as inst: + assert inst.fault == bool(value) + + +def test_version(): + with expected_protocol( + ik.glassman.GlassmanFR, ["\x01V56"], ["B1465"], "\r" + ) as inst: + assert inst.version == "14" + + +def test_device_timeout(): + with expected_protocol( + ik.glassman.GlassmanFR, ["\x01C073", "\x01C174"], ["A", "A"], "\r" + ) as inst: + inst.device_timeout = True + assert inst.device_timeout + inst.device_timeout = False + assert not inst.device_timeout + + +def test_device_timeout_type_error(): + """Raise TypeError if device timeout mode not set with boolean.""" + with expected_protocol(ik.glassman.GlassmanFR, [], [], "\r") as inst: + with pytest.raises(TypeError) as err_info: + inst.device_timeout = 42 + err_msg = err_info.value.args[0] + assert err_msg == "Device timeout mode must be a boolean." + + +def test_sendcmd(): + with expected_protocol(ik.glassman.GlassmanFR, ["\x01123ABC5C"], [], "\r") as inst: + inst.sendcmd("123ABC") + + +def test_query(): + """Query the instrument.""" + response = "R123ABC5C" + with expected_protocol( + ik.glassman.GlassmanFR, ["\x01Q123ABCAD"], [response], "\r" + ) as inst: + assert inst.query("Q123ABC") == response[1:-2] + + +def test_query_invalid_response_code(): + """Raise ValueError when query receives an invalid response code.""" + response = "A123ABC5C" + with expected_protocol( + ik.glassman.GlassmanFR, ["\x01Q123ABCAD"], [response], "\r" + ) as inst: + with pytest.raises(ValueError) as err_info: + inst.query("Q123ABC") + err_msg = err_info.value.args[0] + assert err_msg == f"Invalid response code: {response}" + + +def test_query_invalid_checksum(): + """Raise ValueError if query returns with invalid checksum.""" + response = "R123ABC5A" + with expected_protocol( + ik.glassman.GlassmanFR, ["\x01Q123ABCAD"], [response], "\r" + ) as inst: + with pytest.raises(ValueError) as err_info: + inst.query("Q123ABC") + err_msg = err_info.value.args[0] + assert err_msg == f"Invalid checksum: {response}" + + +@pytest.mark.parametrize("err", ik.glassman.GlassmanFR.ErrorCode) +def test_query_error(err): + """Raise ValueError if query returns with error.""" + err_code = err.value + check_sum = ord(err_code) % 256 + response = f"E{err_code}{format(check_sum, '02X')}" + with expected_protocol( + ik.glassman.GlassmanFR, ["\x01Q123ABCAD"], [response], "\r" + ) as inst: + with pytest.raises(ValueError) as err_info: + inst.query("Q123ABC") + err_msg = err_info.value.args[0] + assert err_msg == f"Instrument responded with error: {err.name}" + + +def test_reset(): + with expected_protocol( + ik.glassman.GlassmanFR, ["\x01S0000000000004C7"], ["A"], "\r" + ) as inst: + inst.reset() + + +def test_set_status(): + with expected_protocol( + ik.glassman.GlassmanFR, + ["\x01S3333330000002D7", "\x01Q51"], + ["A", "R00000000040044"], + "\r", + ) as inst: + set_defaults(inst) + inst.set_status(voltage=10 * u.kilovolt, current=1.2 * u.milliamp, output=True) + assert inst.output + assert inst.voltage == 10 * u.kilovolt + assert inst.current == 1.2 * u.milliamp + + +def test_parse_invalid_response(): + """Raise a RunTime error if response cannot be parsed.""" + response = "000000000X00" # invalid monitors + with expected_protocol(ik.glassman.GlassmanFR, [], [], "\r") as inst: + with pytest.raises(RuntimeError) as err_info: + inst._parse_response(response) + err_msg = err_info.value.args[0] + assert err_msg == f"Cannot parse response packet: {response}" diff --git a/instruments/tests/test_holzworth/test_holzworth_hs9000.py b/instruments/tests/test_holzworth/test_holzworth_hs9000.py index 8e531f586..ce5684cae 100644 --- a/instruments/tests/test_holzworth/test_holzworth_hs9000.py +++ b/instruments/tests/test_holzworth/test_holzworth_hs9000.py @@ -1,19 +1,16 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Unit tests for the Holzworth HS9000 """ # IMPORTS ##################################################################### -from __future__ import absolute_import -import quantities as pq -import mock +from instruments.units import ureg as u import instruments as ik from instruments.tests import expected_protocol -from instruments.units import dBm +from .. import mock # TEST CLASSES ################################################################ @@ -23,15 +20,9 @@ def test_hs9000_name(): with expected_protocol( ik.holzworth.HS9000, - [ - ":ATTACH?", - ":CH1:IDN?" - ], - [ - ":CH1:CH2:FOO", - "Foobar name" - ], - sep="\n" + [":ATTACH?", ":CH1:IDN?"], + [":CH1:CH2:FOO", "Foobar name"], + sep="\n", ) as hs: assert hs.name == "Foobar name" @@ -42,10 +33,8 @@ def test_channel_idx_list(): [ ":ATTACH?", ], - [ - ":CH1:CH2:FOO" - ], - sep="\n" + [":CH1:CH2:FOO"], + sep="\n", ) as hs: assert hs._channel_idxs() == [0, 1, "FOO"] @@ -56,10 +45,8 @@ def test_channel_returns_inner_class(): [ ":ATTACH?", ], - [ - ":CH1:CH2:FOO" - ], - sep="\n" + [":CH1:CH2:FOO"], + sep="\n", ) as hs: channel = hs.channel[0] assert isinstance(channel, hs.Channel) is True @@ -108,163 +95,92 @@ def test_channel_save_state(): def test_channel_temperature(): with expected_protocol( ik.holzworth.HS9000, - [ - ":ATTACH?", - ":CH1:TEMP?" - ], - [ - ":CH1:CH2:FOO", - "10 C" - ], - sep="\n" + [":ATTACH?", ":CH1:TEMP?"], + [":CH1:CH2:FOO", "10 C"], + sep="\n", ) as hs: channel = hs.channel[0] - assert channel.temperature == 10 * pq.degC + assert channel.temperature == u.Quantity(10, u.degC) def test_channel_frequency_getter(): with expected_protocol( ik.holzworth.HS9000, - [ - ":ATTACH?", - ":CH1:FREQ?", - ":CH1:FREQ:MIN?", - ":CH1:FREQ:MAX?" - ], - [ - ":CH1:CH2:FOO", - "1000 MHz", - "100 MHz", - "10 GHz" - ], - sep="\n" + [":ATTACH?", ":CH1:FREQ?", ":CH1:FREQ:MIN?", ":CH1:FREQ:MAX?"], + [":CH1:CH2:FOO", "1000 MHz", "100 MHz", "10 GHz"], + sep="\n", ) as hs: channel = hs.channel[0] - assert channel.frequency == 1 * pq.GHz - assert channel.frequency_min == 100 * pq.MHz - assert channel.frequency_max == 10 * pq.GHz + assert channel.frequency == 1 * u.GHz + assert channel.frequency_min == 100 * u.MHz + assert channel.frequency_max == 10 * u.GHz def test_channel_frequency_setter(): with expected_protocol( ik.holzworth.HS9000, - [ - ":ATTACH?", - ":CH1:FREQ:MIN?", - ":CH1:FREQ:MAX?", - ":CH1:FREQ {:e}".format(1) - ], - [ - ":CH1:CH2:FOO", - "100 MHz", - "10 GHz" - ], - sep="\n" + [":ATTACH?", ":CH1:FREQ:MIN?", ":CH1:FREQ:MAX?", f":CH1:FREQ {1:e}"], + [":CH1:CH2:FOO", "100 MHz", "10 GHz"], + sep="\n", ) as hs: channel = hs.channel[0] - channel.frequency = 1 * pq.GHz + channel.frequency = 1 * u.GHz def test_channel_power_getter(): with expected_protocol( ik.holzworth.HS9000, - [ - ":ATTACH?", - ":CH1:PWR?", - ":CH1:PWR:MIN?", - ":CH1:PWR:MAX?" - ], - [ - ":CH1:CH2:FOO", - "0", - "-100", - "20" - ], - sep="\n" + [":ATTACH?", ":CH1:PWR?", ":CH1:PWR:MIN?", ":CH1:PWR:MAX?"], + [":CH1:CH2:FOO", "0", "-100", "20"], + sep="\n", ) as hs: channel = hs.channel[0] - assert channel.power == 0 * dBm - assert channel.power_min == -100 * dBm - assert channel.power_max == 20 * dBm + assert channel.power == u.Quantity(0, u.dBm) + assert channel.power_min == u.Quantity(-100, u.dBm) + assert channel.power_max == u.Quantity(20, u.dBm) def test_channel_power_setter(): with expected_protocol( ik.holzworth.HS9000, - [ - ":ATTACH?", - ":CH1:PWR:MIN?", - ":CH1:PWR:MAX?", - ":CH1:PWR {:e}".format(0) - ], - [ - ":CH1:CH2:FOO", - "-100", - "20" - ], - sep="\n" + [":ATTACH?", ":CH1:PWR:MIN?", ":CH1:PWR:MAX?", f":CH1:PWR {0:e}"], + [":CH1:CH2:FOO", "-100", "20"], + sep="\n", ) as hs: channel = hs.channel[0] - channel.power = 0 * dBm + channel.power = u.Quantity(0, u.dBm) def test_channel_phase_getter(): with expected_protocol( ik.holzworth.HS9000, - [ - ":ATTACH?", - ":CH1:PHASE?", - ":CH1:PHASE:MIN?", - ":CH1:PHASE:MAX?" - ], - [ - ":CH1:CH2:FOO", - "0", - "-180", - "+180" - ], - sep="\n" + [":ATTACH?", ":CH1:PHASE?", ":CH1:PHASE:MIN?", ":CH1:PHASE:MAX?"], + [":CH1:CH2:FOO", "0", "-180", "+180"], + sep="\n", ) as hs: channel = hs.channel[0] - assert channel.phase == 0 * pq.degree - assert channel.phase_min == -180 * pq.degree - assert channel.phase_max == 180 * pq.degree + assert channel.phase == 0 * u.degree + assert channel.phase_min == -180 * u.degree + assert channel.phase_max == 180 * u.degree def test_channel_phase_setter(): with expected_protocol( ik.holzworth.HS9000, - [ - ":ATTACH?", - ":CH1:PHASE:MIN?", - ":CH1:PHASE:MAX?", - ":CH1:PHASE {:e}".format(0) - ], - [ - ":CH1:CH2:FOO", - "-180", - "+180" - ], - sep="\n" + [":ATTACH?", ":CH1:PHASE:MIN?", ":CH1:PHASE:MAX?", f":CH1:PHASE {0:e}"], + [":CH1:CH2:FOO", "-180", "+180"], + sep="\n", ) as hs: channel = hs.channel[0] - channel.phase = 0 * pq.degree + channel.phase = 0 * u.degree def test_channel_output(): with expected_protocol( ik.holzworth.HS9000, - [ - ":ATTACH?", - ":CH1:PWR:RF?", - ":CH1:PWR:RF:ON", - ":CH1:PWR:RF:OFF" - ], - [ - ":CH1:CH2:FOO", - "OFF" - ], - sep="\n" + [":ATTACH?", ":CH1:PWR:RF?", ":CH1:PWR:RF:ON", ":CH1:PWR:RF:OFF"], + [":CH1:CH2:FOO", "OFF"], + sep="\n", ) as hs: channel = hs.channel[0] assert channel.output is False @@ -275,15 +191,9 @@ def test_channel_output(): def test_hs9000_is_ready(): with expected_protocol( ik.holzworth.HS9000, - [ - ":COMM:READY?", - ":COMM:READY?" - ], - [ - "Ready", - "DANGER DANGER" - ], - sep="\n" + [":COMM:READY?", ":COMM:READY?"], + ["Ready", "DANGER DANGER"], + sep="\n", ) as hs: assert hs.ready is True assert hs.ready is False diff --git a/instruments/tests/test_hp/test_hp3456a.py b/instruments/tests/test_hp/test_hp3456a.py index eb97a2b85..302ef0f39 100644 --- a/instruments/tests/test_hp/test_hp3456a.py +++ b/instruments/tests/test_hp/test_hp3456a.py @@ -1,66 +1,52 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Unit tests for the HP 3456a digital voltmeter """ # IMPORTS ##################################################################### -from __future__ import absolute_import +import time -import quantities as pq -import numpy as np -from nose.tools import raises +import pytest import instruments as ik from instruments.tests import expected_protocol +from instruments.units import ureg as u # TESTS ####################################################################### # pylint: disable=protected-access +@pytest.fixture(autouse=True) +def time_mock(mocker): + """Mock out time to speed up.""" + return mocker.patch.object(time, "sleep", return_value=None) + + def test_hp3456a_trigger_mode(): with expected_protocol( ik.hp.HP3456a, [ "HO0T4SO1", "T4", - ], [ - "" ], - sep="\r" + [""], + sep="\r", ) as dmm: dmm.trigger_mode = dmm.TriggerMode.hold -@raises(ValueError) def test_hp3456a_number_of_digits(): - with expected_protocol( - ik.hp.HP3456a, - [ - "HO0T4SO1", - "W6STG", - "REG" - ], [ - "+06.00000E+0" - ], - sep="\r" + with pytest.raises(ValueError), expected_protocol( + ik.hp.HP3456a, ["HO0T4SO1", "W6STG", "REG"], ["+06.00000E+0"], sep="\r" ) as dmm: dmm.number_of_digits = 7 def test_hp3456a_number_of_digits_invalid(): with expected_protocol( - ik.hp.HP3456a, - [ - "HO0T4SO1", - "W6STG", - "REG" - ], [ - "+06.00000E+0" - ], - sep="\r" + ik.hp.HP3456a, ["HO0T4SO1", "W6STG", "REG"], ["+06.00000E+0"], sep="\r" ) as dmm: dmm.number_of_digits = 6 assert dmm.number_of_digits == 6 @@ -72,25 +58,16 @@ def test_hp3456a_auto_range(): [ "HO0T4SO1", "R1W", - ], [ - "" ], - sep="\r" + [""], + sep="\r", ) as dmm: dmm.auto_range() def test_hp3456a_number_of_readings(): with expected_protocol( - ik.hp.HP3456a, - [ - "HO0T4SO1", - "W10STN", - "REN" - ], [ - "+10.00000E+0" - ], - sep="\r" + ik.hp.HP3456a, ["HO0T4SO1", "W10STN", "REN"], ["+10.00000E+0"], sep="\r" ) as dmm: dmm.number_of_readings = 10 assert dmm.number_of_readings == 10 @@ -98,32 +75,15 @@ def test_hp3456a_number_of_readings(): def test_hp3456a_nplc(): with expected_protocol( - ik.hp.HP3456a, - [ - "HO0T4SO1", - "W1STI", - "REI" - ], [ - "+1.00000E+0" - ], - sep="\r" + ik.hp.HP3456a, ["HO0T4SO1", "W1STI", "REI"], ["+1.00000E+0"], sep="\r" ) as dmm: dmm.nplc = 1 assert dmm.nplc == 1 -@raises(ValueError) def test_hp3456a_nplc_invalid(): - with expected_protocol( - ik.hp.HP3456a, - [ - "HO0T4SO1", - "W1STI", - "REI" - ], [ - "+1.00000E+0" - ], - sep="\r" + with pytest.raises(ValueError), expected_protocol( + ik.hp.HP3456a, ["HO0T4SO1", "W1STI", "REI"], ["+1.00000E+0"], sep="\r" ) as dmm: dmm.nplc = 0 @@ -134,10 +94,9 @@ def test_hp3456a_mode(): [ "HO0T4SO1", "S0F4", - ], [ - "" ], - sep="\r" + [""], + sep="\r", ) as dmm: dmm.mode = dmm.Mode.resistance_2wire @@ -148,10 +107,9 @@ def test_hp3456a_math_mode(): [ "HO0T4SO1", "M2", - ], [ - "" ], - sep="\r" + [""], + sep="\r", ) as dmm: dmm.math_mode = dmm.MathMode.statistic @@ -162,10 +120,9 @@ def test_hp3456a_trigger(): [ "HO0T4SO1", "T3", - ], [ - "" ], - sep="\r" + [""], + sep="\r", ) as dmm: dmm.trigger() @@ -173,25 +130,17 @@ def test_hp3456a_trigger(): def test_hp3456a_fetch(): with expected_protocol( ik.hp.HP3456a, - [ - "HO0T4SO1" - ], + ["HO0T4SO1"], [ "+000.1055E+0,+000.1043E+0,+000.1005E+0,+000.1014E+0", - "+000.1055E+0,+000.1043E+0,+000.1005E+0,+000.1014E+0" + "+000.1055E+0,+000.1043E+0,+000.1005E+0,+000.1014E+0", ], - sep="\r" + sep="\r", ) as dmm: v = dmm.fetch(dmm.Mode.resistance_2wire) - np.testing.assert_array_equal( - v, [0.1055, 0.1043, 0.1005, 0.1014] * pq.ohm - ) - assert v[0].units == pq.ohm + assert v == [0.1055 * u.ohm, 0.1043 * u.ohm, 0.1005 * u.ohm, 0.1014 * u.ohm] v = dmm.fetch() - np.testing.assert_array_equal( - v, [0.1055, 0.1043, 0.1005, 0.1014] * pq.ohm - ) - assert isinstance(v[0], float) + assert v == [0.1055, 0.1043, 0.1005, 0.1014] def test_hp3456a_variance(): @@ -200,12 +149,11 @@ def test_hp3456a_variance(): [ "HO0T4SO1", "REV", - ], [ - "+04.93111E-6" ], - sep="\r" + ["+04.93111E-6"], + sep="\r", ) as dmm: - assert dmm.variance == +04.93111E-6 + assert dmm.variance == +04.93111e-6 def test_hp3456a_count(): @@ -214,10 +162,9 @@ def test_hp3456a_count(): [ "HO0T4SO1", "REC", - ], [ - "+10.00000E+0" ], - sep="\r" + ["+10.00000E+0"], + sep="\r", ) as dmm: assert dmm.count == +10 @@ -228,170 +175,101 @@ def test_hp3456a_mean(): [ "HO0T4SO1", "REM", - ], [ - "+102.1000E-3" ], - sep="\r" + ["+102.1000E-3"], + sep="\r", ) as dmm: - assert dmm.mean == +102.1000E-3 + assert dmm.mean == +102.1000e-3 def test_hp3456a_delay(): with expected_protocol( - ik.hp.HP3456a, - [ - "HO0T4SO1", - "RED", - "W1.0STD" - ], [ - "-000.0000E+0" - ], - sep="\r" + ik.hp.HP3456a, ["HO0T4SO1", "RED", "W1STD"], ["-000.0000E+0"], sep="\r" ) as dmm: assert dmm.delay == 0 - dmm.delay = 1 * pq.sec + dmm.delay = 1 * u.sec def test_hp3456a_lower(): with expected_protocol( - ik.hp.HP3456a, - [ - "HO0T4SO1", - "REL", - "W0.0993STL" - ], [ - "+099.3000E-3" - ], - sep="\r" + ik.hp.HP3456a, ["HO0T4SO1", "REL", "W0.0993STL"], ["+099.3000E-3"], sep="\r" ) as dmm: - assert dmm.lower == +099.3000E-3 - dmm.lower = +099.3000E-3 + assert dmm.lower == +099.3000e-3 + dmm.lower = +099.3000e-3 def test_hp3456a_upper(): with expected_protocol( - ik.hp.HP3456a, - [ - "HO0T4SO1", - "REU", - "W0.1055STU" - ], [ - "+105.5000E-3" - ], - sep="\r" + ik.hp.HP3456a, ["HO0T4SO1", "REU", "W0.1055STU"], ["+105.5000E-3"], sep="\r" ) as dmm: - assert dmm.upper == +105.5000E-3 - dmm.upper = +105.5000E-3 + assert dmm.upper == +105.5000e-3 + dmm.upper = +105.5000e-3 def test_hp3456a_ryz(): with expected_protocol( ik.hp.HP3456a, - [ - "HO0T4SO1", - "RER", - "REY", - "REZ", - "W600.0STR", - "W1.0STY", - "W0.1055STZ" - ], [ - "+0600.000E+0", - "+1.000000E+0", - "+105.5000E-3" - ], - sep="\r" + ["HO0T4SO1", "RER", "REY", "REZ", "W600.0STR", "W1.0STY", "W0.1055STZ"], + ["+0600.000E+0", "+1.000000E+0", "+105.5000E-3"], + sep="\r", ) as dmm: - assert dmm.r == +0600.000E+0 - assert dmm.y == +1.000000E+0 - assert dmm.z == +105.5000E-3 - dmm.r = +0600.000E+0 - dmm.y = +1.000000E+0 - dmm.z = +105.5000E-3 + assert dmm.r == +0600.000e0 + assert dmm.y == +1.000000e0 + assert dmm.z == +105.5000e-3 + dmm.r = +0600.000e0 + dmm.y = +1.000000e0 + dmm.z = +105.5000e-3 def test_hp3456a_measure(): with expected_protocol( ik.hp.HP3456a, - [ - "HO0T4SO1", - "S1F1W1STNT3", - "S0F4W1STNT3", - "S0F1W1STNT3", - "W1STNT3" - ], - [ - "+00.00000E-3", - "+000.1010E+0", - "+000.0002E-3", - "+000.0002E-3" - ], - sep="\r" + ["HO0T4SO1", "S1F1W1STNT3", "S0F4W1STNT3", "S0F1W1STNT3", "W1STNT3"], + ["+00.00000E-3", "+000.1010E+0", "+000.0002E-3", "+000.0002E-3"], + sep="\r", ) as dmm: assert dmm.measure(dmm.Mode.ratio_dcv_dcv) == 0 - assert dmm.measure(dmm.Mode.resistance_2wire) == +000.1010E+0 * pq.ohm - assert dmm.measure(dmm.Mode.dcv) == +000.0002E-3 * pq.volt - assert dmm.measure() == +000.0002E-3 + assert dmm.measure(dmm.Mode.resistance_2wire) == +000.1010e0 * u.ohm + assert dmm.measure(dmm.Mode.dcv) == +000.0002e-3 * u.volt + assert dmm.measure() == +000.0002e-3 def test_hp3456a_input_range(): with expected_protocol( - ik.hp.HP3456a, - [ - "HO0T4SO1", - "R2W", - "R3W" - ], [ - "" - ], - sep="\r" + ik.hp.HP3456a, ["HO0T4SO1", "R2W", "R3W"], [""], sep="\r" ) as dmm: - dmm.input_range = 10 ** -1 * pq.volt - dmm.input_range = 1e3 * pq.ohm + dmm.input_range = 10 ** -1 * u.volt + dmm.input_range = 1e3 * u.ohm + with pytest.raises(NotImplementedError): + _ = dmm.input_range -@raises(ValueError) def test_hp3456a_input_range_invalid_str(): - with expected_protocol( - ik.hp.HP3456a, - [], - [], - sep="\r" + with pytest.raises(ValueError), expected_protocol( + ik.hp.HP3456a, [], [], sep="\r" ) as dmm: dmm.input_range = "derp" -@raises(ValueError) def test_hp3456a_input_range_invalid_range(): - with expected_protocol( - ik.hp.HP3456a, - [], - [], - sep="\r" + with pytest.raises(ValueError), expected_protocol( + ik.hp.HP3456a, [], [], sep="\r" ) as dmm: - dmm.input_range = 1 * pq.ohm + dmm.input_range = 1 * u.ohm -@raises(TypeError) def test_hp3456a_input_range_bad_type(): - with expected_protocol( - ik.hp.HP3456a, - [], - [], - sep="\r" + with pytest.raises(TypeError), expected_protocol( + ik.hp.HP3456a, [], [], sep="\r" ) as dmm: dmm.input_range = True -@raises(ValueError) def test_hp3456a_input_range_bad_units(): - with expected_protocol( - ik.hp.HP3456a, - [], - [], - sep="\r" + with pytest.raises(ValueError), expected_protocol( + ik.hp.HP3456a, [], [], sep="\r" ) as dmm: - dmm.input_range = 1 * pq.amp + dmm.input_range = 1 * u.amp def test_hp3456a_relative(): @@ -401,23 +279,20 @@ def test_hp3456a_relative(): "HO0T4SO1", "M0", "M3", - ], [ + ], + [ "", ], - sep="\r" + sep="\r", ) as dmm: dmm.relative = False dmm.relative = True assert dmm.relative is True -@raises(TypeError) def test_hp3456a_relative_bad_type(): - with expected_protocol( - ik.hp.HP3456a, - [], - [], - sep="\r" + with pytest.raises(TypeError), expected_protocol( + ik.hp.HP3456a, [], [], sep="\r" ) as dmm: dmm.relative = "derp" @@ -429,10 +304,11 @@ def test_hp3456a_auto_zero(): "HO0T4SO1", "Z0", "Z1", - ], [ + ], + [ "", ], - sep="\r" + sep="\r", ) as dmm: dmm.autozero = False dmm.autozero = True @@ -445,43 +321,32 @@ def test_hp3456a_filter(): "HO0T4SO1", "FL0", "FL1", - ], [ + ], + [ "", ], - sep="\r" + sep="\r", ) as dmm: dmm.filter = False dmm.filter = True -@raises(TypeError) def test_hp3456a_register_read_bad_name(): - with expected_protocol( - ik.hp.HP3456a, - [], - [], - sep="\r" + with pytest.raises(TypeError), expected_protocol( + ik.hp.HP3456a, [], [], sep="\r" ) as dmm: dmm._register_read("foobar") -@raises(TypeError) def test_hp3456a_register_write_bad_name(): - with expected_protocol( - ik.hp.HP3456a, - [], - [], - sep="\r" + with pytest.raises(TypeError), expected_protocol( + ik.hp.HP3456a, [], [], sep="\r" ) as dmm: dmm._register_write("foobar", 1) -@raises(ValueError) def test_hp3456a_register_write_bad_register(): - with expected_protocol( - ik.hp.HP3456a, - [], - [], - sep="\r" + with pytest.raises(ValueError), expected_protocol( + ik.hp.HP3456a, [], [], sep="\r" ) as dmm: dmm._register_write(dmm.Register.mean, 1) diff --git a/instruments/tests/test_hp/test_hp6624a.py b/instruments/tests/test_hp/test_hp6624a.py index 93208b728..3e4539519 100644 --- a/instruments/tests/test_hp/test_hp6624a.py +++ b/instruments/tests/test_hp/test_hp6624a.py @@ -1,19 +1,19 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Unit tests for the HP 6624a power supply """ # IMPORTS ##################################################################### -from __future__ import absolute_import - -import quantities as pq -import mock -from nose.tools import raises +import pytest import instruments as ik -from instruments.tests import expected_protocol +from instruments.tests import ( + expected_protocol, + iterable_eq, +) +from instruments.units import ureg as u +from .. import mock # TESTS ####################################################################### @@ -21,12 +21,7 @@ def test_channel_returns_inner_class(): - with expected_protocol( - ik.hp.HP6624a, - [], - [], - sep="\n" - ) as hp: + with expected_protocol(ik.hp.HP6624a, [], [], sep="\n") as hp: channel = hp.channel[0] assert isinstance(channel, hp.Channel) is True assert channel._idx == 1 @@ -58,50 +53,35 @@ def test_channel_query(): assert value == "FOO" +def test_mode(): + """Raise NotImplementedError when mode is called.""" + with expected_protocol(ik.hp.HP6624a, [], [], sep="\n") as hp: + channel = hp.channel[0] + with pytest.raises(NotImplementedError): + _ = channel.mode + with pytest.raises(NotImplementedError): + channel.mode = 42 + + def test_channel_voltage(): with expected_protocol( - ik.hp.HP6624a, - [ - "VSET? 1", - "VSET 1,{:.1f}".format(5) - ], - [ - "2" - ], - sep="\n" + ik.hp.HP6624a, ["VSET? 1", f"VSET 1,{5:.1f}"], ["2"], sep="\n" ) as hp: - assert hp.channel[0].voltage == 2 * pq.V - hp.channel[0].voltage = 5 * pq.V + assert hp.channel[0].voltage == 2 * u.V + hp.channel[0].voltage = 5 * u.V def test_channel_current(): with expected_protocol( - ik.hp.HP6624a, - [ - "ISET? 1", - "ISET 1,{:.1f}".format(5) - ], - [ - "2" - ], - sep="\n" + ik.hp.HP6624a, ["ISET? 1", f"ISET 1,{5:.1f}"], ["2"], sep="\n" ) as hp: - assert hp.channel[0].current == 2 * pq.amp - hp.channel[0].current = 5 * pq.amp + assert hp.channel[0].current == 2 * u.amp + hp.channel[0].current = 5 * u.amp def test_channel_voltage_sense(): - with expected_protocol( - ik.hp.HP6624a, - [ - "VOUT? 1" - ], - [ - "2" - ], - sep="\n" - ) as hp: - assert hp.channel[0].voltage_sense == 2 * pq.V + with expected_protocol(ik.hp.HP6624a, ["VOUT? 1"], ["2"], sep="\n") as hp: + assert hp.channel[0].voltage_sense == 2 * u.V def test_channel_current_sense(): @@ -110,58 +90,28 @@ def test_channel_current_sense(): [ "IOUT? 1", ], - [ - "2" - ], - sep="\n" + ["2"], + sep="\n", ) as hp: - assert hp.channel[0].current_sense == 2 * pq.A + assert hp.channel[0].current_sense == 2 * u.A def test_channel_overvoltage(): with expected_protocol( - ik.hp.HP6624a, - [ - "OVSET? 1", - "OVSET 1,{:.1f}".format(5) - ], - [ - "2" - ], - sep="\n" + ik.hp.HP6624a, ["OVSET? 1", f"OVSET 1,{5:.1f}"], ["2"], sep="\n" ) as hp: - assert hp.channel[0].overvoltage == 2 * pq.V - hp.channel[0].overvoltage = 5 * pq.V + assert hp.channel[0].overvoltage == 2 * u.V + hp.channel[0].overvoltage = 5 * u.V def test_channel_overcurrent(): - with expected_protocol( - ik.hp.HP6624a, - [ - "OVP? 1", - "OVP 1,1" - ], - [ - "1" - ], - sep="\n" - ) as hp: + with expected_protocol(ik.hp.HP6624a, ["OVP? 1", "OVP 1,1"], ["1"], sep="\n") as hp: assert hp.channel[0].overcurrent is True hp.channel[0].overcurrent = True def test_channel_output(): - with expected_protocol( - ik.hp.HP6624a, - [ - "OUT? 1", - "OUT 1,1" - ], - [ - "1" - ], - sep="\n" - ) as hp: + with expected_protocol(ik.hp.HP6624a, ["OUT? 1", "OUT 1,1"], ["1"], sep="\n") as hp: assert hp.channel[0].output is True hp.channel[0].output = True @@ -182,39 +132,29 @@ def test_all_voltage(): "VSET? 2", "VSET? 3", "VSET? 4", - - "VSET 1,{:.1f}".format(5), - "VSET 2,{:.1f}".format(5), - "VSET 3,{:.1f}".format(5), - "VSET 4,{:.1f}".format(5), - - "VSET 1,{:.1f}".format(1), - "VSET 2,{:.1f}".format(2), - "VSET 3,{:.1f}".format(3), - "VSET 4,{:.1f}".format(4) - ], - [ - "2", - "3", - "4", - "5" + f"VSET 1,{5:.1f}", + f"VSET 2,{5:.1f}", + f"VSET 3,{5:.1f}", + f"VSET 4,{5:.1f}", + f"VSET 1,{1:.1f}", + f"VSET 2,{2:.1f}", + f"VSET 3,{3:.1f}", + f"VSET 4,{4:.1f}", ], - sep="\n" + ["2", "3", "4", "5"], + sep="\n", ) as hp: - assert sorted(hp.voltage) == sorted((2, 3, 4, 5) * pq.V) - hp.voltage = 5 * pq.V - hp.voltage = (1 * pq.V, 2 * pq.V, 3 * pq.V, 4 * pq.V) + expected = (2 * u.V, 3 * u.V, 4 * u.V, 5 * u.V) + iterable_eq(hp.voltage, expected) + hp.voltage = 5 * u.V + hp.voltage = (1 * u.V, 2 * u.V, 3 * u.V, 4 * u.V) -@raises(ValueError) def test_all_voltage_wrong_length(): - with expected_protocol( - ik.hp.HP6624a, - [], - [], - sep="\n" + with pytest.raises(ValueError), expected_protocol( + ik.hp.HP6624a, [], [], sep="\n" ) as hp: - hp.voltage = (1 * pq.volt, 2 * pq.volt) + hp.voltage = (1 * u.volt, 2 * u.volt) def test_all_current(): @@ -225,121 +165,73 @@ def test_all_current(): "ISET? 2", "ISET? 3", "ISET? 4", - - "ISET 1,{:.1f}".format(5), - "ISET 2,{:.1f}".format(5), - "ISET 3,{:.1f}".format(5), - "ISET 4,{:.1f}".format(5), - - "ISET 1,{:.1f}".format(1), - "ISET 2,{:.1f}".format(2), - "ISET 3,{:.1f}".format(3), - "ISET 4,{:.1f}".format(4) + f"ISET 1,{5:.1f}", + f"ISET 2,{5:.1f}", + f"ISET 3,{5:.1f}", + f"ISET 4,{5:.1f}", + f"ISET 1,{1:.1f}", + f"ISET 2,{2:.1f}", + f"ISET 3,{3:.1f}", + f"ISET 4,{4:.1f}", ], - [ - "2", - "3", - "4", - "5" - ], - sep="\n" + ["2", "3", "4", "5"], + sep="\n", ) as hp: - assert sorted(hp.current) == sorted((2, 3, 4, 5) * pq.A) - hp.current = 5 * pq.A - hp.current = (1 * pq.A, 2 * pq.A, 3 * pq.A, 4 * pq.A) + expected = (2 * u.A, 3 * u.A, 4 * u.A, 5 * u.A) + iterable_eq(hp.current, expected) + hp.current = 5 * u.A + hp.current = (1 * u.A, 2 * u.A, 3 * u.A, 4 * u.A) -@raises(ValueError) def test_all_current_wrong_length(): - with expected_protocol( - ik.hp.HP6624a, - [], - [], - sep="\n" + with pytest.raises(ValueError), expected_protocol( + ik.hp.HP6624a, [], [], sep="\n" ) as hp: - hp.current = (1 * pq.amp, 2 * pq.amp) + hp.current = (1 * u.amp, 2 * u.amp) def test_all_voltage_sense(): with expected_protocol( ik.hp.HP6624a, - [ - "VOUT? 1", - "VOUT? 2", - "VOUT? 3", - "VOUT? 4" - ], - [ - "2", - "3", - "4", - "5" - ], - sep="\n" + ["VOUT? 1", "VOUT? 2", "VOUT? 3", "VOUT? 4"], + ["2", "3", "4", "5"], + sep="\n", ) as hp: - assert sorted(hp.voltage_sense) == sorted((2, 3, 4, 5) * pq.V) + expected = (2 * u.V, 3 * u.V, 4 * u.V, 5 * u.V) + iterable_eq(hp.voltage_sense, expected) def test_all_current_sense(): with expected_protocol( ik.hp.HP6624a, - [ - "IOUT? 1", - "IOUT? 2", - "IOUT? 3", - "IOUT? 4" - ], - [ - "2", - "3", - "4", - "5" - ], - sep="\n" + ["IOUT? 1", "IOUT? 2", "IOUT? 3", "IOUT? 4"], + ["2", "3", "4", "5"], + sep="\n", ) as hp: - assert sorted(hp.current_sense) == sorted((2, 3, 4, 5) * pq.A) + expected = (2 * u.A, 3 * u.A, 4 * u.A, 5 * u.A) + iterable_eq(hp.current_sense, expected) def test_clear(): - with expected_protocol( - ik.hp.HP6624a, - [ - "CLR" - ], - [], - sep="\n" - ) as hp: + with expected_protocol(ik.hp.HP6624a, ["CLR"], [], sep="\n") as hp: hp.clear() def test_channel_count(): - with expected_protocol( - ik.hp.HP6624a, - [], - [], - sep="\n" - ) as hp: + with expected_protocol(ik.hp.HP6624a, [], [], sep="\n") as hp: assert hp.channel_count == 4 hp.channel_count = 3 -@raises(TypeError) def test_channel_count_wrong_type(): - with expected_protocol( - ik.hp.HP6624a, - [], - [], - sep="\n" + with pytest.raises(TypeError), expected_protocol( + ik.hp.HP6624a, [], [], sep="\n" ) as hp: hp.channel_count = "foobar" -@raises(ValueError) def test_channel_count_too_small(): - with expected_protocol( - ik.hp.HP6624a, - [], - [], - sep="\n" + with pytest.raises(ValueError), expected_protocol( + ik.hp.HP6624a, [], [], sep="\n" ) as hp: hp.channel_count = 0 diff --git a/instruments/tests/test_hp/test_hp6632b.py b/instruments/tests/test_hp/test_hp6632b.py index afe215085..7921e5509 100644 --- a/instruments/tests/test_hp/test_hp6632b.py +++ b/instruments/tests/test_hp/test_hp6632b.py @@ -1,14 +1,13 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Unit tests for the HP 6632b power supply """ # IMPORTS ##################################################################### -from __future__ import absolute_import +import pytest -import quantities as pq +from instruments.units import ureg as u import instruments as ik from instruments.tests import expected_protocol, make_name_test, unit_eq @@ -20,13 +19,7 @@ def test_hp6632b_display_textmode(): with expected_protocol( - ik.hp.HP6632b, - [ - "DISP:MODE?", - "DISP:MODE TEXT" - ], [ - "NORM" - ] + ik.hp.HP6632b, ["DISP:MODE?", "DISP:MODE TEXT"], ["NORM"] ) as psu: assert psu.display_textmode is False psu.display_textmode = True @@ -34,43 +27,22 @@ def test_hp6632b_display_textmode(): def test_hp6632b_display_text(): with expected_protocol( - ik.hp.HP6632b, - [ - 'DISP:TEXT "TEST"', - 'DISP:TEXT "TEST AAAAAAAAAA"' - ], - [] + ik.hp.HP6632b, ['DISP:TEXT "TEST"', 'DISP:TEXT "TEST AAAAAAAAAA"'], [] ) as psu: assert psu.display_text("TEST") == "TEST" assert psu.display_text("TEST AAAAAAAAAAAAAAAA") == "TEST AAAAAAAAAA" def test_hp6632b_output(): - with expected_protocol( - ik.hp.HP6632b, - [ - "OUTP?", - "OUTP 1" - ], [ - "0" - ] - ) as psu: + with expected_protocol(ik.hp.HP6632b, ["OUTP?", "OUTP 1"], ["0"]) as psu: assert psu.output is False psu.output = True def test_hp6632b_voltage(): - with expected_protocol( - ik.hp.HP6632b, - [ - "VOLT?", - "VOLT {:e}".format(1) - ], [ - "10.0" - ] - ) as psu: - unit_eq(psu.voltage, 10 * pq.volt) - psu.voltage = 1.0 * pq.volt + with expected_protocol(ik.hp.HP6632b, ["VOLT?", f"VOLT {1:e}"], ["10.0"]) as psu: + unit_eq(psu.voltage, 10 * u.volt) + psu.voltage = 1.0 * u.volt def test_hp6632b_voltage_sense(): @@ -78,39 +50,24 @@ def test_hp6632b_voltage_sense(): ik.hp.HP6632b, [ "MEAS:VOLT?", - ], [ - "10.0" - ] + ], + ["10.0"], ) as psu: - unit_eq(psu.voltage_sense, 10 * pq.volt) + unit_eq(psu.voltage_sense, 10 * u.volt) def test_hp6632b_overvoltage(): with expected_protocol( - ik.hp.HP6632b, - [ - "VOLT:PROT?", - "VOLT:PROT {:e}".format(1) - ], [ - "10.0" - ] + ik.hp.HP6632b, ["VOLT:PROT?", f"VOLT:PROT {1:e}"], ["10.0"] ) as psu: - unit_eq(psu.overvoltage, 10 * pq.volt) - psu.overvoltage = 1.0 * pq.volt + unit_eq(psu.overvoltage, 10 * u.volt) + psu.overvoltage = 1.0 * u.volt def test_hp6632b_current(): - with expected_protocol( - ik.hp.HP6632b, - [ - "CURR?", - "CURR {:e}".format(1) - ], [ - "10.0" - ] - ) as psu: - unit_eq(psu.current, 10 * pq.amp) - psu.current = 1.0 * pq.amp + with expected_protocol(ik.hp.HP6632b, ["CURR?", f"CURR {1:e}"], ["10.0"]) as psu: + unit_eq(psu.current, 10 * u.amp) + psu.current = 1.0 * u.amp def test_hp6632b_current_sense(): @@ -118,22 +75,15 @@ def test_hp6632b_current_sense(): ik.hp.HP6632b, [ "MEAS:CURR?", - ], [ - "10.0" - ] + ], + ["10.0"], ) as psu: - unit_eq(psu.current_sense, 10 * pq.amp) + unit_eq(psu.current_sense, 10 * u.amp) def test_hp6632b_overcurrent(): with expected_protocol( - ik.hp.HP6632b, - [ - "CURR:PROT:STAT?", - "CURR:PROT:STAT 1" - ], [ - "0" - ] + ik.hp.HP6632b, ["CURR:PROT:STAT?", "CURR:PROT:STAT 1"], ["0"] ) as psu: assert psu.overcurrent is False psu.overcurrent = True @@ -141,27 +91,15 @@ def test_hp6632b_overcurrent(): def test_hp6632b_current_sense_range(): with expected_protocol( - ik.hp.HP6632b, - [ - "SENS:CURR:RANGE?", - "SENS:CURR:RANGE {:e}".format(1) - ], [ - "0.05" - ] + ik.hp.HP6632b, ["SENS:CURR:RANGE?", f"SENS:CURR:RANGE {1:e}"], ["0.05"] ) as psu: - unit_eq(psu.current_sense_range, 0.05 * pq.amp) - psu.current_sense_range = 1 * pq.amp + unit_eq(psu.current_sense_range, 0.05 * u.amp) + psu.current_sense_range = 1 * u.amp def test_hp6632b_output_dfi_source(): with expected_protocol( - ik.hp.HP6632b, - [ - "OUTP:DFI:SOUR?", - "OUTP:DFI:SOUR QUES" - ], [ - "OPER" - ] + ik.hp.HP6632b, ["OUTP:DFI:SOUR?", "OUTP:DFI:SOUR QUES"], ["OPER"] ) as psu: assert psu.output_dfi_source == psu.DFISource.operation psu.output_dfi_source = psu.DFISource.questionable @@ -169,13 +107,7 @@ def test_hp6632b_output_dfi_source(): def test_hp6632b_output_remote_inhibit(): with expected_protocol( - ik.hp.HP6632b, - [ - "OUTP:RI:MODE?", - "OUTP:RI:MODE LATC" - ], [ - "LIVE" - ] + ik.hp.HP6632b, ["OUTP:RI:MODE?", "OUTP:RI:MODE LATC"], ["LIVE"] ) as psu: assert psu.output_remote_inhibit == psu.RemoteInhibit.live psu.output_remote_inhibit = psu.RemoteInhibit.latching @@ -183,41 +115,21 @@ def test_hp6632b_output_remote_inhibit(): def test_hp6632b_digital_function(): with expected_protocol( - ik.hp.HP6632b, - [ - "DIG:FUNC?", - "DIG:FUNC DIG" - ], [ - "RIDF" - ] + ik.hp.HP6632b, ["DIG:FUNC?", "DIG:FUNC DIG"], ["RIDF"] ) as psu: assert psu.digital_function == psu.DigitalFunction.remote_inhibit psu.digital_function = psu.DigitalFunction.data def test_hp6632b_digital_data(): - with expected_protocol( - ik.hp.HP6632b, - [ - "DIG:DATA?", - "DIG:DATA 1" - ], [ - "5" - ] - ) as psu: + with expected_protocol(ik.hp.HP6632b, ["DIG:DATA?", "DIG:DATA 1"], ["5"]) as psu: assert psu.digital_data == 5 psu.digital_data = 1 def test_hp6632b_sense_sweep_points(): with expected_protocol( - ik.hp.HP6632b, - [ - "SENS:SWE:POIN?", - "SENS:SWE:POIN {:e}".format(2048) - ], [ - "5" - ] + ik.hp.HP6632b, ["SENS:SWE:POIN?", f"SENS:SWE:POIN {2048:e}"], ["5"] ) as psu: assert psu.sense_sweep_points == 5 psu.sense_sweep_points = 2048 @@ -226,26 +138,16 @@ def test_hp6632b_sense_sweep_points(): def test_hp6632b_sense_sweep_interval(): with expected_protocol( ik.hp.HP6632b, - [ - "SENS:SWE:TINT?", - "SENS:SWE:TINT {:e}".format(1e-05) - ], [ - "1.56e-05" - ] + ["SENS:SWE:TINT?", f"SENS:SWE:TINT {1e-05:e}"], + ["1.56e-05"], ) as psu: - unit_eq(psu.sense_sweep_interval, 1.56e-05 * pq.second) - psu.sense_sweep_interval = 1e-05 * pq.second + unit_eq(psu.sense_sweep_interval, 1.56e-05 * u.second) + psu.sense_sweep_interval = 1e-05 * u.second def test_hp6632b_sense_window(): with expected_protocol( - ik.hp.HP6632b, - [ - "SENS:WIND?", - "SENS:WIND RECT" - ], [ - "HANN" - ] + ik.hp.HP6632b, ["SENS:WIND?", "SENS:WIND RECT"], ["HANN"] ) as psu: assert psu.sense_window == psu.SenseWindow.hanning psu.sense_window = psu.SenseWindow.rectangular @@ -253,16 +155,10 @@ def test_hp6632b_sense_window(): def test_hp6632b_output_protection_delay(): with expected_protocol( - ik.hp.HP6632b, - [ - "OUTP:PROT:DEL?", - "OUTP:PROT:DEL {:e}".format(5e-02) - ], [ - "8e-02" - ] + ik.hp.HP6632b, ["OUTP:PROT:DEL?", f"OUTP:PROT:DEL {5e-02:e}"], ["8e-02"] ) as psu: - unit_eq(psu.output_protection_delay, 8e-02 * pq.second) - psu.output_protection_delay = 5e-02 * pq.second + unit_eq(psu.output_protection_delay, 8e-02 * u.second) + psu.output_protection_delay = 5e-02 * u.second def test_hp6632b_voltage_alc_bandwidth(): @@ -270,39 +166,26 @@ def test_hp6632b_voltage_alc_bandwidth(): ik.hp.HP6632b, [ "VOLT:ALC:BAND?", - ], [ - "6e4" - ] + ], + ["6e4"], ) as psu: assert psu.voltage_alc_bandwidth == psu.ALCBandwidth.fast def test_hp6632b_voltage_trigger(): with expected_protocol( - ik.hp.HP6632b, - [ - "VOLT:TRIG?", - "VOLT:TRIG {:e}".format(1) - ], [ - "1e+0" - ] + ik.hp.HP6632b, ["VOLT:TRIG?", f"VOLT:TRIG {1:e}"], ["1e+0"] ) as psu: - unit_eq(psu.voltage_trigger, 1 * pq.volt) - psu.voltage_trigger = 1 * pq.volt + unit_eq(psu.voltage_trigger, 1 * u.volt) + psu.voltage_trigger = 1 * u.volt def test_hp6632b_current_trigger(): with expected_protocol( - ik.hp.HP6632b, - [ - "CURR:TRIG?", - "CURR:TRIG {:e}".format(0.1) - ], [ - "1e-01" - ] + ik.hp.HP6632b, ["CURR:TRIG?", f"CURR:TRIG {0.1:e}"], ["1e-01"] ) as psu: - unit_eq(psu.current_trigger, 0.1 * pq.amp) - psu.current_trigger = 0.1 * pq.amp + unit_eq(psu.current_trigger, 0.1 * u.amp) + psu.current_trigger = 0.1 * u.amp def test_hp6632b_init_output_trigger(): @@ -311,7 +194,7 @@ def test_hp6632b_init_output_trigger(): [ "INIT:NAME TRAN", ], - [] + [], ) as psu: psu.init_output_trigger() @@ -322,23 +205,46 @@ def test_hp6632b_abort_output_trigger(): [ "ABORT", ], - [] + [], ) as psu: psu.abort_output_trigger() +def test_line_frequency(): + """Raise NotImplemented error when called.""" + with expected_protocol(ik.hp.HP6632b, [], []) as psu: + with pytest.raises(NotImplementedError): + psu.line_frequency = 42 + with pytest.raises(NotImplementedError): + _ = psu.line_frequency + + +def test_display_brightness(): + """Raise NotImplemented error when called.""" + with expected_protocol(ik.hp.HP6632b, [], []) as psu: + with pytest.raises(NotImplementedError): + psu.display_brightness = 42 + with pytest.raises(NotImplementedError): + _ = psu.display_brightness + + +def test_display_contrast(): + """Raise NotImplemented error when called.""" + with expected_protocol(ik.hp.HP6632b, [], []) as psu: + with pytest.raises(NotImplementedError): + psu.display_contrast = 42 + with pytest.raises(NotImplementedError): + _ = psu.display_contrast + + def test_hp6632b_check_error_queue(): with expected_protocol( ik.hp.HP6632b, [ "SYST:ERR?", "SYST:ERR?", - ], [ - '-222,"Data out of range"', - '+0,"No error"' - ] + ], + ['-222,"Data out of range"', '+0,"No error"'], ) as psu: err_queue = psu.check_error_queue() - assert err_queue == [ - psu.ErrorCodes.data_out_of_range - ], "got {}".format(err_queue) + assert err_queue == [psu.ErrorCodes.data_out_of_range], f"got {err_queue}" diff --git a/instruments/tests/test_hp/test_hp6652a.py b/instruments/tests/test_hp/test_hp6652a.py index a5d1cd13c..7b509ef80 100644 --- a/instruments/tests/test_hp/test_hp6652a.py +++ b/instruments/tests/test_hp/test_hp6652a.py @@ -1,12 +1,11 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Unit tests for the HP 6652a single output power supply """ # IMPORTS ##################################################################### -from __future__ import absolute_import +import pytest import instruments as ik from instruments.tests import expected_protocol @@ -16,49 +15,34 @@ def test_name(): with expected_protocol( - ik.hp.HP6652a, - [ - "*IDN?" - ], - [ - "FOO,BAR,AAA,BBBB" - ], - sep="\n" + ik.hp.HP6652a, ["*IDN?"], ["FOO,BAR,AAA,BBBB"], sep="\n" ) as hp: assert hp.name == "FOO BAR" +def test_mode(): + """Raise NotImplementedError when called.""" + with expected_protocol(ik.hp.HP6652a, [], [], sep="\n") as hp: + with pytest.raises(NotImplementedError): + _ = hp.mode + with pytest.raises(NotImplementedError): + hp.mode = 42 + + def test_reset(): - with expected_protocol( - ik.hp.HP6652a, - [ - "OUTP:PROT:CLE" - ], - [], - sep="\n" - ) as hp: + with expected_protocol(ik.hp.HP6652a, ["OUTP:PROT:CLE"], [], sep="\n") as hp: hp.reset() def test_display_text(): with expected_protocol( - ik.hp.HP6652a, - [ - 'DISP:TEXT "TEST"', - 'DISP:TEXT "TEST AAAAAAAAAA"' - ], - [] + ik.hp.HP6652a, ['DISP:TEXT "TEST"', 'DISP:TEXT "TEST AAAAAAAAAA"'], [] ) as psu: assert psu.display_text("TEST") == "TEST" assert psu.display_text("TEST AAAAAAAAAAAAAAAA") == "TEST AAAAAAAAAA" def test_channel(): - with expected_protocol( - ik.hp.HP6652a, - [], - [], - sep="\n" - ) as hp: + with expected_protocol(ik.hp.HP6652a, [], [], sep="\n") as hp: assert hp.channel[0] == hp assert len(hp.channel) == 1 diff --git a/instruments/tests/test_hp/test_hpe3631a.py b/instruments/tests/test_hp/test_hpe3631a.py new file mode 100644 index 000000000..99a9f11b5 --- /dev/null +++ b/instruments/tests/test_hp/test_hpe3631a.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python +""" +Module containing tests for the HP E3631A power supply +""" + +# IMPORTS ##################################################################### + +import time + +import pytest + +from instruments.units import ureg as u + +import instruments as ik +from instruments.tests import expected_protocol + + +# TESTS ####################################################################### + + +@pytest.fixture(autouse=True) +def time_mock(mocker): + """Mock out time such that the tests go faster.""" + return mocker.patch.object(time, "sleep", return_value=None) + + +def test_channel(): + with expected_protocol( + ik.hp.HPe3631a, + ["SYST:REM", "INST:NSEL?", "INST:NSEL?", "INST:NSEL 2", "INST:NSEL?"], + ["1", "1", "2"], + ) as inst: + assert inst.channelid == 1 + assert inst.channel[2] == inst + assert inst.channelid == 2 + assert inst.channel.__len__() == len([1, 2, 3]) # len of valild set + + +def test_channelid(): + with expected_protocol( + ik.hp.HPe3631a, + ["SYST:REM", "INST:NSEL?", "INST:NSEL 2", "INST:NSEL?"], # 0 # 1 # 2 # 3 + ["1", "2"], # 1 # 3 + ) as inst: + assert inst.channelid == 1 + inst.channelid = 2 + assert inst.channelid == 2 + + +def test_mode(): + """Raise AttributeError since instrument sets mode automatically.""" + with expected_protocol(ik.hp.HPe3631a, ["SYST:REM"], []) as inst: + with pytest.raises(AttributeError) as err_info: + _ = inst.mode() + err_msg = err_info.value.args[0] + assert err_msg == "The `HPe3631a` sets its mode automatically" + + +def test_voltage(): + with expected_protocol( + ik.hp.HPe3631a, + [ + "SYST:REM", # 0 + "SOUR:VOLT? MAX", # 1 + "SOUR:VOLT? MAX", # 2 + "SOUR:VOLT? MAX", # 3.1 + "SOUR:VOLT 3.000000e+00", # 3.2 + "SOUR:VOLT?", # 4 + "SOUR:VOLT? MAX", # 5 + "SOUR:VOLT? MAX", # 6 + ], + ["6.0", "6.0", "6.0", "3.0", "6.0", "6.0"], # 1 # 2 # 3.1 # 4 # 5 # 6 + ) as inst: + assert inst.voltage_min == 0.0 * u.volt + assert inst.voltage_max == 6.0 * u.volt + inst.voltage = 3.0 * u.volt + assert inst.voltage == 3.0 * u.volt + with pytest.raises(ValueError) as err_info: + newval = -1.0 * u.volt + inst.voltage = newval + err_msg = err_info.value.args[0] + assert ( + err_msg == f"Voltage quantity is too low. Got {newval}, " + f"minimum value is {0.}" + ) + with pytest.raises(ValueError) as err_info: + newval = 7.0 * u.volt + inst.voltage = newval + err_msg = err_info.value.args[0] + assert ( + err_msg == f"Voltage quantity is too high. Got {newval}, " + f"maximum value is {u.Quantity(6.0, u.V)}" + ) + + +def test_voltage_range_negative(): + """Get voltage max if negative.""" + max_volts = -6.0 + with expected_protocol( + ik.hp.HPe3631a, + ["SYST:REM", "SOUR:VOLT? MAX"], # 0 # 1 + [ + f"{max_volts}", # 1 + ], + ) as inst: + expected_value = u.Quantity(max_volts, u.V), 0.0 + received_value = inst.voltage_range + assert expected_value == received_value + + +def test_current(): + with expected_protocol( + ik.hp.HPe3631a, + [ + "SYST:REM", # 0 + "SOUR:CURR? MIN", # 1.1 + "SOUR:CURR? MAX", # 1.2 + "SOUR:CURR? MIN", # 2.1 + "SOUR:CURR? MAX", # 2.2 + "SOUR:CURR 2.000000e+00", # 3 + "SOUR:CURR?", # 4 + "SOUR:CURR? MIN", # 5 + "SOUR:CURR? MIN", # 6.1 + "SOUR:CURR? MAX", # 6.2 + ], + [ + "0.0", # 1.1 + "5.0", # 1.2 + "0.0", # 2.1 + "5.0", # 2.2 + "2.0", # 4 + "0.0", # 5 + "0.0", # 6.1 + "5.0", # 6.2 + ], + ) as inst: + assert inst.current_min == 0.0 * u.amp + assert inst.current_max == 5.0 * u.amp + inst.current = 2.0 * u.amp + assert inst.current == 2.0 * u.amp + try: + inst.current = -1.0 * u.amp + except ValueError: + pass + try: + inst.current = 6.0 * u.amp + except ValueError: + pass + + +def test_voltage_sense(): + with expected_protocol( + ik.hp.HPe3631a, ["SYST:REM", "MEAS:VOLT?"], ["1.234"] # 0 # 1 # 1 + ) as inst: + assert inst.voltage_sense == 1.234 * u.volt + + +def test_current_sense(): + with expected_protocol( + ik.hp.HPe3631a, ["SYST:REM", "MEAS:CURR?"], ["1.234"] # 0 # 1 # 1 + ) as inst: + assert inst.current_sense == 1.234 * u.amp diff --git a/instruments/tests/test_keithley/test_keithley195.py b/instruments/tests/test_keithley/test_keithley195.py new file mode 100644 index 000000000..01e2adac8 --- /dev/null +++ b/instruments/tests/test_keithley/test_keithley195.py @@ -0,0 +1,341 @@ +#!/usr/bin/env python +""" +Module containing tests for the Keithley 195 digital multimeter. +""" + +# IMPORTS #################################################################### + +import struct +import time + +from hypothesis import ( + given, + strategies as st, +) +import pytest + +import instruments as ik +from instruments.tests import expected_protocol +from instruments.units import ureg as u + +# TESTS ###################################################################### + + +# pylint: disable=redefined-outer-name + + +# PYTEST FIXTURES FOR INITIALIZATION # + + +@pytest.fixture +def init(): + """Returns the initialization command that is sent to instrument.""" + return "YX\nG1DX" + + +@pytest.fixture +def statusword(): + """Return a standard statusword for the status of the instrument.""" + trigger = b"1" # talk_one_shot + mode = b"2" # resistance + range = b"3" # 2kOhm in resistance mode + eoi = b"1" # disabled + buffer = b"3" # reading done, currently unused + rate = b"5" # Line cycle integration + srqmode = b"0" # disabled + relative = b"1" # relative mode is activated + delay = b"0" # no delay, currently unused + multiplex = b"0" # multiplex enabled + selftest = b"2" # self test successful, currently unused + dataformat = b"1" # Readings without prefix/suffix. + datacontrol = b"0" # Readings without prefix/suffix. + filter = b"0" # filter disabled + terminator = b"1" + + statusword_p1 = b"195 " # sends a space after 195! + statusword_p2 = struct.pack( + "@4c2s3c2s5c2s", + trigger, + mode, + range, + eoi, + buffer, + rate, + srqmode, + relative, + delay, + multiplex, + selftest, + dataformat, + datacontrol, + filter, + terminator, + ) + return statusword_p1 + statusword_p2 + + +# TEST INSTRUMENT # + + +def test_keithley195_mode(init, statusword): + """Get / set the measurement mode.""" + with expected_protocol( + ik.keithley.Keithley195, [init, "F2DX", "U0DX"], [statusword], sep="\n" + ) as mul: + mul.mode = mul.Mode.resistance + assert mul.mode == mul.Mode.resistance + + +def test_keithley195_mode_string(init, statusword): + """Get / set the measurement mode using a string.""" + with expected_protocol( + ik.keithley.Keithley195, [init, "F2DX", "U0DX"], [statusword], sep="\n" + ) as mul: + mul.mode = "resistance" + assert mul.mode == mul.Mode.resistance + + +def test_keithley195_mode_type_error(init): + """Raise type error when setting the mode with the wrong type.""" + wrong_type = 42 + with expected_protocol(ik.keithley.Keithley195, [init], [], sep="\n") as mul: + with pytest.raises(TypeError) as err_info: + mul.mode = wrong_type + err_msg = err_info.value.args[0] + assert ( + err_msg == f"Mode must be specified as a Keithley195.Mode " + f"value, got {wrong_type} instead." + ) + + +def test_keithley195_trigger_mode(init, statusword): + """Get / set the trigger mode.""" + with expected_protocol( + ik.keithley.Keithley195, [init, "T1X", "U0DX"], [statusword], sep="\n" + ) as mul: + mul.trigger_mode = mul.TriggerMode.talk_one_shot + assert mul.trigger_mode == mul.TriggerMode.talk_one_shot + + +def test_keithley195_trigger_mode_string(init, statusword): + """Get / set the trigger using a string.""" + with expected_protocol( + ik.keithley.Keithley195, [init, "T1X", "U0DX"], [statusword], sep="\n" + ) as mul: + mul.trigger_mode = "talk_one_shot" + assert mul.trigger_mode == mul.TriggerMode.talk_one_shot + + +def test_keithley195_trigger_mode_type_error(init): + """Raise type error when setting the trigger mode with the wrong type.""" + wrong_type = 42 + with expected_protocol(ik.keithley.Keithley195, [init], [], sep="\n") as mul: + with pytest.raises(TypeError) as err_info: + mul.trigger_mode = wrong_type + err_msg = err_info.value.args[0] + assert ( + err_msg == f"Drive must be specified as a " + f"Keithley195.TriggerMode, got {wrong_type} instead." + ) + + +def test_keithley195_relative(init, statusword): + """Get / set the relative mode""" + with expected_protocol( + ik.keithley.Keithley195, [init, "Z0DX", "Z1DX", "U0DX"], [statusword], sep="\n" + ) as mul: + mul.relative = False + mul.relative = True + assert mul.relative + + +def test_keithley195_relative_type_error(init): + """Raise type error when setting relative non-bool.""" + wrong_type = 42 + with expected_protocol( + ik.keithley.Keithley195, + [ + init, + ], + [], + sep="\n", + ) as mul: + with pytest.raises(TypeError) as err_info: + mul.relative = wrong_type + err_msg = err_info.value.args[0] + assert err_msg == "Relative mode must be a boolean." + + +@pytest.mark.parametrize("range", ik.keithley.Keithley195.ValidRange.resistance.value) +def test_keithley195_input_range(init, statusword, range): + """Get / set input range. + + Set unitful and w/o units. + """ + mode = ik.keithley.Keithley195.Mode(int(statusword.decode()[5])) + index = ik.keithley.Keithley195.ValidRange[mode.name].value.index(range) + # new statusword + new_statusword = list(statusword.decode()) + new_statusword[6] = str(index + 1) + new_statusword = "".join(new_statusword) + # units + units = ik.keithley.keithley195.UNITS2[mode] + with expected_protocol( + ik.keithley.Keithley195, + [ + init, + "U0DX", + f"R{index + 1}DX", + "U0DX", + f"R{index + 1}DX", + "U0DX", # query + "U0DX", + ], + [statusword, statusword, new_statusword, new_statusword], + sep="\n", + ) as mul: + mul.input_range = range + mul.input_range = u.Quantity(range, units) + assert mul.input_range == range * units + + +def test_keithley195_input_range_auto(init, statusword): + """Get / set input range auto.""" + # new statusword + new_statusword = list(statusword.decode()) + new_statusword[6] = "0" + new_statusword = "".join(new_statusword) + with expected_protocol( + ik.keithley.Keithley195, [init, "R0DX", "U0DX"], [new_statusword], sep="\n" + ) as mul: + mul.input_range = "Auto" + assert mul.input_range == "auto" + + +def test_keithley195_input_range_set_wrong_string(init): + """Raise Value error if input range set w/ string other than 'auto'.""" + bad_string = "forty-two" + with expected_protocol(ik.keithley.Keithley195, [init], [], sep="\n") as mul: + with pytest.raises(ValueError) as err_info: + mul.input_range = bad_string + err_msg = err_info.value.args[0] + assert ( + err_msg == 'Only "auto" is acceptable when specifying the ' + "input range as a string." + ) + + +def test_keithley195_input_range_set_wrong_range(init, statusword): + """Raise Value error if input range set w/ out of range value.""" + mode = ik.keithley.Keithley195.Mode(int(statusword.decode()[5])) + valid = ik.keithley.Keithley195.ValidRange[mode.name].value + out_of_range_value = 42 + with expected_protocol( + ik.keithley.Keithley195, [init, "U0DX"], [statusword], sep="\n" + ) as mul: + with pytest.raises(ValueError) as err_info: + mul.input_range = out_of_range_value + err_msg = err_info.value.args[0] + assert err_msg == f"Valid range settings for mode {mode} are: {valid}" + + +def test_keithley195_input_range_set_wrong_type(init, statusword): + """Raise TypeError if input range set w/ wrong type.""" + wrong_type = {"The Answer": 42} + with expected_protocol( + ik.keithley.Keithley195, [init, "U0DX"], [statusword], sep="\n" + ) as mul: + with pytest.raises(TypeError) as err_info: + mul.input_range = wrong_type + err_msg = err_info.value.args[0] + assert ( + err_msg == f"Range setting must be specified as a float, " + f'int, or the string "auto", got ' + f"{type(wrong_type)}" + ) + + +@given(value=st.floats(allow_infinity=False, allow_nan=False)) +def test_measure_mode_is_none(init, statusword, value): + """Get a measurement in current measure mode.""" + mode = ik.keithley.Keithley195.Mode(int(statusword.decode()[5])) + units = ik.keithley.keithley195.UNITS2[mode] + with expected_protocol( + ik.keithley.Keithley195, [init, "U0DX"], [statusword, f"{value}"], sep="\n" + ) as mul: + assert mul.measure() == value * units + + +def test_measure_mode_is_current(init, statusword): + """Get a measurement with given mode, which is already set.""" + mode = ik.keithley.Keithley195.Mode(int(statusword.decode()[5])) + units = ik.keithley.keithley195.UNITS2[mode] + value = 3.14 + with expected_protocol( + ik.keithley.Keithley195, [init, "U0DX"], [statusword, f"{value}"], sep="\n" + ) as mul: + assert mul.measure(mode=mode) == value * units + + +def test_measure_new_mode(init, statusword, mocker): + """Get a measurement with given mode, which is already set. + + Mock time.sleep() call and assert it is called with 2 seconds. + """ + # patch call to time.sleep with mock + mock_time = mocker.patch.object(time, "sleep", return_value=None) + + # new modes + new_mode = ik.keithley.Keithley195.Mode(0) + units = ik.keithley.keithley195.UNITS2[new_mode] + value = 3.14 + with expected_protocol( + ik.keithley.Keithley195, + [init, "U0DX", "F0DX"], # send new mode + [statusword, f"{value}"], + sep="\n", + ) as mul: + assert mul.measure(mode=new_mode) == value * units + + # assert time.sleep is called with 2 second argument + mock_time.assert_called_with(2) + + +def test_parse_status_word_value_error(init): + """Raise ValueError if status word does not start with '195'.""" + wrong_statusword = "42 314" + with expected_protocol( + ik.keithley.Keithley195, + [ + init, + ], + [], + sep="\n", + ) as mul: + with pytest.raises(ValueError) as err_info: + mul.parse_status_word(wrong_statusword) + err_msg = err_info.value.args[0] + assert ( + err_msg == f"Status word starts with wrong prefix, expected " + f"195, got {wrong_statusword}" + ) + + +def test_trigger(init): + """Send a trigger command.""" + with expected_protocol(ik.keithley.Keithley195, [init, "X"], [], sep="\n") as mul: + mul.trigger() + + +def test_auto_range(init): + """Set input range to 'auto'.""" + with expected_protocol( + ik.keithley.Keithley195, + [ + init, + "R0DX", + ], + [], + sep="\n", + ) as mul: + mul.auto_range() diff --git a/instruments/tests/test_keithley/test_keithley2182.py b/instruments/tests/test_keithley/test_keithley2182.py index 02303b0da..6400ae8a5 100644 --- a/instruments/tests/test_keithley/test_keithley2182.py +++ b/instruments/tests/test_keithley/test_keithley2182.py @@ -1,19 +1,20 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Unit tests for the Keithley 2182 nano-voltmeter """ # IMPORTS ##################################################################### -from __future__ import absolute_import -import quantities as pq -import numpy as np -from nose.tools import raises +import pytest import instruments as ik -from instruments.tests import expected_protocol +from instruments.optional_dep_finder import numpy +from instruments.tests import ( + expected_protocol, + iterable_eq, +) +from instruments.units import ureg as u # TESTS ####################################################################### @@ -31,63 +32,80 @@ def test_channel_mode(): ], [ "VOLT", - ] + ], ) as inst: channel = inst.channel[0] assert channel.mode == inst.Mode.voltage_dc + with pytest.raises(NotImplementedError): + channel.mode = 42 + + +def test_channel_trigger_mode(): + """Raise NotImplementedError when getting / setting trigger mode.""" + with expected_protocol(ik.keithley.Keithley2182, [], []) as inst: + channel = inst.channel[0] + with pytest.raises(NotImplementedError): + _ = channel.trigger_mode + with pytest.raises(NotImplementedError): + channel.trigger_mode = 42 + + +def test_channel_relative(): + """Raise NotImplementedError when getting / setting relative.""" + with expected_protocol(ik.keithley.Keithley2182, [], []) as inst: + channel = inst.channel[0] + with pytest.raises(NotImplementedError): + _ = channel.relative + with pytest.raises(NotImplementedError): + channel.relative = 42 + + +def test_channel_input_range(): + """Raise NotImplementedError when getting / setting input range.""" + with expected_protocol(ik.keithley.Keithley2182, [], []) as inst: + channel = inst.channel[0] + with pytest.raises(NotImplementedError): + _ = channel.input_range + with pytest.raises(NotImplementedError): + channel.input_range = 42 + + +def test_channel_measure_mode_not_none(): + """Raise NotImplementedError measuring with non-None mode.""" + with expected_protocol(ik.keithley.Keithley2182, [], []) as inst: + channel = inst.channel[0] + with pytest.raises(NotImplementedError): + channel.measure(mode="Some Mode") def test_channel_measure_voltage(): with expected_protocol( ik.keithley.Keithley2182, - [ - "SENS:CHAN 1", - "SENS:DATA:FRES?", - "SENS:FUNC?" - ], + ["SENS:CHAN 1", "SENS:DATA:FRES?", "SENS:FUNC?"], [ "1.234", "VOLT", - ] + ], ) as inst: channel = inst.channel[0] - assert channel.measure() == 1.234 * pq.volt + assert channel.measure() == 1.234 * u.volt def test_channel_measure_temperature(): with expected_protocol( ik.keithley.Keithley2182, - [ - "SENS:CHAN 1", - "SENS:DATA:FRES?", - "SENS:FUNC?", - "UNIT:TEMP?" - ], - [ - "1.234", - "TEMP", - "C" - ] + ["SENS:CHAN 1", "SENS:DATA:FRES?", "SENS:FUNC?", "UNIT:TEMP?"], + ["1.234", "TEMP", "C"], ) as inst: channel = inst.channel[0] - assert channel.measure() == 1.234 * pq.celsius + assert channel.measure() == u.Quantity(1.234, u.degC) -@raises(ValueError) def test_channel_measure_unknown_temperature_units(): - with expected_protocol( + with pytest.raises(ValueError), expected_protocol( ik.keithley.Keithley2182, - [ - "SENS:CHAN 1", - "SENS:DATA:FRES?", - "SENS:FUNC?", - "UNIT:TEMP?" - ], - [ - "1.234", - "TEMP", - "Z" - ] + ["SENS:CHAN 1", "SENS:DATA:FRES?", "SENS:FUNC?", "UNIT:TEMP?"], + ["1.234", "TEMP", "Z"], ) as inst: inst.channel[0].measure() @@ -98,55 +116,35 @@ def test_units(): [ "SENS:FUNC?", "UNIT:TEMP?", - "SENS:FUNC?", "UNIT:TEMP?", - "SENS:FUNC?", "UNIT:TEMP?", - - "SENS:FUNC?" + "SENS:FUNC?", ], - [ - "TEMP", - "C", - - "TEMP", - "F", - - "TEMP", - "K", - - "VOLT" - ] + ["TEMP", "C", "TEMP", "F", "TEMP", "K", "VOLT"], ) as inst: - units = str(inst.units.units).split()[1] - assert units == "degC" - - units = str(inst.units.units).split()[1] - assert units == "degF" - - units = str(inst.units.units).split()[1] - assert units == "K" - - assert inst.units == pq.volt + assert inst.units == u.degC + assert inst.units == u.degF + assert inst.units == u.kelvin + assert inst.units == u.volt def test_fetch(): with expected_protocol( ik.keithley.Keithley2182, - [ - "FETC?", - "SENS:FUNC?" - ], + ["FETC?", "SENS:FUNC?"], [ "1.234,1,5.678", "VOLT", - ] + ], ) as inst: - np.testing.assert_array_equal( - inst.fetch(), [1.234, 1, 5.678] * pq.volt - ) + data = inst.fetch() + vals = [1.234, 1, 5.678] + expected_data = tuple(v * u.volt for v in vals) + if numpy: + expected_data = vals * u.volt + iterable_eq(data, expected_data) def test_measure(): @@ -157,21 +155,14 @@ def test_measure(): "MEAS:VOLT?", "SENS:FUNC?", ], - [ - "VOLT", - "1.234", - "VOLT" - ] + ["VOLT", "1.234", "VOLT"], ) as inst: - assert inst.measure() == 1.234 * pq.volt + assert inst.measure() == 1.234 * u.volt -@raises(TypeError) def test_measure_invalid_mode(): - with expected_protocol( - ik.keithley.Keithley2182, - [], - [] + with pytest.raises(TypeError), expected_protocol( + ik.keithley.Keithley2182, [], [] ) as inst: inst.measure("derp") @@ -179,14 +170,8 @@ def test_measure_invalid_mode(): def test_relative_get(): with expected_protocol( ik.keithley.Keithley2182, - [ - "SENS:FUNC?", - "SENS:VOLT:CHAN1:REF:STAT?" - ], - [ - "VOLT", - "ON" - ] + ["SENS:FUNC?", "SENS:VOLT:CHAN1:REF:STAT?"], + ["VOLT", "ON"], ) as inst: assert inst.relative is True @@ -198,13 +183,13 @@ def test_relative_set_already_enabled(): "SENS:FUNC?", "SENS:FUNC?", "SENS:VOLT:CHAN1:REF:STAT?", - "SENS:VOLT:CHAN1:REF:ACQ" + "SENS:VOLT:CHAN1:REF:ACQ", ], [ "VOLT", "VOLT", "ON", - ] + ], ) as inst: inst.relative = True @@ -216,22 +201,28 @@ def test_relative_set_start_disabled(): "SENS:FUNC?", "SENS:FUNC?", "SENS:VOLT:CHAN1:REF:STAT?", - "SENS:VOLT:CHAN1:REF:STAT ON" + "SENS:VOLT:CHAN1:REF:STAT ON", ], [ "VOLT", "VOLT", "OFF", - ] + ], ) as inst: inst.relative = True -@raises(TypeError) def test_relative_set_wrong_type(): - with expected_protocol( - ik.keithley.Keithley2182, - [], - [] + with pytest.raises(TypeError), expected_protocol( + ik.keithley.Keithley2182, [], [] ) as inst: inst.relative = "derp" + + +def test_input_range(): + """Raise NotImplementedError when getting / setting input range.""" + with expected_protocol(ik.keithley.Keithley2182, [], []) as inst: + with pytest.raises(NotImplementedError): + _ = inst.input_range + with pytest.raises(NotImplementedError): + inst.input_range = 42 diff --git a/instruments/tests/test_keithley/test_keithley485.py b/instruments/tests/test_keithley/test_keithley485.py new file mode 100644 index 000000000..b1841bda7 --- /dev/null +++ b/instruments/tests/test_keithley/test_keithley485.py @@ -0,0 +1,260 @@ +#!/usr/bin/env python +""" +Module containing tests for the Keithley 485 picoammeter +""" + +# IMPORTS #################################################################### + +import pytest + +from instruments.units import ureg as u + +import instruments as ik +from instruments.tests import expected_protocol + + +# TESTS ###################################################################### + +# pylint: disable=protected-access + + +def test_zero_check(): + with expected_protocol( + ik.keithley.Keithley485, ["C0X", "C1X", "U0X"], ["4851000000000:"] + ) as inst: + inst.zero_check = False + inst.zero_check = True + assert inst.zero_check + with pytest.raises(TypeError) as err_info: + inst.zero_check = 42 + err_msg = err_info.value.args[0] + assert err_msg == "Zero Check mode must be a boolean." + + +def test_log(): + with expected_protocol( + ik.keithley.Keithley485, ["D0X", "D1X", "U0X"], ["4850100000000:"] + ) as inst: + inst.log = False + inst.log = True + assert inst.log + with pytest.raises(TypeError) as err_info: + inst.log = 42 + err_msg = err_info.value.args[0] + assert err_msg == "Log mode must be a boolean." + + +def test_input_range(): + with expected_protocol( + ik.keithley.Keithley485, ["R0X", "R7X", "U0X"], ["4850070000000:"] + ) as inst: + inst.input_range = "auto" + inst.input_range = 2e-3 + assert inst.input_range == 2.0 * u.milliamp + + +def test_relative(): + with expected_protocol( + ik.keithley.Keithley485, ["Z0X", "Z1X", "U0X"], ["4850001000000:"] + ) as inst: + inst.relative = False + inst.relative = True + assert inst.relative + with pytest.raises(TypeError) as err_info: + inst.relative = 42 + err_msg = err_info.value.args[0] + assert err_msg == "Relative mode must be a boolean." + + +def test_eoi_mode(): + with expected_protocol( + ik.keithley.Keithley485, ["K0X", "K1X", "U0X"], ["4850000100000:"] + ) as inst: + inst.eoi_mode = True + inst.eoi_mode = False + assert not inst.eoi_mode + with pytest.raises(TypeError) as err_info: + inst.eoi_mode = 42 + err_msg = err_info.value.args[0] + assert err_msg == "EOI mode must be a boolean." + + +def test_trigger_mode(): + with expected_protocol( + ik.keithley.Keithley485, ["T0X", "T5X", "U0X"], ["4850000050000:"] + ) as inst: + inst.trigger_mode = "continuous_ontalk" + inst.trigger_mode = "oneshot_onx" + assert inst.trigger_mode == "oneshot_onx" + with pytest.raises(TypeError) as err_info: + newval = 42 + inst.trigger_mode = newval + err_msg = err_info.value.args[0] + assert ( + err_msg == f"Drive must be specified as a " + f"Keithley485.TriggerMode, got {newval} instead." + ) + + +def test_auto_range(): + with expected_protocol( + ik.keithley.Keithley485, ["R0X", "U0X"], ["4850000000000:"] + ) as inst: + inst.auto_range() + assert inst.input_range == "auto" + + +@pytest.mark.parametrize("newval", (2e-9, 2e-8, 2e-7, 2e-6, 2e-5, 2e-4, 2e-3)) +def test_input_range_value(newval): + """Set input range with a given value from list.""" + valid = ("auto", 2e-9, 2e-8, 2e-7, 2e-6, 2e-5, 2e-4, 2e-3) + with expected_protocol( + ik.keithley.Keithley485, [f"R{valid.index(newval)}X"], [] + ) as inst: + inst.input_range = newval + + +def test_input_range_quantity(): + """Set input range with a given value from list.""" + valid = ("auto", 2e-9, 2e-8, 2e-7, 2e-6, 2e-5, 2e-4, 2e-3) + newval = 2e-9 + quant = u.Quantity(newval, u.A) + with expected_protocol( + ik.keithley.Keithley485, [f"R{valid.index(newval)}X"], [] + ) as inst: + inst.input_range = quant + + +def test_input_range_invalid_value(): + """Raise ValueError if invalid value is given.""" + valid = ("auto", 2e-9, 2e-8, 2e-7, 2e-6, 2e-5, 2e-4, 2e-3) + with expected_protocol(ik.keithley.Keithley485, [], []) as inst: + with pytest.raises(ValueError) as err_info: + inst.input_range = 42 + err_msg = err_info.value.args[0] + assert err_msg == f"Valid range settings are: {valid}" + + +def test_input_range_invalid_type(): + """Raise TypeError if invalid type is given.""" + invalid_type = [42] + with expected_protocol(ik.keithley.Keithley485, [], []) as inst: + with pytest.raises(TypeError) as err_info: + inst.input_range = invalid_type + err_msg = err_info.value.args[0] + assert ( + err_msg == f"Range setting must be specified as a float, " + f"int, or the string `auto`, got " + f"{type(invalid_type)}" + ) + + +def test_input_range_invalid_string(): + """Raise ValueError if input range set with invalid string.""" + with expected_protocol(ik.keithley.Keithley485, [], []) as inst: + with pytest.raises(ValueError) as err_info: + inst.input_range = "2e-9" + err_msg = err_info.value.args[0] + assert ( + err_msg == "Only `auto` is acceptable when specifying the " + "range as a string." + ) + + +def test_get_status(): + with expected_protocol( + ik.keithley.Keithley485, ["U0X"], ["4850000000000:"] + ) as inst: + inst.get_status() + + +def test_measure(): + with expected_protocol( + ik.keithley.Keithley485, ["X", "X"], ["NDCA+1.2345E-9", "NDCL-9.0000E+0"] + ) as inst: + assert 1.2345 * u.nanoamp == inst.measure() + assert 1 * u.nanoamp == inst.measure() + + +def test_get_status_word_fails(): + """Raise IOError if status word query fails > 5 times.""" + with expected_protocol( + ik.keithley.Keithley485, + ["U0X", "U0X", "U0X", "U0X", "U0X"], + ["", "", "", "", ""], + ) as inst: + with pytest.raises(IOError) as err_info: + inst._get_status_word() + err_msg = err_info.value.args[0] + assert err_msg == "Could not retrieve status word" + + +def test_parse_status_word_wrong_prefix(): + """Raise ValueError if statusword has wrong prefix.""" + wrong_statusword = "wrong statusword" + with expected_protocol(ik.keithley.Keithley485, [], []) as inst: + with pytest.raises(ValueError) as err_info: + inst._parse_status_word(wrong_statusword) + err_msg = err_info.value.args[0] + assert ( + err_msg == f"Status word starts with wrong prefix: " f"{wrong_statusword}" + ) + + +def test_parse_status_word_cannot_parse(): + """Raise RuntimeError if statusword cannot be parsed.""" + bad_statusword = "485FFFFFFFFFF" + with expected_protocol(ik.keithley.Keithley485, [], []) as inst: + with pytest.raises(RuntimeError) as err_info: + inst._parse_status_word(bad_statusword) + err_msg = err_info.value.args[0] + assert err_msg == f"Cannot parse status word: {bad_statusword}" + + +def test_parse_measurement_invalid_status(): + """Raise ValueError if invalild status encountered.""" + status = "L" + bad_measurement = f"{status}DCA+1.2345E-9" + with expected_protocol(ik.keithley.Keithley485, [], []) as inst: + with pytest.raises(ValueError) as err_info: + inst._parse_measurement(bad_measurement) + err_msg = err_info.value.args[0] + assert ( + err_msg == f"Invalid status word in measurement: " + f"{bytes(status, 'utf-8')}" + ) + + +def test_parse_measurement_bad_status(): + """Raise ValueError if non-normal status encountered.""" + status = ik.keithley.Keithley485.Status.overflow + bad_measurement = f"{status.value.decode('utf-8')}DCA+1.2345E-9" + with expected_protocol(ik.keithley.Keithley485, [], []) as inst: + with pytest.raises(ValueError) as err_info: + inst._parse_measurement(bad_measurement) + err_msg = err_info.value.args[0] + assert err_msg == f"Instrument not in normal mode: {status.name}" + + +def test_parse_measurement_bad_function(): + """Raise ValueError if non-normal function encountered.""" + function = "XX" + bad_measurement = f"N{function}A+1.2345E-9" + with expected_protocol(ik.keithley.Keithley485, [], []) as inst: + with pytest.raises(ValueError) as err_info: + inst._parse_measurement(bad_measurement) + err_msg = err_info.value.args[0] + assert ( + err_msg == f"Instrument not returning DC function: " + f"{bytes(function, 'utf-8')}" + ) + + +def test_parse_measurement_bad_measurement(): + """Raise ValueError if non-normal function encountered.""" + bad_measurement = f"NDCA+1.23X5E-9" + with expected_protocol(ik.keithley.Keithley485, [], []) as inst: + with pytest.raises(Exception) as err_info: + inst._parse_measurement(bad_measurement) + err_msg = err_info.value.args[0] + assert err_msg == f"Cannot parse measurement: {bad_measurement}" diff --git a/instruments/tests/test_keithley/test_keithley580.py b/instruments/tests/test_keithley/test_keithley580.py new file mode 100644 index 000000000..1d6047827 --- /dev/null +++ b/instruments/tests/test_keithley/test_keithley580.py @@ -0,0 +1,692 @@ +#!/usr/bin/env python +""" +Module containing tests for the Keithley 580 digital multimeter. +""" + +# IMPORTS #################################################################### + + +import struct +import time + +from hypothesis import ( + given, + strategies as st, +) +import pytest + +import instruments as ik +from instruments.tests import expected_protocol +from instruments.units import ureg as u + + +# TESTS ###################################################################### + + +# pylint: disable=redefined-outer-name + + +# PYTEST FIXTURES FOR INITIALIZATION # + + +@pytest.fixture +def init(): + """Returns the initialization command that is sent to instrument.""" + return "Y:X:" + + +@pytest.fixture +def create_statusword(): + """Create a function that can create a status word. + + Variables used in tests can be set manually, but useful default + values are set as well. Note: The terminator is not created, since + it is already sent by `expected_protocol`. + + :return: Method to make a status word. + :rtype: `method` + """ + + def make_statusword( + drive=b"1", + polarity=b"0", + drycircuit=b"0", + operate=b"0", + rng=b"0", + relative=b"0", + trigger=b"1", + linefreq=b"0", + ): + """Create the status word.""" + # other variables + eoi = b"0" + sqrondata = b"0" + sqronerror = b"0" + + status_word = struct.pack( + "@8c2s2sc", + drive, + polarity, + drycircuit, + operate, + rng, + relative, + eoi, + trigger, + sqrondata, + sqronerror, + linefreq, + ) + + return b"580" + status_word + + return make_statusword + + +@pytest.fixture +def create_measurement(): + """Create a function that can create a measurement. + + Variables used in tests can be set manually, but useful default + values are set as well. + + :return: Method to make a measurement. + :rtype: `method` + """ + + def make_measurement( + status=b"N", polarity=b"+", drycircuit=b"D", drive=b"P", resistance=b"42" + ): + """Create a measurement.""" + resistance = bytes(resistance.decode().zfill(11), "utf-8") + measurement = struct.pack( + "@4c11s", status, polarity, drycircuit, drive, resistance + ) + + return measurement + + return make_measurement + + +@pytest.fixture(autouse=True) +def mock_time(mocker): + """Mock the time.sleep object for use. + + Use by default, such that getting status word is fast in tests. + """ + return mocker.patch.object(time, "sleep", return_value=None) + + +# PROPERTIES # + + +@pytest.mark.parametrize("newval", ik.keithley.Keithley580.Polarity) +def test_polarity(init, create_statusword, newval): + """Get / set instrument polarity.""" + status_word = create_statusword(polarity=bytes(str(newval.value), "utf-8")) + with expected_protocol( + ik.keithley.Keithley580, + [init, f"P{newval.value}X" + ":", "U0X:", ":"], + [status_word + b":"], + sep="\n", + ) as inst: + inst.polarity = newval + assert inst.polarity == newval + + +@pytest.mark.parametrize( + "newval_str", [it.name for it in ik.keithley.Keithley580.Polarity] +) +def test_polarity_string(init, newval_str): + """Set polarity with a string.""" + newval = ik.keithley.Keithley580.Polarity[newval_str] + with expected_protocol( + ik.keithley.Keithley580, + [ + init, + f"P{newval.value}X" + ":", + ], + [], + sep="\n", + ) as inst: + inst.polarity = newval_str + + +def test_polarity_wrong_type(init): + """Raise TypeError if setting polarity with wrong type.""" + wrong_type = 42 + with expected_protocol( + ik.keithley.Keithley580, + [ + init, + ], + [], + sep="\n", + ) as inst: + with pytest.raises(TypeError) as err_info: + inst.polarity = wrong_type + err_msg = err_info.value.args[0] + assert ( + err_msg == f"Polarity must be specified as a " + f"Keithley580.Polarity, got {wrong_type} " + f"instead." + ) + + +@pytest.mark.parametrize("newval", ik.keithley.Keithley580.Drive) +def test_drive(init, create_statusword, newval): + """Get / set instrument drive.""" + status_word = create_statusword(drive=bytes(str(newval.value), "utf-8")) + with expected_protocol( + ik.keithley.Keithley580, + [init, f"D{newval.value}X" + ":", "U0X:", ":"], + [status_word + b":"], + sep="\n", + ) as inst: + inst.drive = newval + assert inst.drive == newval + + +@pytest.mark.parametrize( + "newval_str", [it.name for it in ik.keithley.Keithley580.Drive] +) +def test_drive_string(init, newval_str): + """Set drive with a string.""" + newval = ik.keithley.Keithley580.Drive[newval_str] + with expected_protocol( + ik.keithley.Keithley580, + [ + init, + f"D{newval.value}X" + ":", + ], + [], + sep="\n", + ) as inst: + inst.drive = newval_str + + +def test_drive_wrong_type(init): + """Raise TypeError if setting drive with wrong type.""" + wrong_type = 42 + with expected_protocol( + ik.keithley.Keithley580, + [ + init, + ], + [], + sep="\n", + ) as inst: + with pytest.raises(TypeError) as err_info: + inst.drive = wrong_type + err_msg = err_info.value.args[0] + assert ( + err_msg == f"Drive must be specified as a " + f"Keithley580.Drive, got {wrong_type} " + f"instead." + ) + + +@pytest.mark.parametrize("newval", (True, False)) +def test_dry_circuit_test(init, create_statusword, newval): + """Get / set dry circuit test.""" + status_word = create_statusword(drycircuit=bytes(str(int(newval)), "utf-8")) + with expected_protocol( + ik.keithley.Keithley580, + [init, f"C{int(newval)}X" + ":", "U0X:", ":"], + [status_word + b":"], + sep="\n", + ) as inst: + inst.dry_circuit_test = newval + assert inst.dry_circuit_test == newval + + +def test_dry_circuit_test_wrong_type(init): + """Raise TypeError if setting dry circuit test with wrong type.""" + wrong_type = 42 + with expected_protocol( + ik.keithley.Keithley580, + [ + init, + ], + [], + sep="\n", + ) as inst: + with pytest.raises(TypeError) as err_info: + inst.dry_circuit_test = wrong_type + err_msg = err_info.value.args[0] + assert err_msg == "DryCircuitTest mode must be a boolean." + + +@pytest.mark.parametrize("newval", (True, False)) +def test_operate(init, create_statusword, newval): + """Get / set operate.""" + status_word = create_statusword(operate=bytes(str(int(newval)), "utf-8")) + with expected_protocol( + ik.keithley.Keithley580, + [init, f"O{int(newval)}X" + ":", "U0X:", ":"], + [status_word + b":"], + sep="\n", + ) as inst: + inst.operate = newval + assert inst.operate == newval + + +def test_operate_wrong_type(init): + """Raise TypeError if setting operate with wrong type.""" + wrong_type = 42 + with expected_protocol( + ik.keithley.Keithley580, + [ + init, + ], + [], + sep="\n", + ) as inst: + with pytest.raises(TypeError) as err_info: + inst.operate = wrong_type + err_msg = err_info.value.args[0] + assert err_msg == "Operate mode must be a boolean." + + +@pytest.mark.parametrize("newval", (True, False)) +def test_relative(init, create_statusword, newval): + """Get / set relative.""" + status_word = create_statusword(relative=bytes(str(int(newval)), "utf-8")) + with expected_protocol( + ik.keithley.Keithley580, + [init, f"Z{int(newval)}X" + ":", "U0X:", ":"], + [status_word + b":"], + sep="\n", + ) as inst: + inst.relative = newval + assert inst.relative == newval + + +def test_relative_wrong_type(init): + """Raise TypeError if setting relative with wrong type.""" + wrong_type = 42 + with expected_protocol( + ik.keithley.Keithley580, + [ + init, + ], + [], + sep="\n", + ) as inst: + with pytest.raises(TypeError) as err_info: + inst.relative = wrong_type + err_msg = err_info.value.args[0] + assert err_msg == "Relative mode must be a boolean." + + +def test_trigger_mode_get(init): + """Getting trigger mode raises NotImplementedError. + + Unclear why this is not implemented. + """ + with expected_protocol( + ik.keithley.Keithley580, + [ + init, + ], + [], + sep="\n", + ) as inst: + with pytest.raises(NotImplementedError): + assert inst.trigger_mode + + +@pytest.mark.parametrize("newval", ik.keithley.Keithley580.TriggerMode) +def test_trigger_mode_set(init, newval): + """Set instrument trigger mode.""" + with expected_protocol( + ik.keithley.Keithley580, [init, f"T{newval.value}X" + ":"], [], sep="\n" + ) as inst: + inst.trigger_mode = newval + + +@pytest.mark.parametrize("newval", ik.keithley.Keithley580.TriggerMode) +def test_trigger_mode_set_string(init, newval): + """Set instrument trigger mode as a string.""" + newval_str = newval.name + with expected_protocol( + ik.keithley.Keithley580, [init, f"T{newval.value}X" + ":"], [], sep="\n" + ) as inst: + inst.trigger_mode = newval_str + + +def test_trigger_mode_set_type_error(init): + """Raise TypeError when setting trigger mode with wrong type.""" + wrong_type = 42 + with expected_protocol(ik.keithley.Keithley580, [init], [], sep="\n") as inst: + with pytest.raises(TypeError) as err_info: + inst.trigger_mode = wrong_type + err_msg = err_info.value.args[0] + assert ( + err_msg == f"Drive must be specified as a " + f"Keithley580.TriggerMode, got " + f"{wrong_type} instead." + ) + + +@pytest.mark.parametrize("newval", (2e-1, 2e0, 2e1, 2e2, 2e3, 2e4, 2e5)) +def test_input_range_float(init, create_statusword, newval): + """Get / set input range with a float, unitful and unitless.""" + valid = ("auto", 2e-1, 2e0, 2e1, 2e2, 2e3, 2e4, 2e5) + newval_unitful = newval * u.ohm + newval_index = valid.index(newval) + + status_word = create_statusword(rng=bytes(str(newval_index), "utf-8")) + + with expected_protocol( + ik.keithley.Keithley580, + [init, f"R{newval_index}X" + ":", f"R{newval_index}X" + ":", "U0X:", ":"], + [status_word + b":"], + sep="\n", + ) as inst: + inst.input_range = newval + inst.input_range = newval_unitful + assert inst.input_range == newval_unitful + + +def test_input_range_auto(init, create_statusword): + """Get / set input range auto.""" + newval = "auto" + newval_index = 0 + + status_word = create_statusword(rng=bytes(str(newval_index), "utf-8")) + + with expected_protocol( + ik.keithley.Keithley580, + [init, f"R{newval_index}X" + ":", "U0X:", ":"], + [status_word + b":"], + sep="\n", + ) as inst: + inst.input_range = newval + assert inst.input_range == newval + + +@given( + newval=st.floats().filter(lambda x: x not in (2e-1, 2e0, 2e1, 2e2, 2e3, 2e4, 2e5)) +) +def test_input_range_float_value_error(init, newval): + """Raise ValueError if input range set to invalid value.""" + valid = ("auto", 2e-1, 2e0, 2e1, 2e2, 2e3, 2e4, 2e5) + with expected_protocol(ik.keithley.Keithley580, [init], [], sep="\n") as inst: + with pytest.raises(ValueError) as err_info: + inst.input_range = newval + err_msg = err_info.value.args[0] + assert err_msg == f"Valid range settings are: {valid}" + + +def test_input_range_auto_value_error(init): + """Raise ValueError if string set as input range is not 'auto'.""" + newval = "automatic" + + with expected_protocol(ik.keithley.Keithley580, [init], [], sep="\n") as inst: + with pytest.raises(ValueError) as err_info: + inst.input_range = newval + err_msg = err_info.value.args[0] + assert ( + err_msg == 'Only "auto" is acceptable when specifying the ' + "input range as a string." + ) + + +def test_input_range_type_error(init): + """Raise TypeError if input range is set with wrong type.""" + wrong_type = {"The Answer": 42} + + with expected_protocol(ik.keithley.Keithley580, [init], [], sep="\n") as inst: + with pytest.raises(TypeError) as err_info: + inst.input_range = wrong_type + err_msg = err_info.value.args[0] + assert ( + err_msg == f"Range setting must be specified as a float, " + f'int, or the string "auto", got ' + f"{type(wrong_type)}" + ) + + +# METHODS # + + +def test_trigger(init): + """Send a trigger to instrument.""" + with expected_protocol(ik.keithley.Keithley580, [init, "X:"], [], sep="\n") as inst: + inst.trigger() + + +def test_auto_range(init): + """Put instrument into auto range mode.""" + with expected_protocol( + ik.keithley.Keithley580, [init, "R0X:"], [], sep="\n" + ) as inst: + inst.auto_range() + + +def test_set_calibration_value(init): + """Raise NotImplementedError when trying to set calibration value.""" + value = None + with expected_protocol(ik.keithley.Keithley580, [init], [], sep="\n") as inst: + with pytest.raises(NotImplementedError) as err_info: + inst.set_calibration_value(value) + err_msg = err_info.value.args[0] + assert err_msg == "setCalibrationValue not implemented" + + +def test_store_calibration_constants(init): + """Raise NotImplementedError when trying to store calibration constants.""" + with expected_protocol(ik.keithley.Keithley580, [init], [], sep="\n") as inst: + with pytest.raises(NotImplementedError) as err_info: + inst.store_calibration_constants() + err_msg = err_info.value.args[0] + assert err_msg == "storeCalibrationConstants not implemented" + + +# STATUS WORD # + + +def test_get_status_word(init, create_statusword, mock_time): + """Test getting a default status word.""" + status_word = create_statusword() + + with expected_protocol( + ik.keithley.Keithley580, [init, "U0X:", ":"], [status_word + b":"], sep="\n" + ) as inst: + assert inst.get_status_word() == status_word + mock_time.assert_called_with(1) + + +def test_get_status_word_fails(init, mock_time): + """Raise IOError after 5 reads with bad returns.""" + wrong_status_word = b"195 12345" + + with expected_protocol( + ik.keithley.Keithley580, + [init, "U0X:", ":", "U0X:", ":", "U0X:", ":", "U0X:", ":", "U0X:", ":"], + [ + wrong_status_word, + wrong_status_word, + wrong_status_word, + wrong_status_word, + wrong_status_word, + ], + sep="\n", + ) as inst: + with pytest.raises(IOError) as err_info: + inst.get_status_word() + err_msg = err_info.value.args[0] + assert err_msg == "could not retrieve status word" + + mock_time.assert_called_with(1) + + +@pytest.mark.parametrize("line_frequency", (("0", "60Hz"), ("1", "50Hz"))) +def test_parse_status_word(init, create_statusword, line_frequency): + """Parse a given status word. + + Note: full range of parameters explored in individual routines. + Here, we thus just use the default status word created by the + fixture and only parametrize where other routines do not. + """ + status_word = create_statusword(linefreq=bytes(line_frequency[0], "utf-8")) + # create the dictionary to compare to + expected_dict = { + "drive": "dc", + "polarity": "+", + "drycircuit": False, + "operate": False, + "range": "auto", + "relative": False, + "eoi": b"0", + "trigger": True, + "sqrondata": struct.pack("@2s", b"0"), + "sqronerror": struct.pack("@2s", b"0"), + "linefreq": line_frequency[1], + } + with expected_protocol(ik.keithley.Keithley580, [init], [], sep="\n") as inst: + # add terminator to expected dict: + expected_dict["terminator"] = inst.terminator + assert inst.parse_status_word(status_word) == expected_dict + + +@given( + drive=st.integers(min_value=2, max_value=9), + polarity=st.integers(min_value=2, max_value=9), + rng=st.integers(min_value=8, max_value=9), + linefreq=st.integers(min_value=2, max_value=9), +) +def test_parse_status_word_invalid_values( + init, create_statusword, drive, polarity, rng, linefreq +): + """Raise RuntimeError if status word contains invalid values.""" + status_word = create_statusword( + drive=bytes(str(drive), "utf-8"), + polarity=bytes(str(polarity), "utf-8"), + rng=bytes(str(rng), "utf-8"), + linefreq=bytes(str(linefreq), "utf-8"), + ) + with expected_protocol(ik.keithley.Keithley580, [init], [], sep="\n") as inst: + with pytest.raises(RuntimeError) as err_info: + inst.parse_status_word(status_word) + err_msg = err_info.value.args[0] + assert err_msg == f"Cannot parse status word: {status_word}" + + +def test_parse_status_word_invalid_prefix(init): + """Raise ValueError if status word has invalid prefix.""" + invalid_status_word = b"314 424242" + with expected_protocol(ik.keithley.Keithley580, [init], [], sep="\n") as inst: + with pytest.raises(ValueError) as err_info: + inst.parse_status_word(invalid_status_word) + err_msg = err_info.value.args[0] + assert ( + err_msg == f"Status word starts with wrong prefix: " + f"{invalid_status_word}" + ) + + +# MEASUREMENT # + + +@given(resistance=st.floats(min_value=0.001, max_value=1000000)) +def test_measure(init, create_measurement, resistance): + """Perform a resistance measurement.""" + # cap resistance at max of 11 character with given max_value + resistance_byte = bytes(f"{resistance:.3f}", "utf-8") + measurement = create_measurement(resistance=resistance_byte) + with expected_protocol( + ik.keithley.Keithley580, + [init, "X:", ":"], # trigger + [measurement + b":"], + sep="\n", + ) as inst: + read_value = inst.measure() + assert read_value.magnitude == pytest.approx(resistance, rel=1e-5) + assert read_value.units == u.ohm + + +@pytest.mark.parametrize("status", (b"S", b"N", b"O", b"Z")) +@pytest.mark.parametrize("polarity", (b"+", b"-")) +@pytest.mark.parametrize("drycircuit", (b"N", b"D")) +@pytest.mark.parametrize("drive", (b"P", b"D")) +def test_parse_measurement( + init, create_measurement, status, polarity, drycircuit, drive +): + """Parse a given measurement.""" + resistance = b"42" + measurement = create_measurement( + status=status, + polarity=polarity, + drycircuit=drycircuit, + drive=drive, + resistance=resistance, + ) + + # valid states + valid = { + "status": {b"S": "standby", b"N": "normal", b"O": "overflow", b"Z": "relative"}, + "polarity": {b"+": "+", b"-": "-"}, + "drycircuit": {b"N": False, b"D": True}, + "drive": {b"P": "pulsed", b"D": "dc"}, + } + + # create expected dictionary + dict_expected = { + "status": valid["status"][status], + "polarity": valid["polarity"][polarity], + "drycircuit": valid["drycircuit"][drycircuit], + "drive": valid["drive"][drive], + "resistance": float(resistance.decode()) * u.ohm, + } + + with expected_protocol( + ik.keithley.Keithley580, + [ + init, + ], + [], + sep="\n", + ) as inst: + assert inst.parse_measurement(measurement) == dict_expected + + +def test_parse_measurement_invalid(init, create_measurement): + """Raise an exception if the status contains invalid character.""" + measurement = create_measurement(status=bytes("V", "utf-8")) + + with expected_protocol( + ik.keithley.Keithley580, + [ + init, + ], + [], + sep="\n", + ) as inst: + with pytest.raises(Exception) as exc_info: + inst.parse_measurement(measurement) + err_msg = exc_info.value.args[0] + assert err_msg == f"Cannot parse measurement: {measurement}" + + +# COMMUNICATION METHODS # + + +def test_sendcmd(init): + """Send a command to the instrument.""" + cmd = "COMMAND" + with expected_protocol( + ik.keithley.Keithley580, [init, cmd + ":"], [], sep="\n" + ) as inst: + inst.sendcmd(cmd) + + +def test_query(init): + """Query the instrument.""" + cmd = "COMMAND" + answer = "ANSWER" + with expected_protocol( + ik.keithley.Keithley580, [init, cmd + ":"], [answer + ":"], sep="\n" + ) as inst: + assert inst.query(cmd) == answer diff --git a/instruments/tests/test_keithley/test_keithley6220.py b/instruments/tests/test_keithley/test_keithley6220.py index 35e12a99b..8829f353f 100644 --- a/instruments/tests/test_keithley/test_keithley6220.py +++ b/instruments/tests/test_keithley/test_keithley6220.py @@ -1,14 +1,14 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Unit tests for the Keithley 6220 constant current supply """ # IMPORTS ##################################################################### -from __future__ import absolute_import -import quantities as pq +import pytest + +from instruments.units import ureg as u import instruments as ik from instruments.tests import expected_protocol @@ -21,29 +21,33 @@ def test_channel(): assert inst.channel[0] == inst +def test_voltage(): + """Raise NotImplementedError when getting / setting voltage.""" + with expected_protocol(ik.keithley.Keithley6220, [], []) as inst: + with pytest.raises(NotImplementedError) as err_info: + _ = inst.voltage + err_msg = err_info.value.args[0] + assert err_msg == "The Keithley 6220 does not support voltage " "settings." + with pytest.raises(NotImplementedError) as err_info: + inst.voltage = 42 + err_msg = err_info.value.args[0] + assert err_msg == "The Keithley 6220 does not support voltage " "settings." + + def test_current(): with expected_protocol( ik.keithley.Keithley6220, - [ - "SOUR:CURR?", - "SOUR:CURR {:e}".format(0.05) - ], + ["SOUR:CURR?", f"SOUR:CURR {0.05:e}"], [ "0.1", - ] + ], ) as inst: - assert inst.current == 100 * pq.milliamp - assert inst.current_min == -105 * pq.milliamp - assert inst.current_max == +105 * pq.milliamp - inst.current = 50 * pq.milliamp + assert inst.current == 100 * u.milliamp + assert inst.current_min == -105 * u.milliamp + assert inst.current_max == +105 * u.milliamp + inst.current = 50 * u.milliamp def test_disable(): - with expected_protocol( - ik.keithley.Keithley6220, - [ - "SOUR:CLE:IMM" - ], - [] - ) as inst: + with expected_protocol(ik.keithley.Keithley6220, ["SOUR:CLE:IMM"], []) as inst: inst.disable() diff --git a/instruments/tests/test_keithley/test_keithley6514.py b/instruments/tests/test_keithley/test_keithley6514.py index 167e29ff7..e5f4b5ccc 100644 --- a/instruments/tests/test_keithley/test_keithley6514.py +++ b/instruments/tests/test_keithley/test_keithley6514.py @@ -1,18 +1,16 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Unit tests for the Keithley 6514 electrometer """ # IMPORTS ##################################################################### -from __future__ import absolute_import -import quantities as pq -from nose.tools import raises +import pytest import instruments as ik from instruments.tests import expected_protocol +from instruments.units import ureg as u # TESTS ####################################################################### @@ -23,15 +21,14 @@ def test_valid_range(): inst = ik.keithley.Keithley6514.open_test() assert inst._valid_range(inst.Mode.voltage) == inst.ValidRange.voltage assert inst._valid_range(inst.Mode.current) == inst.ValidRange.current - assert inst._valid_range( - inst.Mode.resistance) == inst.ValidRange.resistance + assert inst._valid_range(inst.Mode.resistance) == inst.ValidRange.resistance assert inst._valid_range(inst.Mode.charge) == inst.ValidRange.charge -@raises(ValueError) def test_valid_range_invalid(): - inst = ik.keithley.Keithley6514.open_test() - inst._valid_range(inst.TriggerMode.immediate) + with pytest.raises(ValueError): + inst = ik.keithley.Keithley6514.open_test() + inst._valid_range(inst.TriggerMode.immediate) def test_parse_measurement(): @@ -40,26 +37,17 @@ def test_parse_measurement(): [ "FUNCTION?", ], - [ - '"VOLT:DC"' - ] + ['"VOLT:DC"'], ) as inst: reading, timestamp, status = inst._parse_measurement("1.0,1234,5678") - assert reading == 1.0 * pq.volt + assert reading == 1.0 * u.volt assert timestamp == 1234 assert status == 5678 def test_mode(): with expected_protocol( - ik.keithley.Keithley6514, - [ - "FUNCTION?", - 'FUNCTION "VOLT:DC"' - ], - [ - '"VOLT:DC"' - ] + ik.keithley.Keithley6514, ["FUNCTION?", 'FUNCTION "VOLT:DC"'], ['"VOLT:DC"'] ) as inst: assert inst.mode == inst.Mode.voltage inst.mode = inst.Mode.voltage @@ -67,14 +55,7 @@ def test_mode(): def test_trigger_source(): with expected_protocol( - ik.keithley.Keithley6514, - [ - "TRIGGER:SOURCE?", - "TRIGGER:SOURCE IMM" - ], - [ - "TLINK" - ] + ik.keithley.Keithley6514, ["TRIGGER:SOURCE?", "TRIGGER:SOURCE IMM"], ["TLINK"] ) as inst: assert inst.trigger_mode == inst.TriggerMode.tlink inst.trigger_mode = inst.TriggerMode.immediate @@ -82,14 +63,7 @@ def test_trigger_source(): def test_arm_source(): with expected_protocol( - ik.keithley.Keithley6514, - [ - "ARM:SOURCE?", - "ARM:SOURCE IMM" - ], - [ - "TIM" - ] + ik.keithley.Keithley6514, ["ARM:SOURCE?", "ARM:SOURCE IMM"], ["TIM"] ) as inst: assert inst.arm_source == inst.ArmSource.timer inst.arm_source = inst.ArmSource.immediate @@ -97,14 +71,7 @@ def test_arm_source(): def test_zero_check(): with expected_protocol( - ik.keithley.Keithley6514, - [ - "SYST:ZCH?", - "SYST:ZCH ON" - ], - [ - "OFF" - ] + ik.keithley.Keithley6514, ["SYST:ZCH?", "SYST:ZCH ON"], ["OFF"] ) as inst: assert inst.zero_check is False inst.zero_check = True @@ -112,14 +79,7 @@ def test_zero_check(): def test_zero_correct(): with expected_protocol( - ik.keithley.Keithley6514, - [ - "SYST:ZCOR?", - "SYST:ZCOR ON" - ], - [ - "OFF" - ] + ik.keithley.Keithley6514, ["SYST:ZCOR?", "SYST:ZCOR ON"], ["OFF"] ) as inst: assert inst.zero_correct is False inst.zero_correct = True @@ -131,27 +91,16 @@ def test_unit(): [ "FUNCTION?", ], - [ - '"VOLT:DC"' - ] + ['"VOLT:DC"'], ) as inst: - assert inst.unit == pq.volt + assert inst.unit == u.volt def test_auto_range(): with expected_protocol( ik.keithley.Keithley6514, - [ - "FUNCTION?", - "VOLT:DC:RANGE:AUTO?", - "FUNCTION?", - "VOLT:DC:RANGE:AUTO 1" - ], - [ - '"VOLT:DC"', - "0", - '"VOLT:DC"' - ] + ["FUNCTION?", "VOLT:DC:RANGE:AUTO?", "FUNCTION?", "VOLT:DC:RANGE:AUTO 1"], + ['"VOLT:DC"', "0", '"VOLT:DC"'], ) as inst: assert inst.auto_range is False inst.auto_range = True @@ -164,30 +113,19 @@ def test_input_range(): "FUNCTION?", "VOLT:DC:RANGE:UPPER?", "FUNCTION?", - "VOLT:DC:RANGE:UPPER {:e}".format(20) + f"VOLT:DC:RANGE:UPPER {20:e}", ], - [ - '"VOLT:DC"', - "10", - '"VOLT:DC"' - ] + ['"VOLT:DC"', "10", '"VOLT:DC"'], ) as inst: - assert inst.input_range == 10 * pq.volt - inst.input_range = 20 * pq.volt + assert inst.input_range == 10 * u.volt + inst.input_range = 20 * u.volt -@raises(ValueError) def test_input_range_invalid(): - with expected_protocol( - ik.keithley.Keithley6514, - [ - "FUNCTION?" - ], - [ - '"VOLT:DC"' - ] + with pytest.raises(ValueError), expected_protocol( + ik.keithley.Keithley6514, ["FUNCTION?"], ['"VOLT:DC"'] ) as inst: - inst.input_range = 10 * pq.volt + inst.input_range = 10 * u.volt def test_auto_config(): @@ -196,7 +134,7 @@ def test_auto_config(): [ "CONF:VOLT:DC", ], - [] + [], ) as inst: inst.auto_config(inst.Mode.voltage) @@ -208,13 +146,10 @@ def test_fetch(): "FETC?", "FUNCTION?", ], - [ - "1.0,1234,5678", - '"VOLT:DC"' - ] + ["1.0,1234,5678", '"VOLT:DC"'], ) as inst: reading, timestamp = inst.fetch() - assert reading == 1.0 * pq.volt + assert reading == 1.0 * u.volt assert timestamp == 1234 @@ -225,11 +160,8 @@ def test_read(): "READ?", "FUNCTION?", ], - [ - "1.0,1234,5678", - '"VOLT:DC"' - ] + ["1.0,1234,5678", '"VOLT:DC"'], ) as inst: reading, timestamp = inst.read_measurements() - assert reading == 1.0 * pq.volt + assert reading == 1.0 * u.volt assert timestamp == 1234 diff --git a/instruments/tests/test_lakeshore/__init__.py b/instruments/tests/test_lakeshore/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/instruments/tests/test_lakeshore/test_lakeshore340.py b/instruments/tests/test_lakeshore/test_lakeshore340.py new file mode 100644 index 000000000..976c8770a --- /dev/null +++ b/instruments/tests/test_lakeshore/test_lakeshore340.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python +""" +Module containing tests for the Lakeshore 340 +""" + +# IMPORTS #################################################################### + +import instruments as ik +from instruments.units import ureg as u +from instruments.tests import expected_protocol + +# TESTS ###################################################################### + +# pylint: disable=protected-access + +# TEST SENSOR CLASS # + + +def test_lakeshore340_sensor_init(): + """ + Test initialization of sensor class. + """ + with expected_protocol( + ik.lakeshore.Lakeshore340, + [], + [], + ) as cryo: + sensor = cryo.sensor[0] + assert sensor._parent is cryo + assert sensor._idx == 1 + + +def test_lakeshore340_sensor_temperature(): + """ + Receive a unitful temperature from a sensor. + """ + with expected_protocol( + ik.lakeshore.Lakeshore340, + ["KRDG?1"], + ["77"], + ) as cryo: + assert cryo.sensor[0].temperature == u.Quantity(77, u.K) diff --git a/instruments/tests/test_lakeshore/test_lakeshore370.py b/instruments/tests/test_lakeshore/test_lakeshore370.py new file mode 100644 index 000000000..e24ecf488 --- /dev/null +++ b/instruments/tests/test_lakeshore/test_lakeshore370.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python +""" +Module containing tests for the Lakeshore 370 +""" + +# IMPORTS #################################################################### + +import pytest + +import instruments as ik +from instruments.units import ureg as u +from instruments.tests import expected_protocol + +# TESTS ###################################################################### + +# pylint: disable=redefined-outer-name,protected-access + +# PYTEST FIXTURES FOR INITIALIZATION # + + +@pytest.fixture +def init(): + """Returns the command the instrument sends at initaliation.""" + return "IEEE 3,0" + + +# TEST SENSOR CLASS # + + +def test_lakeshore370_channel_init(init): + """ + Test initialization of channel class. + """ + with expected_protocol( + ik.lakeshore.Lakeshore370, + [init], + [], + ) as lsh: + channel = lsh.channel[7] + assert channel._parent is lsh + assert channel._idx == 8 + + +def test_lakeshore370_channel_resistance(init): + """ + Receive a unitful resistance from a channel. + """ + with expected_protocol( + ik.lakeshore.Lakeshore370, + [init, "RDGR? 1"], + ["100."], + ) as lsh: + assert lsh.channel[0].resistance == u.Quantity(100, u.ohm) diff --git a/instruments/tests/test_lakeshore/test_lakeshore475.py b/instruments/tests/test_lakeshore/test_lakeshore475.py new file mode 100644 index 000000000..7a8a40c11 --- /dev/null +++ b/instruments/tests/test_lakeshore/test_lakeshore475.py @@ -0,0 +1,479 @@ +#!/usr/bin/env python +""" +Module containing tests for the Lakeshore 475 Gaussmeter +""" + +# IMPORTS #################################################################### + +import pytest + +import instruments as ik +from instruments.units import ureg as u +from instruments.tests import expected_protocol + +# TESTS ###################################################################### + + +# TEST LAKESHORE475 CLASS PROPERTIES # + + +def test_lakeshore475_field(): + """ + Get field from connected probe unitful. + """ + with expected_protocol( + ik.lakeshore.Lakeshore475, + ["RDGFIELD?", "UNIT?"], + ["200.", "2"], + ) as lsh: + assert lsh.field == u.Quantity(200.0, u.tesla) + + +def test_lakeshore475_field_units(): + """ + Get / set field unit on device. + """ + with expected_protocol( + ik.lakeshore.Lakeshore475, + ["UNIT?", "UNIT 2"], + ["3"], + ) as lsh: + assert lsh.field_units == u.oersted + lsh.field_units = u.tesla + + +def test_lakeshore475_field_units_invalid_unit(): + """ + Raise a ValueError if an invalid unit is given. + """ + with expected_protocol( + ik.lakeshore.Lakeshore475, + [], + [], + ) as lsh: + with pytest.raises(ValueError) as exc_info: + lsh.field_units = u.m + exc_msg = exc_info.value.args[0] + assert exc_msg == "Not an acceptable Python quantities object" + + +def test_lakeshore475_field_units_not_a_unit(): + """ + Raise a ValueError if something else than a quantity is given. + """ + with expected_protocol( + ik.lakeshore.Lakeshore475, + [], + [], + ) as lsh: + with pytest.raises(TypeError) as exc_info: + lsh.field_units = 42 + exc_msg = exc_info.value.args[0] + assert exc_msg == "Field units must be a Python quantity" + + +def test_lakeshore475_temp_units(): + """ + Get / set temperature unit on device. + """ + with expected_protocol( + ik.lakeshore.Lakeshore475, + ["TUNIT?", "TUNIT 2"], + ["1"], + ) as lsh: + assert lsh.temp_units == u.celsius + lsh.temp_units = u.kelvin + + +def test_lakeshore475_temp_units_invalid_unit(): + """ + Raise a ValueError if an invalid unit is given. + """ + with expected_protocol( + ik.lakeshore.Lakeshore475, + [], + [], + ) as lsh: + with pytest.raises(TypeError) as exc_info: + lsh.temp_units = u.fahrenheit + exc_msg = exc_info.value.args[0] + assert exc_msg == "Not an acceptable Python quantities object" + + +def test_lakeshore475_temp_units_not_a_unit(): + """ + Raise a ValueError if something else than a quantity is given. + """ + with expected_protocol( + ik.lakeshore.Lakeshore475, + [], + [], + ) as lsh: + with pytest.raises(TypeError) as exc_info: + lsh.temp_units = 42 + exc_msg = exc_info.value.args[0] + assert exc_msg == "Temperature units must be a Python quantity" + + +def test_lakeshore475_field_setpoint(): + """ + Get / set field set point. + """ + with expected_protocol( + ik.lakeshore.Lakeshore475, + [ + "CSETP?", + "UNIT?", + "UNIT?", + "CSETP 1.0", # send 1 tesla + "UNIT?", + "CSETP 23.0", # send 23 unitless (equals gauss) + ], + ["10.", "1", "2", "1"], + ) as lsh: + assert lsh.field_setpoint == u.Quantity(10, u.gauss) + + lsh.field_setpoint = u.Quantity(1.0, u.tesla) + lsh.field_setpoint = 23.0 + + +def test_lakeshore475_field_setpoint_wrong_units(): + """ + Setting the field setpoint with the wrong units + """ + with expected_protocol( + ik.lakeshore.Lakeshore475, + [ + "UNIT?", + ], + ["1"], + ) as lsh: + with pytest.raises(ValueError) as exc_info: + lsh.field_setpoint = u.Quantity(1.0, u.tesla) + exc_msg = exc_info.value.args[0] + assert "Field setpoint must be specified in the same units" in exc_msg + + +def test_lakeshore475_field_get_control_params(): + """ + Get field control parameters. + """ + with expected_protocol( + ik.lakeshore.Lakeshore475, + ["CPARAM?", "UNIT?"], + ["+1.0E+0,+1.0E+1,+4.2E+1,+1.0E+2", "2"], # teslas + ) as lsh: + current_params = lsh.field_control_params + assert current_params == ( + 1.0, + 10.0, + u.Quantity(42.0, u.tesla / u.min), + u.Quantity(100.0, u.volt / u.min), + ) + + +def test_lakeshore475_field_set_control_params(): + """ + Set field control parameters, unitful and using assumed units. + """ + with expected_protocol( + ik.lakeshore.Lakeshore475, + ["UNIT?", "CPARAM 5.0,50.0,120.0,60.0", "UNIT?", "CPARAM 5.0,50.0,120.0,60.0"], + ["2", "2"], # teslas # teslas + ) as lsh: + # currently set units are used + lsh.field_control_params = ( + 5.0, + 50.0, + u.Quantity(120.0, u.tesla / u.min), + u.Quantity(60.0, u.volt / u.min), + ) + # no units are used + lsh.field_control_params = (5.0, 50.0, 120.0, 60.0) + + +def test_lakeshore475_field_set_control_params_not_a_tuple(): + """ + Set field control parameters with wrong type. + """ + with expected_protocol( + ik.lakeshore.Lakeshore475, + [], + [], + ) as lsh: + with pytest.raises(TypeError) as exc_info: + lsh.field_control_params = 42 + exc_msg = exc_info.value.args[0] + assert exc_msg == "Field control parameters must be specified as " " a tuple" + + +def test_lakeshore475_field_set_control_params_wrong_units(): + """ + Set field control parameters with the wrong units + """ + with expected_protocol( + ik.lakeshore.Lakeshore475, + [ + "UNIT?", + ], + [ + "1", # gauss + ], + ) as lsh: + with pytest.raises(ValueError) as exc_info: + lsh.field_control_params = ( + 5.0, + 50.0, + u.Quantity(120.0, u.tesla / u.min), + u.Quantity(60.0, u.volt / u.min), + ) + exc_msg = exc_info.value.args[0] + assert ( + "Field control params ramp rate must be specified in the same units" + in exc_msg + ) + + +def test_lakeshore475_p_value(): + """ + Get / set p-value. + """ + with expected_protocol( + ik.lakeshore.Lakeshore475, + [ + "CPARAM?", + "UNIT?", + "CPARAM?", + "UNIT?", + "UNIT?", + "CPARAM 5.0,10.0,42.0,100.0", + ], + [ + "+1.0E+0,+1.0E+1,+4.2E+1,+1.0E+2", + "2", # teslas + "+1.0E+0,+1.0E+1,+4.2E+1,+1.0E+2", + "2", # teslas + "2", + ], + ) as lsh: + assert lsh.p_value == 1.0 + lsh.p_value = 5.0 + + +def test_lakeshore475_i_value(): + """ + Get / set i-value. + """ + with expected_protocol( + ik.lakeshore.Lakeshore475, + [ + "CPARAM?", + "UNIT?", + "CPARAM?", + "UNIT?", + "UNIT?", + "CPARAM 1.0,5.0,42.0,100.0", + ], + [ + "+1.0E+0,+1.0E+1,+4.2E+1,+1.0E+2", + "2", # teslas + "+1.0E+0,+1.0E+1,+4.2E+1,+1.0E+2", + "2", # teslas + "2", + ], + ) as lsh: + assert lsh.i_value == 10.0 + lsh.i_value = 5.0 + + +def test_lakeshore475_ramp_rate(): + """ + Get / set ramp rate, unitful and not. + """ + with expected_protocol( + ik.lakeshore.Lakeshore475, + [ + "CPARAM?", + "UNIT?", + "UNIT?", + "CPARAM?", + "UNIT?", + "UNIT?", + "CPARAM 1.0,10.0,420.0,100.0", + "UNIT?", + "CPARAM?", + "UNIT?", + "UNIT?", + "CPARAM 1.0,10.0,420.0,100.0", + ], + [ + "+1.0E+0,+1.0E+1,+4.2E+1,+1.0E+2", + "2", # teslas + "2", + "+1.0E+0,+1.0E+1,+4.2E+1,+1.0E+2", + "2", # teslas + "2", + "2", + "+1.0E+0,+1.0E+1,+4.2E+1,+1.0E+2", + "2", + "2", + ], + ) as lsh: + assert lsh.ramp_rate == u.Quantity(42.0, u.tesla / u.min) + lsh.ramp_rate = u.Quantity(420.0, u.tesla / u.min) + lsh.ramp_rate = 420.0 + + +def test_lakeshore475_control_slope_limit(): + """ + Get / set slope limit, unitful and not. + """ + with expected_protocol( + ik.lakeshore.Lakeshore475, + [ + "CPARAM?", + "UNIT?", + "CPARAM?", + "UNIT?", + "UNIT?", + "CPARAM 1.0,10.0,42.0,42.0", + "CPARAM?", + "UNIT?", + "UNIT?", + "CPARAM 1.0,10.0,42.0,42.0", + ], + [ + "+1.0E+0,+1.0E+1,+4.2E+1,+1.0E+2", + "2", # teslas + "+1.0E+0,+1.0E+1,+4.2E+1,+1.0E+2", + "2", # teslas + "2", + "+1.0E+0,+1.0E+1,+4.2E+1,+1.0E+2", + "2", + "2", + ], + ) as lsh: + assert lsh.control_slope_limit == u.Quantity(100.0, u.V / u.min) + lsh.control_slope_limit = u.Quantity(42000.0, u.mV / u.min) + lsh.control_slope_limit = 42.0 + + +def test_lakeshore475_control_mode(): + """ + Get / set control mode. + """ + with expected_protocol( + ik.lakeshore.Lakeshore475, + ["CMODE?", "CMODE 1"], + ["0"], + ) as lsh: + assert not lsh.control_mode + lsh.control_mode = True + + +# TEST LAKESHORE475 CLASS METHODS # + + +def test_lakeshore475_change_measurement_mode(): + """ + Change the measurement mode with valid values and ensure properly + sent to device. + """ + with expected_protocol( + ik.lakeshore.Lakeshore475, + ["RDGMODE 1,2,3,2,1"], + [], + ) as lsh: + # parameters to send + mode = lsh.Mode.dc + resolution = 4 + filter_type = lsh.Filter.lowpass + peak_mode = lsh.PeakMode.pulse + peak_disp = lsh.PeakDisplay.positive + # send them + lsh.change_measurement_mode(mode, resolution, filter_type, peak_mode, peak_disp) + + +def test_lakeshore475_change_measurement_mode_mismatched_type(): + """ + Ensure that mismatched input type raises a TypeError. + """ + with expected_protocol( + ik.lakeshore.Lakeshore475, + [], + [], + ) as lsh: + # parameters to send + mode = lsh.Mode.dc + resolution = 4 + filter_type = lsh.Filter.lowpass + peak_mode = lsh.PeakMode.pulse + peak_disp = lsh.PeakDisplay.positive + # check mode + with pytest.raises(TypeError) as exc_info: + lsh.change_measurement_mode( + 42, resolution, filter_type, peak_mode, peak_disp + ) + exc_msg = exc_info.value.args[0] + assert ( + exc_msg == f"Mode setting must be a `Lakeshore475.Mode` " + f"value, got {type(42)} instead." + ) + # check resolution + with pytest.raises(TypeError) as exc_info: + lsh.change_measurement_mode(mode, 3.14, filter_type, peak_mode, peak_disp) + exc_msg = exc_info.value.args[0] + assert exc_msg == 'Parameter "resolution" must be an integer.' + # check filter_type + with pytest.raises(TypeError) as exc_info: + lsh.change_measurement_mode(mode, resolution, 42, peak_mode, peak_disp) + exc_msg = exc_info.value.args[0] + assert ( + exc_msg == f"Filter type setting must be a " + f"`Lakeshore475.Filter` value, " + f"got {type(42)} instead." + ) + # check peak_mode + with pytest.raises(TypeError) as exc_info: + lsh.change_measurement_mode(mode, resolution, filter_type, 42, peak_disp) + exc_msg = exc_info.value.args[0] + assert ( + exc_msg == f"Peak measurement type setting must be a " + f"`Lakeshore475.PeakMode` value, " + f"got {type(42)} instead." + ) + # check peak_display + with pytest.raises(TypeError) as exc_info: + lsh.change_measurement_mode(mode, resolution, filter_type, peak_mode, 42) + exc_msg = exc_info.value.args[0] + assert ( + exc_msg == f"Peak display type setting must be a " + f"`Lakeshore475.PeakDisplay` value, " + f"got {type(42)} instead." + ) + + +def test_lakeshore475_change_measurement_mode_invalid_resolution(): + """ + Ensure that mismatched input type raises a TypeError. + """ + with expected_protocol( + ik.lakeshore.Lakeshore475, + [], + [], + ) as lsh: + # parameters to send + mode = lsh.Mode.dc + filter_type = lsh.Filter.lowpass + peak_mode = lsh.PeakMode.pulse + peak_disp = lsh.PeakDisplay.positive + # check resolution too low + with pytest.raises(ValueError) as exc_info: + lsh.change_measurement_mode(mode, 2, filter_type, peak_mode, peak_disp) + exc_msg = exc_info.value.args[0] + assert exc_msg == "Only 3,4,5 are valid resolutions." + # check resolution too high + with pytest.raises(ValueError) as exc_info: + lsh.change_measurement_mode(mode, 6, filter_type, peak_mode, peak_disp) + exc_msg = exc_info.value.args[0] + assert exc_msg == "Only 3,4,5 are valid resolutions." diff --git a/instruments/tests/test_minghe/test_minghe_mhs5200a.py b/instruments/tests/test_minghe/test_minghe_mhs5200a.py new file mode 100644 index 000000000..1760091a2 --- /dev/null +++ b/instruments/tests/test_minghe/test_minghe_mhs5200a.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python +""" +Module containing tests for the MingHe MHS52000a +""" + +# IMPORTS #################################################################### + + +import pytest +from instruments.units import ureg as u + +import instruments as ik +from instruments.tests import expected_protocol + + +# TESTS ###################################################################### + + +def test_mhs_amplitude(): + with expected_protocol( + ik.minghe.MHS5200, + [":r1a", ":r2a", ":s1a660", ":s2a800"], + [":r1a330", ":r2a500", "ok", "ok"], + sep="\r\n", + ) as mhs: + assert mhs.channel[0].amplitude[0] == 3.3 * u.V + assert mhs.channel[1].amplitude[0] == 5.0 * u.V + mhs.channel[0].amplitude = 6.6 * u.V + mhs.channel[1].amplitude = 8.0 * u.V + + +def test_mhs_amplitude_dbm_notimplemented(): + with expected_protocol(ik.minghe.MHS5200, [], [], sep="\r\n") as mhs: + with pytest.raises(NotImplementedError): + mhs.channel[0].amplitude = u.Quantity(6.6, u.dBm) + + +def test_mhs_duty_cycle(): + with expected_protocol( + ik.minghe.MHS5200, + [":r1d", ":r2d", ":s1d6", ":s2d80"], + [":r1d010", ":r2d100", "ok", "ok"], + sep="\r\n", + ) as mhs: + assert mhs.channel[0].duty_cycle == 1.0 + assert mhs.channel[1].duty_cycle == 10.0 + mhs.channel[0].duty_cycle = 0.06 + mhs.channel[1].duty_cycle = 0.8 + + +def test_mhs_enable(): + with expected_protocol( + ik.minghe.MHS5200, + [":r1b", ":r2b", ":s1b0", ":s2b1"], + [":r1b1", ":r2b0", "ok", "ok"], + sep="\r\n", + ) as mhs: + assert mhs.channel[0].enable + assert not mhs.channel[1].enable + mhs.channel[0].enable = False + mhs.channel[1].enable = True + + +def test_mhs_frequency(): + with expected_protocol( + ik.minghe.MHS5200, + [":r1f", ":r2f", ":s1f600000", ":s2f800000"], + [":r1f3300000", ":r2f50000000", "ok", "ok"], + sep="\r\n", + ) as mhs: + assert mhs.channel[0].frequency == 33.0 * u.kHz + assert mhs.channel[1].frequency == 500.0 * u.kHz + mhs.channel[0].frequency = 6 * u.kHz + mhs.channel[1].frequency = 8 * u.kHz + + +def test_mhs_offset(): + with expected_protocol( + ik.minghe.MHS5200, + [":r1o", ":r2o", ":s1o60", ":s2o180"], + [":r1o120", ":r2o0", "ok", "ok"], + sep="\r\n", + ) as mhs: + assert mhs.channel[0].offset == 0 + assert mhs.channel[1].offset == -1.2 + mhs.channel[0].offset = -0.6 + mhs.channel[1].offset = 0.6 + + +def test_mhs_phase(): + with expected_protocol( + ik.minghe.MHS5200, + [":r1p", ":r2p", ":s1p60", ":s2p180"], + [":r1p120", ":r2p0", "ok", "ok"], + sep="\r\n", + ) as mhs: + assert mhs.channel[0].phase == 120 * u.degree + assert mhs.channel[1].phase == 0 * u.degree + mhs.channel[0].phase = 60 + mhs.channel[1].phase = 180 + + +def test_mhs_wave_type(): + with expected_protocol( + ik.minghe.MHS5200, + [":r1w", ":r2w", ":s1w2", ":s2w3"], + [":r1w0", ":r2w1", "ok", "ok"], + sep="\r\n", + ) as mhs: + assert mhs.channel[0].function == mhs.Function.sine + assert mhs.channel[1].function == mhs.Function.square + mhs.channel[0].function = mhs.Function.triangular + mhs.channel[1].function = mhs.Function.sawtooth_up + + +def test_mhs_serial_number(): + with expected_protocol( + ik.minghe.MHS5200, + [":r0c"], + [ + ":r0c5225A1", + ], + sep="\r\n", + ) as mhs: + assert mhs.serial_number == "5225A1" + + +def test_mhs_get_amplitude(): + """Raise NotImplementedError when trying to get amplitude""" + with expected_protocol(ik.minghe.MHS5200, [], [], sep="\r\n") as mhs: + with pytest.raises(NotImplementedError): + mhs._get_amplitude_() + + +def test_mhs_set_amplitude(): + """Raise NotImplementedError when trying to set amplitude""" + with expected_protocol(ik.minghe.MHS5200, [], [], sep="\r\n") as mhs: + with pytest.raises(NotImplementedError): + mhs._set_amplitude_(1, 2) diff --git a/instruments/tests/test_named_struct.py b/instruments/tests/test_named_struct.py new file mode 100644 index 000000000..542a96905 --- /dev/null +++ b/instruments/tests/test_named_struct.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python +""" +Module containing tests for named structures. +""" + +# IMPORTS #################################################################### + + +from unittest import TestCase + +from hypothesis import given +import hypothesis.strategies as st + +from instruments.named_struct import Field, StringField, Padding, NamedStruct + + +# TESTS ###################################################################### + +# We disable pylint warnings that are not as applicable for unit tests. +# pylint: disable=no-member,protected-access,blacklisted-name,missing-docstring,no-self-use + + +class TestNamedStruct(TestCase): + @given( + st.integers(min_value=0, max_value=0x7FFF * 2 + 1), + st.integers(min_value=0, max_value=0xFF), + ) + def test_roundtrip(self, var1, var2): + class Foo(NamedStruct): + a = Field("H") + padding = Padding(12) + b = Field("B") + + foo = Foo(a=var1, b=var2) + assert Foo.unpack(foo.pack()) == foo + + def test_str(self): + class Foo(NamedStruct): + a = StringField(8, strip_null=False) + b = StringField(9, strip_null=True) + c = StringField(2, encoding="utf-8") + + foo = Foo(a="0123456\x00", b="abc", c="α") + assert Foo.unpack(foo.pack()) == foo + + # Also check that we can get fields out directly. + self.assertEqual(foo.a, "0123456\x00") + self.assertEqual(foo.b, "abc") + self.assertEqual(foo.c, "α") + + def test_negative_len(self): + """ + Checks whether negative field lengths correctly raise. + """ + with self.assertRaises(TypeError): + + class Foo(NamedStruct): # pylint: disable=unused-variable + a = StringField(-1) + + def test_equality(self): + class Foo(NamedStruct): + a = Field("H") + b = Field("B") + c = StringField(5, encoding="utf8", strip_null=True) + + foo1 = Foo(a=0x1234, b=0x56, c="ω") + foo2 = Foo(a=0xABCD, b=0xEF, c="α") + + assert foo1 == foo1 + assert foo1 != foo2 diff --git a/instruments/tests/test_newport/test_agilis.py b/instruments/tests/test_newport/test_agilis.py new file mode 100644 index 000000000..440343e27 --- /dev/null +++ b/instruments/tests/test_newport/test_agilis.py @@ -0,0 +1,409 @@ +#!/usr/bin/env python +""" +Module containing tests for the Agilis Controller +""" + +# IMPORTS ##################################################################### + +import time + +import pytest + +import instruments as ik +from instruments.tests import expected_protocol + +# TESTS ####################################################################### + + +# pylint: disable=protected-access + + +# FIXTURES # + + +@pytest.fixture(autouse=True) +def mock_time(mocker): + """Mock `time.sleep` for and set to zero as autouse fixture.""" + return mocker.patch.object(time, "sleep", return_value=None) + + +# CONTROLLER TESTS # + + +def test_aguc2_enable_remote_mode(): + """ + Check enabling of remote mode. + """ + with expected_protocol(ik.newport.AGUC2, ["MR", "ML"], [], sep="\r\n") as agl: + agl.enable_remote_mode = True + assert agl.enable_remote_mode is True + agl.enable_remote_mode = False + assert agl.enable_remote_mode is False + + +def test_aguc2_error_previous_command_no_error(): + """Test return of an error value (`No Error`) from previous command.""" + with expected_protocol(ik.newport.AGUC2, ["TE"], ["TE0"], sep="\r\n") as agl: + assert agl.error_previous_command == "No error" + + +def test_aguc2_error_previous_command(): + """ + Check the call error of previous command routine. Note that the test will + return "Error code must be given as an integer." will be returned because + no actual error code is fed to the error message checker. + """ + with expected_protocol(ik.newport.AGUC2, ["TE"], [], sep="\r\n") as agl: + assert agl.error_previous_command == "Error code query failed." + + +def test_aguc2_firmware_version(): + """ + Check firmware version + AG-UC2 v2.2.1 + """ + with expected_protocol( + ik.newport.AGUC2, ["VE"], ["AG-UC2 v2.2.1"], sep="\r\n" + ) as agl: + assert agl.firmware_version == "AG-UC2 v2.2.1" + + +def test_aguc2_limit_status(): + """ + Check the limit status routine. + """ + with expected_protocol( + ik.newport.AGUC2, ["MR", "PH"], ["PH0"], sep="\r\n" # initialize remote mode + ) as agl: + assert agl.limit_status == "PH0" + + +def test_aguc2_sleep_time(): + """ + Check setting, getting the sleep time. + """ + with expected_protocol(ik.newport.AGUC2, [], [], sep="\r\n") as agl: + agl.sleep_time = 3 + assert agl.sleep_time == 3 + with pytest.raises(ValueError): + agl.sleep_time = -3.14 + + +def test_aguc2_reset_controller(): + """ + Check reset controller function. + """ + with expected_protocol(ik.newport.AGUC2, ["RS"], [], sep="\r\n") as agl: + agl.reset_controller() + assert agl.enable_remote_mode is False + + +def test_aguc2_ag_sendcmd(): + """ + Check agilis sendcommand wrapper. + """ + with expected_protocol( + ik.newport.AGUC2, ["MR"], [], sep="\r\n" # some command, here remote mode + ) as agl: + agl.ag_sendcmd("MR") + + +def test_aguc2_ag_query(): + """ + Check agilis query wrapper. + """ + with expected_protocol( + ik.newport.AGUC2, ["VE"], ["AG-UC2 v2.2.1"], sep="\r\n" + ) as agl: + assert agl.ag_query("VE") == "AG-UC2 v2.2.1" + + +def test_aguc2_ag_query_io_error(mocker): + """Respond with `Query timed out.` if IOError occurs.""" + # mock the query to raise an IOError + io_error_mock = mocker.Mock() + io_error_mock.side_effect = IOError + mocker.patch.object(ik.newport.AGUC2, "query", io_error_mock) + + with expected_protocol(ik.newport.AGUC2, [], [], sep="\r\n") as agl: + assert agl.ag_query("VE") == "Query timed out." + + +# AXIS TESTS # + + +@pytest.mark.parametrize("axis", ik.newport.AGUC2.Axes) +def test_aguc2_axis_init_enum(axis): + """Initialize an axis externally with an enum.""" + with expected_protocol(ik.newport.AGUC2, [], [], sep="\r\n") as agl: + ax = ik.newport.agilis.AGUC2.Axis(agl, axis) + assert ax._ax == axis.value + + +def test_aguc2_axis_init_wrong_type(): + """Raise TypeError when not initialized from AGUC2 parent class.""" + with pytest.raises(TypeError) as err_info: + ik.newport.agilis.AGUC2.Axis(42, ik.newport.AGUC2.Axes.X) + err_msg = err_info.value.args[0] + assert err_msg == "Don't do that." + + +@pytest.mark.parametrize("axis", ik.newport.AGUC2.Axes) +@pytest.mark.parametrize("still", (True, False)) +def test_aguc2_axis_am_i_still(axis, still): + """Check if axis is still or not.""" + with expected_protocol( + ik.newport.AGUC2, + [ + "MR", # initialize remote mode + f"{axis.value} TS", + ], + [f"{axis.value}TS {int(not still)}"], + sep="\r\n", + ) as agl: + assert agl.axis[axis].am_i_still() == still + + +def test_aguc2_axis_am_i_still_io_error(): + """Raise IOError if max retries achieved.""" + with expected_protocol( + ik.newport.AGUC2, + ["MR", "1 TS", "2 TS", "2 TS", "2 TS"], # initialize remote mode + [], + sep="\r\n", + ) as agl: + with pytest.raises(IOError): + agl.axis["X"].am_i_still(max_retries=1) + with pytest.raises(IOError): + agl.axis["Y"].am_i_still(max_retries=3) + + +@pytest.mark.parametrize("axis", ik.newport.AGUC2.Axes) +def test_aguc2_axis_axis_status_not_moving(axis): + """Check status of axis and return axis not moving.""" + with expected_protocol( + ik.newport.AGUC2, + [ + "MR", # initialize remote mode + f"{axis.value} TS", + ], + [f"{axis.value}TS0"], + sep="\r\n", + ) as agl: + assert agl.axis[axis].axis_status == "Ready (not moving)." + + +def test_aguc2_axis_axis_status(): + """ + Check the status of the axis. Note that the test will return + "Status code query failed." since no instrument is connected. + """ + with expected_protocol( + ik.newport.AGUC2, + ["MR", "1 TS", "2 TS"], # initialize remote mode + [], + sep="\r\n", + ) as agl: + assert agl.axis["X"].axis_status == "Status code query failed." + assert agl.axis["Y"].axis_status == "Status code query failed." + + +def test_aguc2_axis_jog(): + """Get / set jog function.""" + with expected_protocol( + ik.newport.AGUC2, + ["MR", "1 JA 3", "1 JA?", "2 JA -4", "2 JA?"], # initialize remote mode + ["1JA3", "2JA-4"], + sep="\r\n", + ) as agl: + agl.axis["X"].jog = 3 + assert agl.axis["X"].jog == 3 + agl.axis["Y"].jog = -4 + assert agl.axis["Y"].jog == -4 + with pytest.raises(ValueError): + agl.axis["X"].jog = -5 + with pytest.raises(ValueError): + agl.axis["Y"].jog = 5 + + +def test_aguc2_axis_number_of_steps(): + """ + Check the number of steps function. + """ + with expected_protocol( + ik.newport.AGUC2, + [ + "MR", # initialize remote mode + "1 TP", + ], + ["1TP0"], + sep="\r\n", + ) as agl: + assert agl.axis["X"].number_of_steps == 0 + + +def test_aguc2_axis_move_relative(): + """ + Check the move relative function. + """ + with expected_protocol( + ik.newport.AGUC2, + ["MR", "1 PR 1000", "1 PR?", "2 PR -340", "2 PR?"], # initialize remote mode + ["1PR1000", "2PR-340"], + sep="\r\n", + ) as agl: + agl.axis["X"].move_relative = 1000 + assert agl.axis["X"].move_relative == 1000 + agl.axis["Y"].move_relative = -340 + assert agl.axis["Y"].move_relative == -340 + with pytest.raises(ValueError): + agl.axis["X"].move_relative = 2147483648 + with pytest.raises(ValueError): + agl.axis["Y"].move_relative = -2147483649 + + +def test_aguc2_axis_move_to_limit(): + """ + Check for move to limit function. + This function is UNTESTED to work, here simply command sending is checked + """ + with expected_protocol( + ik.newport.AGUC2, + ["MR", "2 MA 3", "2 MA?"], # initialize remote mode + ["2MA42"], + sep="\r\n", + ) as agl: + agl.axis["Y"].move_to_limit = 3 + assert agl.axis["Y"].move_to_limit == 42 + with pytest.raises(ValueError): + agl.axis["Y"].move_to_limit = -5 + with pytest.raises(ValueError): + agl.axis["X"].move_to_limit = 5 + + +def test_aguc2_axis_step_amplitude(): + """ + Check for step amplitude function + """ + with expected_protocol( + ik.newport.AGUC2, + [ + "MR", # initialize remote mode + "1 SU-?", + "1 SU+?", + "1 SU -35", + "1 SU 47", + "1 SU -23", + "1 SU 13", + ], + ["1SU-35", "1SU+35"], + sep="\r\n", + ) as agl: + assert agl.axis["X"].step_amplitude == (-35, 35) + agl.axis["X"].step_amplitude = -35 + agl.axis["X"].step_amplitude = 47 + agl.axis["X"].step_amplitude = (-23, 13) + with pytest.raises(ValueError): + agl.axis["X"].step_amplitude = 0 + with pytest.raises(ValueError): + agl.axis["Y"].step_amplitude = -51 + with pytest.raises(ValueError): + agl.axis["Y"].step_amplitude = 51 + + +def test_aguc2_axis_step_delay(): + """ + Check the step delay function. + """ + with expected_protocol( + ik.newport.AGUC2, + ["MR", "2 DL?", "1 DL 1000", "1 DL 200"], # initialize remote mode + ["2DL0"], + sep="\r\n", + ) as agl: + assert agl.axis["Y"].step_delay == 0 + agl.axis["X"].step_delay = 1000 + agl.axis["X"].step_delay = 200 + with pytest.raises(ValueError): + agl.axis["X"].step_delay = -1 + with pytest.raises(ValueError): + agl.axis["Y"].step_delay = 2000001 + + +def test_aguc2_axis_stop(): + """ + Check the stop function. + """ + with expected_protocol( + ik.newport.AGUC2, + ["MR", "1 ST", "2 ST"], # initialize remote mode + [], + sep="\r\n", + ) as agl: + agl.axis["X"].stop() + agl.axis["Y"].stop() + + +def test_aguc2_axis_zero_position(): + """ + Check the stop function. + """ + with expected_protocol( + ik.newport.AGUC2, + ["MR", "1 ZP", "2 ZP"], # initialize remote mode + [], + sep="\r\n", + ) as agl: + agl.axis["X"].zero_position() + agl.axis["Y"].zero_position() + + +# FUNCTION TESTS # + + +def test_agilis_error_message(): + # regular error messages + assert ik.newport.agilis.agilis_error_message(0) == "No error" + assert ( + ik.newport.agilis.agilis_error_message(-6) == "Not allowed in " "current state" + ) + # out of range integers + assert ik.newport.agilis.agilis_error_message(1) == "An unknown error " "occurred." + assert ik.newport.agilis.agilis_error_message(-7) == "An unknown error " "occurred." + # non-integers + assert ( + ik.newport.agilis.agilis_error_message(-7.5) == "Error code is " + "not an integer." + ) + assert ( + ik.newport.agilis.agilis_error_message("TE0") == "Error code is " + "not an integer." + ) + + +def test_agilis_status_message(): + # regular status messages + assert ik.newport.agilis.agilis_status_message(0) == "Ready (not moving)." + assert ( + ik.newport.agilis.agilis_status_message(3) + == "Moving to limit (currently executing " + "`measure_current_position`, `move_to_limit`, or " + "`move_absolute` command)." + ) + # out of range integers + assert ( + ik.newport.agilis.agilis_status_message(4) == "An unknown " "status occurred." + ) + assert ( + ik.newport.agilis.agilis_status_message(-1) == "An unknown " "status occurred." + ) + # non integers + assert ( + ik.newport.agilis.agilis_status_message(3.14) == "Status code is " + "not an integer." + ) + assert ( + ik.newport.agilis.agilis_status_message("1TS0") == "Status code " + "is not an " + "integer." + ) diff --git a/instruments/tests/test_newport/test_errors.py b/instruments/tests/test_newport/test_errors.py new file mode 100644 index 000000000..1e3c8cbc2 --- /dev/null +++ b/instruments/tests/test_newport/test_errors.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python +""" +Module containing tests for NewportError class +""" + +# IMPORTS #################################################################### + + +import datetime + +from instruments.newport.errors import NewportError + + +# TESTS ###################################################################### + + +# pylint: disable=protected-access + + +def test_init_none(): + """Initialized with both arguments as `None`.""" + cls = NewportError() + assert isinstance(cls._timestamp, datetime.timedelta) + assert cls._errcode is None + assert cls._axis is None + + +def test_init_with_timestamp(): + """Initialized with a time stamp.""" + timestamp = datetime.datetime.now() + cls = NewportError(timestamp=timestamp) + assert isinstance(cls._timestamp, datetime.timedelta) + + +def test_init_with_error_code(): + """Initialize with non-axis specific error code.""" + err_code = 7 # parameter out of range + cls = NewportError(errcode=err_code) + assert cls._axis is None + assert cls._errcode == 7 + + +def test_init_with_error_code_axis(): + """Initialize with axis-specific error code.""" + err_code = 313 # ax 3 not enabled + cls = NewportError(errcode=err_code) + assert cls._axis == 3 + assert cls._errcode == 13 + + +def test_get_message(): + """Get the message for a given error code.""" + err_code = "7" + cls = NewportError() + assert cls.get_message(err_code) == cls.messageDict[err_code] + + +def test_timestamp(): + """Get the timestamp for a given error.""" + cls = NewportError() + assert cls.timestamp == cls._timestamp + + +def test_errcode(): + """Get the error code reported by device.""" + cls = NewportError(errcode=7) + assert cls.errcode == cls._errcode + + +def test_axis(): + """Get axis for given error code.""" + cls = NewportError(errcode=313) + assert cls.axis == cls._axis diff --git a/instruments/tests/test_newport/test_newport_pmc8742.py b/instruments/tests/test_newport/test_newport_pmc8742.py new file mode 100644 index 000000000..8cc9cee00 --- /dev/null +++ b/instruments/tests/test_newport/test_newport_pmc8742.py @@ -0,0 +1,567 @@ +#!/usr/bin/env python +""" +Tests for the Newport Picomotor Controller 8742. +""" + +# IMPORTS ##################################################################### + +from hypothesis import given, strategies as st +import pytest + +import instruments as ik +from instruments.units import ureg as u +from instruments.tests import expected_protocol + +# pylint: disable=protected-access + + +# INSTRUMENT # + + +def test_init(): + """Initialize a new Picomotor PMC8742 instrument.""" + with expected_protocol( + ik.newport.PicoMotorController8742, [], [], sep="\r\n" + ) as inst: + assert inst.terminator == "\r\n" + assert not inst.multiple_controllers + + +def test_controller_address(): + """Set and get controller address.""" + with expected_protocol( + ik.newport.PicoMotorController8742, ["SA2", "SA?"], ["2"], sep="\r\n" + ) as inst: + inst.controller_address = 2 + assert inst.controller_address == 2 + + +def test_controller_configuration(): + """Set and get controller configuration.""" + with expected_protocol( + ik.newport.PicoMotorController8742, + ["ZZ11", "ZZ11", "ZZ11", "ZZ?"], + ["11"], + sep="\r\n", + ) as inst: + inst.controller_configuration = 3 + inst.controller_configuration = 0b11 + inst.controller_configuration = "11" + assert inst.controller_configuration == "11" + + +def test_dhcp_mode(): + """Set and get DHCP mode.""" + with expected_protocol( + ik.newport.PicoMotorController8742, + ["IPMODE0", "IPMODE1", "IPMODE?"], + ["1"], + sep="\r\n", + ) as inst: + inst.dhcp_mode = False + inst.dhcp_mode = True + assert inst.dhcp_mode + + +def test_error_code(): + """Get error code.""" + with expected_protocol( + ik.newport.PicoMotorController8742, ["TE?"], ["0"], sep="\r\n" + ) as inst: + assert inst.error_code == 0 + + +def test_error_code_and_message(): + """Get error code and message as tuple.""" + with expected_protocol( + ik.newport.PicoMotorController8742, + ["TB?"], + ["0, NO ERROR DETECTED"], + sep="\r\n", + ) as inst: + err_expected = (0, "NO ERROR DETECTED") + err_received = inst.error_code_and_message + assert err_received == err_expected + assert isinstance(err_received, tuple) + + +def test_firmware_version(): + """Get firmware version.""" + with expected_protocol( + ik.newport.PicoMotorController8742, ["VE?"], ["0123456789"], sep="\r\n" + ) as inst: + assert inst.firmware_version == "0123456789" + + +def test_gateway(): + """Set / get gateway.""" + ip_addr = "192.168.1.1" + with expected_protocol( + ik.newport.PicoMotorController8742, + [f"GATEWAY {ip_addr}", "GATEWAY?"], + [f"{ip_addr}"], + sep="\r\n", + ) as inst: + inst.gateway = ip_addr + assert inst.gateway == ip_addr + + +def test_hostname(): + """Set / get hostname.""" + host = "192.168.1.1" + with expected_protocol( + ik.newport.PicoMotorController8742, + [f"HOSTNAME {host}", "HOSTNAME?"], + [f"{host}"], + sep="\r\n", + ) as inst: + inst.hostname = host + assert inst.hostname == host + + +def test_ip_address(): + """Set / get ip address.""" + ip_addr = "192.168.1.1" + with expected_protocol( + ik.newport.PicoMotorController8742, + [f"IPADDR {ip_addr}", "IPADDR?"], + [f"{ip_addr}"], + sep="\r\n", + ) as inst: + inst.ip_address = ip_addr + assert inst.ip_address == ip_addr + + +def test_mac_address(): + """Set / get mac address.""" + mac_addr = "5827809, 8087" + with expected_protocol( + ik.newport.PicoMotorController8742, ["MACADDR?"], [f"{mac_addr}"], sep="\r\n" + ) as inst: + assert inst.mac_address == mac_addr + + +def test_name(): + """Get name of the current instrument.""" + with expected_protocol( + ik.newport.PicoMotorController8742, ["*IDN?"], ["NAME"], sep="\r\n" + ) as inst: + assert inst.name == "NAME" + + +def test_netmask(): + """Set / get netmask.""" + ip_addr = "192.168.1.1" + with expected_protocol( + ik.newport.PicoMotorController8742, + [f"NETMASK {ip_addr}", "NETMASK?"], + [f"{ip_addr}"], + sep="\r\n", + ) as inst: + inst.netmask = ip_addr + assert inst.netmask == ip_addr + + +def test_scan_controller(): + """Scan connected controllers.""" + with expected_protocol( + ik.newport.PicoMotorController8742, ["SC?"], ["11"], sep="\r\n" + ) as inst: + assert inst.scan_controllers == "11" + + +def test_scan_done(): + """Query if a controller scan is completed.""" + with expected_protocol( + ik.newport.PicoMotorController8742, ["SD?", "SD?"], ["1", "0"], sep="\r\n" + ) as inst: + assert inst.scan_done + assert not inst.scan_done + + +def test_abort_motion(): + """Abort all motion.""" + with expected_protocol( + ik.newport.PicoMotorController8742, ["AB"], [], sep="\r\n" + ) as inst: + inst.abort_motion() + + +def test_motor_check(): + """Check the connected motors.""" + with expected_protocol( + ik.newport.PicoMotorController8742, ["MC"], [], sep="\r\n" + ) as inst: + inst.motor_check() + + +@pytest.mark.parametrize("mode", [0, 1, 2]) +def test_scan(mode): + """Scan address configuration of motors for default and other modes.""" + with expected_protocol( + ik.newport.PicoMotorController8742, ["SC2", f"SC{mode}"], [], sep="\r\n" + ) as inst: + inst.scan() + inst.scan(mode) + + +def test_purge(): + """Purge the memory.""" + with expected_protocol( + ik.newport.PicoMotorController8742, ["XX"], [], sep="\r\n" + ) as inst: + inst.purge() + + +@pytest.mark.parametrize("mode", [0, 1]) +def test_recall_parameters(mode): + """Recall parameters, by default the factory set values.""" + with expected_protocol( + ik.newport.PicoMotorController8742, ["*RCL0", f"*RCL{mode}"], [], sep="\r\n" + ) as inst: + inst.recall_parameters() + inst.recall_parameters(mode) + + +def test_reset(): + """Soft reset of the controller.""" + with expected_protocol( + ik.newport.PicoMotorController8742, ["*RST"], [], sep="\r\n" + ) as inst: + inst.reset() + + +def test_save_settings(): + """Save settings of the controller.""" + with expected_protocol( + ik.newport.PicoMotorController8742, ["SM"], [], sep="\r\n" + ) as inst: + inst.save_settings() + + +def test_query_bad_header(): + """Ensure stripping of bad header if present, see comment in query.""" + retval = b"\xff\xfd\x03\xff\xfb\x01192.168.2.161" + val_expected = "192.168.2.161" + with expected_protocol( + ik.newport.PicoMotorController8742, ["IPADDR?"], [retval], sep="\r\n" + ) as inst: + assert inst.ip_address == val_expected + + +# AXIS SPECIFIC COMMANDS - CONTROLLER COMMANDS PER AXIS TESTED ABOVE # + + +@given(ax=st.integers(min_value=0, max_value=3)) +def test_axis_returns(ax): + """Return axis with given axis number testing all valid axes.""" + with expected_protocol( + ik.newport.PicoMotorController8742, [], [], sep="\r\n" + ) as inst: + axis = inst.axis[ax] + assert isinstance(axis, ik.newport.PicoMotorController8742.Axis) + assert axis._parent == inst + assert axis._idx == ax + 1 + assert axis._address == "" + + +def test_axis_returns_type_error(): + """Raise TypeError if parent class is not PicoMotorController8742.""" + with pytest.raises(TypeError): + _ = ik.newport.PicoMotorController8742.Axis(0, 0) + + +@given(ax=st.integers(min_value=4)) +def test_axis_return_index_error(ax): + """Raise IndexError if axis out of bounds and in one controller mode.""" + with expected_protocol( + ik.newport.PicoMotorController8742, [], [], sep="\r\n" + ) as inst: + with pytest.raises(IndexError): + _ = inst.axis[ax] + + +@given(val=st.integers(min_value=1, max_value=200000)) +def test_axis_acceleration(val): + """Set / get axis acceleration unitful and without units.""" + val_unit = u.Quantity(val, u.s ** -2) + val_unit_other = val_unit.to(u.min ** -2) + with expected_protocol( + ik.newport.PicoMotorController8742, + [f"1AC{val}", f"1AC{val}", "1AC?"], + [f"{val}"], + sep="\r\n", + ) as inst: + axis = inst.axis[0] + axis.acceleration = val + axis.acceleration = val_unit_other + assert axis.acceleration == val_unit + + +@given(val=st.integers().filter(lambda x: not 1 <= x <= 200000)) +def test_axis_acceleration_value_error(val): + """Raise ValueError if acceleration out of range.""" + with expected_protocol( + ik.newport.PicoMotorController8742, [], [], sep="\r\n" + ) as inst: + axis = inst.axis[0] + with pytest.raises(ValueError): + axis.acceleration = val + + +@given(val=st.integers(min_value=-2147483648, max_value=2147483647)) +def test_axis_home_position(val): + """Set / get axis home position.""" + with expected_protocol( + ik.newport.PicoMotorController8742, + [f"1DH{val}", "1DH?"], + [f"{val}"], + sep="\r\n", + ) as inst: + axis = inst.axis[0] + axis.home_position = val + assert axis.home_position == val + + +@pytest.mark.parametrize("val", [-2147483649, 2147483648]) +def test_axis_home_position_value_error(val): + """Raise ValueError if home position out of range.""" + with expected_protocol( + ik.newport.PicoMotorController8742, [], [], sep="\r\n" + ) as inst: + axis = inst.axis[0] + with pytest.raises(ValueError): + axis.home_position = val + + +@pytest.mark.parametrize("val", ["0", "1"]) +def test_axis_is_stopped(val): + """Query if axis is stopped.""" + exp_result = True if val == "1" else False + with expected_protocol( + ik.newport.PicoMotorController8742, ["1MD?"], [f"{val}"], sep="\r\n" + ) as inst: + axis = inst.axis[0] + assert axis.is_stopped == exp_result + + +@pytest.mark.parametrize("val", ik.newport.PicoMotorController8742.Axis.MotorType) +def test_axis_motor_type(val): + """Set / get motor type.""" + with expected_protocol( + ik.newport.PicoMotorController8742, + [f"1QM{val.value}", "1QM?"], + [f"{val.value}"], + sep="\r\n", + ) as inst: + axis = inst.axis[0] + axis.motor_type = val + assert axis.motor_type == val + + +def test_axis_motor_type_wrong_type(): + """Raise TypeError if not appropriate motor type.""" + with expected_protocol( + ik.newport.PicoMotorController8742, [], [], sep="\r\n" + ) as inst: + axis = inst.axis[0] + with pytest.raises(TypeError): + axis.motor_type = 2 + + +@given(val=st.integers(min_value=-2147483648, max_value=2147483647)) +def test_axis_move_absolute(val): + """Set / get axis move absolute.""" + with expected_protocol( + ik.newport.PicoMotorController8742, + [f"1PA{val}", "1PA?"], + [f"{val}"], + sep="\r\n", + ) as inst: + axis = inst.axis[0] + axis.move_absolute = val + assert axis.move_absolute == val + + +@pytest.mark.parametrize("val", [-2147483649, 2147483648]) +def test_axis_move_absolute_value_error(val): + """Raise ValueError if move absolute out of range.""" + with expected_protocol( + ik.newport.PicoMotorController8742, [], [], sep="\r\n" + ) as inst: + axis = inst.axis[0] + with pytest.raises(ValueError): + axis.move_absolute = val + + +@given(val=st.integers(min_value=-2147483648, max_value=2147483647)) +def test_axis_move_relative(val): + """Set / get axis move relative.""" + with expected_protocol( + ik.newport.PicoMotorController8742, + [f"1PR{val}", "1PR?"], + [f"{val}"], + sep="\r\n", + ) as inst: + axis = inst.axis[0] + axis.move_relative = val + assert axis.move_relative == val + + +@pytest.mark.parametrize("val", [-2147483649, 2147483648]) +def test_axis_move_relative_value_error(val): + """Raise ValueError if move relative out of range.""" + with expected_protocol( + ik.newport.PicoMotorController8742, [], [], sep="\r\n" + ) as inst: + axis = inst.axis[0] + with pytest.raises(ValueError): + axis.move_relative = val + + +def test_axis_position(): + """Query position of an axis.""" + with expected_protocol( + ik.newport.PicoMotorController8742, ["1TP?"], ["42"], sep="\r\n" + ) as inst: + axis = inst.axis[0] + assert axis.position == 42 + + +@given(val=st.integers(min_value=1, max_value=2000)) +def test_axis_velocity(val): + """Set / get axis velocity, unitful and unitless.""" + val_unit = u.Quantity(val, 1 / u.s) + val_unit_other = val_unit.to(1 / u.hour) + with expected_protocol( + ik.newport.PicoMotorController8742, + [f"1QM?", f"1VA{val}", f"1QM?", f"1VA{val}", "1VA?"], + ["3", "3", f"{val}"], + sep="\r\n", + ) as inst: + axis = inst.axis[0] + axis.velocity = val + axis.velocity = val_unit_other + assert axis.velocity == val_unit + + +@given(val=st.integers().filter(lambda x: not 1 <= x <= 2000)) +@pytest.mark.parametrize("motor", [0, 1, 3]) +def test_axis_velocity_value_error_regular(val, motor): + """Raise ValueError if velocity is out of range for non-tiny motor.""" + with expected_protocol( + ik.newport.PicoMotorController8742, ["1QM?"], [f"{motor}"], sep="\r\n" + ) as inst: + axis = inst.axis[0] + with pytest.raises(ValueError): + axis.velocity = val + + +@given(val=st.integers().filter(lambda x: not 1 <= x <= 1750)) +def test_axis_velocity_value_error_tiny(val): + """Raise ValueError if velocity is out of range for tiny motor.""" + with expected_protocol( + ik.newport.PicoMotorController8742, ["1QM?"], ["2"], sep="\r\n" + ) as inst: + axis = inst.axis[0] + with pytest.raises(ValueError): + axis.velocity = val + + +@pytest.mark.parametrize("direction", ["+", "-"]) +def test_axis_move_indefinite(direction): + """Move axis indefinitely.""" + with expected_protocol( + ik.newport.PicoMotorController8742, [f"1MV{direction}"], [], sep="\r\n" + ) as inst: + axis = inst.axis[0] + axis.move_indefinite(direction) + + +def test_axis_stop(): + """Stop axis.""" + with expected_protocol( + ik.newport.PicoMotorController8742, [f"1ST"], [], sep="\r\n" + ) as inst: + axis = inst.axis[0] + axis.stop() + + +# SOME ADDITIONAL TESTS FOR MAIN / SECONDARY CONTROLLER SETUP # + + +def test_multi_controllers(): + """Enable and disable multiple controllers.""" + with expected_protocol( + ik.newport.PicoMotorController8742, [], [], sep="\r\n" + ) as inst: + inst.multiple_controllers = True + assert inst.multiple_controllers + inst.multiple_controllers = False + assert not inst.multiple_controllers + + +@given(ax=st.integers(min_value=0, max_value=31 * 4 - 1)) +def test_axis_return_multi(ax): + """Return axis properly for multi-controller setup.""" + with expected_protocol( + ik.newport.PicoMotorController8742, [], [], sep="\r\n" + ) as inst: + inst.multiple_controllers = True + axis = inst.axis[ax] + assert isinstance(axis, ik.newport.PicoMotorController8742.Axis) + assert axis._parent == inst + assert axis._idx == ax % 4 + 1 + assert axis._address == f"{ax // 4 + 1}>" + + +@given(ax=st.integers(min_value=124)) +def test_axis_return_multi_index_error(ax): + """Raise IndexError if axis out of bounds and in multi controller mode.""" + with expected_protocol( + ik.newport.PicoMotorController8742, [], [], sep="\r\n" + ) as inst: + inst.multiple_controllers = True + with pytest.raises(IndexError): + _ = inst.axis[ax] + + +@given(ax=st.integers(min_value=0, max_value=31 * 4 - 1)) +def test_axis_sendcmd_multi(ax): + """Send correct command in multiple axis mode.""" + address = ax // 4 + 1 + axis = ax % 4 + 1 + with expected_protocol( + ik.newport.PicoMotorController8742, [f"{address}>{axis}CMD"], [], sep="\r\n" + ) as inst: + inst.multiple_controllers = True + axis = inst.axis[ax] + axis.sendcmd("CMD") + + +@given(ax=st.integers(min_value=0, max_value=31 * 4 - 1)) +def test_axis_query_multi(ax): + """Query command in multiple axis mode and strip address routing.""" + address = ax // 4 + 1 + axis = ax % 4 + 1 + answer_expected = f"{axis}ANSWER" + with expected_protocol( + ik.newport.PicoMotorController8742, + [f"{address}>{axis}CMD"], + [f"{address}>{answer_expected}"], + sep="\r\n", + ) as inst: + inst.multiple_controllers = True + axis = inst.axis[ax] + assert axis.query("CMD") == answer_expected + + +def test_axis_query_multi_io_error(): + """Raise IOError if query response from wrong controller.""" + with expected_protocol( + ik.newport.PicoMotorController8742, [f"1>1CMD"], [f"4>1ANSWER"], sep="\r\n" + ) as inst: + inst.multiple_controllers = True + axis = inst.axis[0] + with pytest.raises(IOError): + axis.query("CMD") diff --git a/instruments/tests/test_newport/test_newportesp301.py b/instruments/tests/test_newport/test_newportesp301.py index 3d0499eb2..ebaec93a9 100644 --- a/instruments/tests/test_newport/test_newportesp301.py +++ b/instruments/tests/test_newport/test_newportesp301.py @@ -1,31 +1,2006 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Unit tests for the Newport ESP 301 axis controller """ # IMPORTS ##################################################################### -from __future__ import absolute_import +import time + +from hypothesis import given, strategies as st +import pytest import instruments as ik +from instruments.units import ureg as u from instruments.tests import expected_protocol # TESTS ####################################################################### -def test_axis_returns_axis_class(): +# pylint: disable=protected-access,too-many-lines + + +# INSTRUMENT # + + +def test_init(): + """Initialize a Newport ESP301 instrument.""" + with expected_protocol(ik.newport.NewportESP301, [], [], sep="\r") as inst: + assert inst._execute_immediately + assert inst._command_list == [] + assert inst._bulk_query_resp == "" + assert inst.terminator == "\r" + + +@given(ax=st.integers(min_value=0, max_value=99)) +def test_axis_returns_axis_class(ax): + """Return axis class with given axis number.""" + with expected_protocol( + ik.newport.NewportESP301, + [f"{ax+1}SN?", "TB?"], # error check query + ["1", "0,0,0"], + sep="\r", + ) as inst: + axis = inst.axis[ax] + assert isinstance(axis, ik.newport.NewportESP301.Axis) + + +def test_newport_cmd(mocker): + """Send a low level command to some randomly chosen target. + + Execute command immediately (default), but no error check. + """ + target = "TARG" + cmd = "COMMAND" + params = (1, 2, 3) + # stitch together raw command to send + raw_cmd = f"{target}{cmd}{','.join(map(str, params))}" + with expected_protocol(ik.newport.NewportESP301, [raw_cmd], [], sep="\r") as inst: + execute_spy = mocker.spy(inst, "_execute_cmd") + resp = inst._newport_cmd(cmd, params=params, target=target, errcheck=False) + assert resp is None + execute_spy.assert_called_with(raw_cmd, False) + + +def test_newport_cmd_add_to_list(): + """Send a low level command to some randomly chosen target. + + Do not execute, just add command to list. + """ + target = "TARG" + cmd = "COMMAND" + params = (1, 2, 3) + # stitch together raw command to send + raw_cmd = f"{target}{cmd}{','.join(map(str, params))}" + with expected_protocol(ik.newport.NewportESP301, [], [], sep="\r") as inst: + inst._execute_immediately = False + resp = inst._newport_cmd(cmd, params=params, target=target) + assert resp is None + assert inst._command_list == [raw_cmd] + + +def test_newport_cmd_with_axis(): + """Send a low level command for a given axis.""" + ax = 42 + cmd = "COMMAND" + params = (1, 2, 3) + # stitch together raw command to send + raw_cmd = f"{ax+1}{cmd}{','.join(map(str, params))}" + + with expected_protocol( + ik.newport.NewportESP301, + [f"{ax+1}SN?", "TB?", raw_cmd], # error check query + ["1", "0,0,0"], + sep="\r", + ) as inst: + axis = inst.axis[ax] + resp = inst._newport_cmd(cmd, params=params, target=axis, errcheck=False) + assert resp is None + + +def test_execute_cmd_query(): + """Execute a query.""" + query = "QUERY?" + response = "RESPONSE" + + with expected_protocol( + ik.newport.NewportESP301, + [query, "TB?"], + [response, "0,0,0"], # no error + sep="\r", + ) as inst: + assert inst._execute_cmd(query) == response + + +def test_execute_cmd_query_error(): + """Raise an error while sending a command to the instrument. + + Only check for the context of the specific error message, since + timestamp is not frozen. + """ + cmd = "COMMAND" + with expected_protocol( + ik.newport.NewportESP301, [cmd, "TB?"], ["13,0,0"], sep="\r" # no error + ) as inst: + with pytest.raises(ik.newport.errors.NewportError) as err_info: + inst._execute_cmd(cmd) + err_msg = err_info.value.args[0] + assert "GROUP NUMBER MISSING" in err_msg + + +def test_home(mocker): + """Search for home. + + Mock `_newport_cmd`, this routine is already tested. Just assert + that it is called with correct arguments. + """ + axis = "ax" + params = 1, 2, 3 + errcheck = False + with expected_protocol(ik.newport.NewportESP301, [], [], sep="\r") as inst: + mock_cmd = mocker.patch.object(inst, "_newport_cmd") + inst._home(axis, params, errcheck) + mock_cmd.assert_called_with( + "OR", target=axis, params=[params], errcheck=errcheck + ) + + +@pytest.mark.parametrize("search_mode", ik.newport.NewportESP301.HomeSearchMode) +def test_search_for_home(mocker, search_mode): + """Search for home with specific method. + + Mock `_home` routine (already tested) and just assert that called + with the correct arguments. + """ + axis = 3 + errcheck = True + with expected_protocol(ik.newport.NewportESP301, [], [], sep="\r") as inst: + mock_cmd = mocker.patch.object(inst, "_home") + inst.search_for_home(axis, search_mode, errcheck) + mock_cmd.assert_called_with( + axis=axis, search_mode=search_mode, errcheck=errcheck + ) + + +def test_reset(mocker): + """Reset the device. + + Mock `_newport_cmd`, this routine is already tested. Just assert + that it is called with correct arguments. + """ + with expected_protocol(ik.newport.NewportESP301, [], [], sep="\r") as inst: + mock_cmd = mocker.patch.object(inst, "_newport_cmd") + inst.reset() + mock_cmd.assert_called_with("RS", errcheck=False) + + +@given(prg_id=st.integers(min_value=1, max_value=100)) +def test_define_program(mocker, prg_id): + """Define an empty program. + + Mock out the `_newport_cmd` routine. Already tested and not + required. + """ + with expected_protocol(ik.newport.NewportESP301, [], [], sep="\r") as inst: + mock_cmd = mocker.patch.object(inst, "_newport_cmd") + with inst.define_program(prg_id): + pass + calls = ( + mocker.call("XX", target=prg_id), + mocker.call("EP", target=prg_id), + mocker.call("QP"), + ) + mock_cmd.has_calls(calls) + + +@given(prg_id=st.integers().filter(lambda x: x < 1 or x > 100)) +def test_define_program_value_error(prg_id): + """Raise ValueError when defining program with invalid ID.""" + with expected_protocol(ik.newport.NewportESP301, [], [], sep="\r") as inst: + with pytest.raises(ValueError) as err_info: + with inst.define_program(prg_id): + pass + err_msg = err_info.value.args[0] + assert ( + err_msg == "Invalid program ID. Must be an integer from 1 to " + "100 (inclusive)." + ) + + +@pytest.mark.parametrize("errcheck", (True, False)) +def test_execute_bulk_command(mocker, errcheck): + """Execute bulk commands. + + Mock out the `_execute_cmd` call and simply assert that calls are + in correct order. + + We will just do three move commands, one with steps of 1, 10, and + 11. + """ + ax = 0 + move_commands_sent = "1PA1.0 ; 1PA10.0 ; ; 1PA11.0 ; " + resp = "Response" with expected_protocol( ik.newport.NewportESP301, [ - "1SN?", - "TB?" # error check query - ], - [ - "1", - "0,0,0" + f"{ax+1}SN?", + "TB?", # error check query ], - sep="\r" + ["1", "0,0,0"], + sep="\r", + ) as inst: + axis = inst.axis[ax] + mock_exec = mocker.patch.object(inst, "_execute_cmd", return_value=resp) + with inst.execute_bulk_command(errcheck=errcheck): + assert not inst._execute_immediately + # some move commands + axis.move(1.0) + axis.move(10.0) + axis.move(11.0) + mock_exec.assert_called_with(move_commands_sent, errcheck) + assert inst._bulk_query_resp == resp + assert inst._command_list == [] + assert inst._execute_immediately + + +@given(prg_id=st.integers(min_value=1, max_value=100)) +def test_run_program(mocker, prg_id): + """Run a program. + + Mock out the `_newport_cmd` routine. Already tested and not + required. + """ + with expected_protocol(ik.newport.NewportESP301, [], [], sep="\r") as inst: + mock_cmd = mocker.patch.object(inst, "_newport_cmd") + inst.run_program(prg_id) + mock_cmd.assert_called_with("EX", target=prg_id) + + +@given(prg_id=st.integers().filter(lambda x: x < 1 or x > 100)) +def test_run_program_value_error(prg_id): + """Raise ValueError when defining program with invalid ID.""" + with expected_protocol(ik.newport.NewportESP301, [], [], sep="\r") as inst: + with pytest.raises(ValueError) as err_info: + inst.run_program(prg_id) + err_msg = err_info.value.args[0] + assert ( + err_msg == "Invalid program ID. Must be an integer from 1 to " + "100 (inclusive)." + ) + + +# AXIS # + + +# commands to send, return when initializing axis zero +ax_init = "1SN?\rTB?", "1\r0,0,0" + + +def test_axis_init(): + """Initialize a new axis.""" + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + assert axis._controller == inst + assert axis._axis_id == 1 + assert axis._units == u.Quantity(1, u.count) + + +def test_axis_init_type_error(): + """Raise TypeError when axis initialized from wrong parent.""" + with pytest.raises(TypeError) as err_info: + _ = ik.newport.newportesp301.NewportESP301.Axis(42, 0) + err_msg = err_info.value.args[0] + assert ( + err_msg == "Axis must be controlled by a Newport ESP-301 motor " "controller." + ) + + +def test_axis_units_of(mocker): + """Context manager with reset of units after usage. + + Mock out the getting and setting the units. These two routines are + tested separately, thus only assert that the correct calls are + issued. + """ + get_unit = ik.newport.newportesp301.NewportESP301.Units.millimeter + set_unit = ik.newport.newportesp301.NewportESP301.Units.inches + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_get = mocker.patch.object(axis, "_get_units", return_value=get_unit) + mock_set = mocker.patch.object(axis, "_set_units", return_value=None) + with axis._units_of(set_unit): + mock_get.assert_called() + mock_set.assert_called_with(set_unit) + mock_set.assert_called_with(get_unit) + + +def test_axis_get_units(mocker): + """Get units from the axis. + + Mock out the command sending and receiving. + """ + resp = "2" + unit = ik.newport.newportesp301.NewportESP301.Units(int(resp)) + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=resp) + assert unit == axis._get_units() + mock_cmd.assert_called_with("SN?", target=1) + + +def test_axis_set_units(mocker): + """Set units for a given axis. + + Mock out the actual command sending for simplicity, but assert it + has been called. + """ + unit = ik.newport.newportesp301.NewportESP301.Units.radian # just pick one + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=None) + assert axis._set_units(unit) is None + mock_cmd.assert_called_with("SN", target=1, params=[int(unit)]) + + +def test_axis_id(): + """Get axis ID.""" + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + assert axis.axis_id == 1 + + +@pytest.mark.parametrize("resp", ("0", "1")) +def test_axis_is_motion_done(mocker, resp): + """Get if motion is done. + + Mock out the command sending, as above. + """ + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=resp) + assert axis.is_motion_done is bool(int(resp)) + mock_cmd.assert_called_with("MD?", target=1) + + +def test_axis_acceleration(mocker): + """Set / get axis acceleration. + + Mock out `_newport_cmd` since tested elsewhere. + """ + value = 42 + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=value) + axis.acceleration = value + mock_cmd.assert_called_with("AC", target=1, params=[float(value)]) + assert axis.acceleration == u.Quantity(value, axis._units / u.s ** 2) + mock_cmd.assert_called_with("AC?", target=1) + + +def test_axis_acceleration_none(): + """Set axis acceleration with `None` does nothing.""" + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + axis.acceleration = None + + +def test_axis_deceleration(mocker): + """Set / get axis deceleration. + + Mock out `_newport_cmd` since tested elsewhere. + """ + value = 42 + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=value) + axis.deceleration = value + mock_cmd.assert_called_with("AG", target=1, params=[float(value)]) + assert axis.deceleration == u.Quantity(value, axis._units / u.s ** 2) + mock_cmd.assert_called_with("AG?", target=1) + + +def test_axis_deceleration_none(): + """Set axis deceleration with `None` does nothing.""" + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + axis.deceleration = None + + +def test_axis_estop_deceleration(mocker): + """Set / get axis estop deceleration. + + Mock out `_newport_cmd` since tested elsewhere. + """ + value = 42 + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=value) + axis.estop_deceleration = value + mock_cmd.assert_called_with("AE", target=1, params=[float(value)]) + assert axis.estop_deceleration == u.Quantity(value, axis._units / u.s ** 2) + mock_cmd.assert_called_with("AE?", target=1) + + +def test_axis_jerk(mocker): + """Set / get axis jerk rate. + + Mock out `_newport_cmd` since tested elsewhere. + """ + value = 42 + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=value) + axis.jerk = value + mock_cmd.assert_called_with("JK", target=1, params=[float(value)]) + assert axis.jerk == u.Quantity(value, axis._units / u.s ** 3) + mock_cmd.assert_called_with("JK?", target=1) + + +def test_axis_velocity(mocker): + """Set / get axis velocity. + + Mock out `_newport_cmd` since tested elsewhere. + """ + value = 42 + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=value) + axis.velocity = value + mock_cmd.assert_called_with("VA", target=1, params=[float(value)]) + assert axis.velocity == u.Quantity(value, axis._units / u.s) + mock_cmd.assert_called_with("VA?", target=1) + + +def test_axis_max_velocity(mocker): + """Set / get axis maximum velocity. + + Mock out `_newport_cmd` since tested elsewhere. + """ + value = 42 + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=value) + axis.max_velocity = value + mock_cmd.assert_called_with("VU", target=1, params=[float(value)]) + assert axis.max_velocity == u.Quantity(value, axis._units / u.s) + mock_cmd.assert_called_with("VU?", target=1) + + +def test_axis_max_velocity_none(): + """Set axis maximum velocity with `None` does nothing.""" + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + axis.max_velocity = None + + +def test_axis_max_base_velocity(mocker): + """Set / get axis maximum base velocity. + + Mock out `_newport_cmd` since tested elsewhere. + """ + value = 42 + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=value) + axis.max_base_velocity = value + mock_cmd.assert_called_with("VB", target=1, params=[float(value)]) + assert axis.max_base_velocity == u.Quantity(value, axis._units / u.s) + mock_cmd.assert_called_with("VB?", target=1) + + +def test_axis_max_base_velocity_none(): + """Set axis maximum base velocity with `None` does nothing.""" + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + axis.max_base_velocity = None + + +def test_axis_jog_high_velocity(mocker): + """Set / get axis jog high velocity. + + Mock out `_newport_cmd` since tested elsewhere. + """ + value = 42 + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=value) + axis.jog_high_velocity = value + mock_cmd.assert_called_with("JH", target=1, params=[float(value)]) + assert axis.jog_high_velocity == u.Quantity(value, axis._units / u.s) + mock_cmd.assert_called_with("JH?", target=1) + + +def test_axis_jog_high_velocity_none(): + """Set axis jog high velocity with `None` does nothing.""" + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + axis.jog_high_velocity = None + + +def test_axis_jog_low_velocity(mocker): + """Set / get axis jog low velocity. + + Mock out `_newport_cmd` since tested elsewhere. + """ + value = 42 + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=value) + axis.jog_low_velocity = value + mock_cmd.assert_called_with("JW", target=1, params=[float(value)]) + assert axis.jog_low_velocity == u.Quantity(value, axis._units / u.s) + mock_cmd.assert_called_with("JW?", target=1) + + +def test_axis_jog_low_velocity_none(): + """Set axis jog low velocity with `None` does nothing.""" + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + axis.jog_low_velocity = None + + +def test_axis_homing_velocity(mocker): + """Set / get axis homing velocity. + + Mock out `_newport_cmd` since tested elsewhere. + """ + value = 42 + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=value) + axis.homing_velocity = value + mock_cmd.assert_called_with("OH", target=1, params=[float(value)]) + assert axis.homing_velocity == u.Quantity(value, axis._units / u.s) + mock_cmd.assert_called_with("OH?", target=1) + + +def test_axis_homing_velocity_none(): + """Set axis homing velocity with `None` does nothing.""" + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + axis.homing_velocity = None + + +def test_axis_max_acceleration(mocker): + """Set / get axis maximum acceleration. + + Mock out `_newport_cmd` since tested elsewhere. + """ + value = 42 + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=value) + axis.max_acceleration = value + mock_cmd.assert_called_with("AU", target=1, params=[float(value)]) + assert axis.max_acceleration == u.Quantity(value, axis._units / u.s ** 2) + mock_cmd.assert_called_with("AU?", target=1) + + +def test_axis_max_acceleration_none(): + """Set axis maximum acceleration with `None` does nothing.""" + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + axis.max_acceleration = None + + +def test_axis_max_deceleration(mocker): + """Set / get axis maximum deceleration. + + Mock out `_newport_cmd` since tested elsewhere. + """ + value = 42 + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=value) + axis.max_deceleration = value + mock_cmd.assert_called_with("AU", target=1, params=[float(value)]) + assert axis.max_deceleration == u.Quantity(value, axis._units / u.s ** 2) + mock_cmd.assert_called_with("AU?", target=1) + + +def test_axis_position(mocker): + """Get axis position. + + Mock out `_newport_cmd` since tested elsewhere. + """ + retval = "42" + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=retval) + assert axis.position == u.Quantity(float(retval), axis._units) + mock_cmd.assert_called_with("TP?", target=1) + + +def test_axis_desired_position(mocker): + """Get axis desired position. + + Mock out `_newport_cmd` since tested elsewhere. + """ + retval = "42" + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=retval) + assert axis.desired_position == u.Quantity(float(retval), axis._units) + mock_cmd.assert_called_with("DP?", target=1) + + +def test_axis_desired_velocity(mocker): + """Get axis desired velocity. + + Mock out `_newport_cmd` since tested elsewhere. + """ + retval = "42" + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=retval) + assert axis.desired_velocity == u.Quantity(float(retval), axis._units / u.s) + mock_cmd.assert_called_with("DV?", target=1) + + +def test_axis_home(mocker): + """Set / get axis home position. + + Mock out `_newport_cmd` since tested elsewhere. + """ + value = 42 + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=value) + axis.home = value + mock_cmd.assert_called_with("DH", target=1, params=[float(value)]) + assert axis.home == u.Quantity(value, axis._units) + mock_cmd.assert_called_with("DH?", target=1) + + +def test_axis_home_none(): + """Set axis home with `None` does nothing.""" + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + axis.home = None + + +def test_axis_units(mocker): + """Get / set units. + + Mock out `_newport_cmd` since tested elsewhere. Returns u.counts + """ + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value="0") + assert axis.units == u.counts + mock_cmd.reset_mock() + # set units with None + axis.units = None + mock_cmd.assert_not_called() + # set units with um as number (num 3) + axis.units = 3 + assert axis._units == u.um + mock_cmd.assert_called_with("SN", target=1, params=[3]) + # set units with millimeters as quantity (num 2) + axis.units = u.mm + assert axis._units == u.mm + mock_cmd.assert_called_with("SN", target=1, params=[2]) + + +def test_axis_encoder_resolution(mocker): + """Set / get axis encoder resolution. + + Mock out `_newport_cmd` since tested elsewhere. + """ + value = 42 + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=value) + axis.encoder_resolution = value + mock_cmd.assert_called_with("SU", target=1, params=[float(value)]) + assert axis.encoder_resolution == u.Quantity(value, axis._units) + mock_cmd.assert_called_with("SU?", target=1) + + +def test_axis_encoder_resolution_none(): + """Set axis encoder resolution with `None` does nothing.""" + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + axis.encoder_resolution = None + + +def test_axis_full_step_resolution(mocker): + """Set / get axis full step resolution. + + Mock out `_newport_cmd` since tested elsewhere. + """ + value = 42 + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=value) + axis.full_step_resolution = value + mock_cmd.assert_called_with("FR", target=1, params=[float(value)]) + assert axis.full_step_resolution == u.Quantity(value, axis._units) + mock_cmd.assert_called_with("FR?", target=1) + + +def test_axis_full_step_resolution_none(): + """Set axis full step resolution with `None` does nothing.""" + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + axis.full_step_resolution = None + + +def test_axis_left_limit(mocker): + """Set / get axis left limit. + + Mock out `_newport_cmd` since tested elsewhere. + """ + value = 42 + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=value) + axis.left_limit = value + mock_cmd.assert_called_with("SL", target=1, params=[float(value)]) + assert axis.left_limit == u.Quantity(value, axis._units) + mock_cmd.assert_called_with("SL?", target=1) + + +def test_axis_right_limit(mocker): + """Set / get axis right limit. + + Mock out `_newport_cmd` since tested elsewhere. + """ + value = 42 + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=value) + axis.right_limit = value + mock_cmd.assert_called_with("SR", target=1, params=[float(value)]) + assert axis.right_limit == u.Quantity(value, axis._units) + mock_cmd.assert_called_with("SR?", target=1) + + +def test_axis_error_threshold(mocker): + """Set / get axis error threshold. + + Mock out `_newport_cmd` since tested elsewhere. + """ + value = 42 + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=value) + axis.error_threshold = value + mock_cmd.assert_called_with("FE", target=1, params=[float(value)]) + assert axis.error_threshold == u.Quantity(value, axis._units) + mock_cmd.assert_called_with("FE?", target=1) + + +def test_axis_error_threshold_none(): + """Set axis error threshold with `None` does nothing.""" + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + axis.error_threshold = None + + +def test_axis_current(mocker): + """Set / get axis current. + + Mock out `_newport_cmd` since tested elsewhere. + """ + value = 42 + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=value) + axis.current = value + mock_cmd.assert_called_with("QI", target=1, params=[float(value)]) + assert axis.current == u.Quantity(value, u.A) + mock_cmd.assert_called_with("QI?", target=1) + + +def test_axis_current_none(): + """Set axis current with `None` does nothing.""" + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + axis.current = None + + +def test_axis_voltage(mocker): + """Set / get axis voltage. + + Mock out `_newport_cmd` since tested elsewhere. + """ + value = 42 + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=value) + axis.voltage = value + mock_cmd.assert_called_with("QV", target=1, params=[float(value)]) + assert axis.voltage == u.Quantity(value, u.V) + mock_cmd.assert_called_with("QV?", target=1) + + +def test_axis_voltage_none(): + """Set axis voltage with `None` does nothing.""" + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + axis.voltage = None + + +def test_axis_motor_type(mocker): + """Set / get axis motor type. + + Mock out `_newport_cmd` since tested elsewhere. + """ + value = 1 # DC Servo + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=value) + axis.motor_type = value + mock_cmd.assert_called_with("QM", target=1, params=[float(value)]) + assert axis.motor_type == value + mock_cmd.assert_called_with("QM?", target=1) + + +def test_axis_motor_type_none(): + """Set axis motor type with `None` does nothing.""" + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + axis.motor_type = None + + +def test_axis_feedback_configuration(mocker): + """Set / get axis feedback configuration. + + Mock out `_newport_cmd` since tested elsewhere. + """ + value_ret = "A13\r\n" # 2 additional characters that will be cancelled + value = int(value_ret[:-2], 16) + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=value_ret) + axis.feedback_configuration = value + mock_cmd.assert_called_with("ZB", target=1, params=[float(value)]) + assert axis.feedback_configuration == value + mock_cmd.assert_called_with("ZB?", target=1) + + +def test_axis_feedback_configuration_none(): + """Set axis feedback configuration with `None` does nothing.""" + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + axis.feedback_configuration = None + + +def test_axis_position_display_resolution(mocker): + """Set / get axis position display resolution. + + Mock out `_newport_cmd` since tested elsewhere. + """ + value = 42 + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=value) + axis.position_display_resolution = value + mock_cmd.assert_called_with("FP", target=1, params=[float(value)]) + assert axis.position_display_resolution == value + mock_cmd.assert_called_with("FP?", target=1) + + +def test_axis_position_display_resolution_none(): + """Set axis position display resolution with `None` does nothing.""" + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + axis.position_display_resolution = None + + +def test_axis_trajectory(mocker): + """Set / get axis trajectory. + + Mock out `_newport_cmd` since tested elsewhere. + """ + value = 42 + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=value) + axis.trajectory = value + mock_cmd.assert_called_with("TJ", target=1, params=[float(value)]) + assert axis.trajectory == value + mock_cmd.assert_called_with("TJ?", target=1) + + +def test_axis_trajectory_none(): + """Set axis trajectory with `None` does nothing.""" + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + axis.trajectory = None + + +def test_axis_microstep_factor(mocker): + """Set / get axis microstep factor. + + Mock out `_newport_cmd` since tested elsewhere. + """ + value = 42 + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=value) + axis.microstep_factor = value + mock_cmd.assert_called_with("QS", target=1, params=[float(value)]) + assert axis.microstep_factor == value + mock_cmd.assert_called_with("QS?", target=1) + + +def test_axis_microstep_factor_none(): + """Set axis microstep factor with `None` does nothing.""" + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + axis.microstep_factor = None + + +@given(fct=st.integers().filter(lambda x: x < 1 or x > 250)) +def test_axis_microstep_factor_out_of_range(fct): + """Raise ValueError when microstep factor is out of range.""" + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + with pytest.raises(ValueError) as err_info: + axis.microstep_factor = fct + err_msg = err_info.value.args[0] + assert err_msg == "Microstep factor must be between 1 and 250" + + +def test_axis_hardware_limit_configuration(mocker): + """Set / get axis hardware limit configuration. + + Mock out `_newport_cmd` since tested elsewhere. + """ + value_ret = "42\r\n" # add two characters to delete later + value = int(value_ret[:-2]) + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=value_ret) + axis.hardware_limit_configuration = value + mock_cmd.assert_called_with("ZH", target=1, params=[float(value)]) + assert axis.hardware_limit_configuration == value + mock_cmd.assert_called_with("ZH?", target=1) + + +def test_axis_hardware_limit_configuration_none(): + """Set axis hardware limit configuration with `None` does nothing.""" + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + axis.hardware_limit_configuration = None + + +def test_axis_acceleration_feed_forward(mocker): + """Set / get axis acceleration feed forward. + + Mock out `_newport_cmd` since tested elsewhere. + """ + value = 42 + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=value) + axis.acceleration_feed_forward = value + mock_cmd.assert_called_with("AF", target=1, params=[float(value)]) + assert axis.acceleration_feed_forward == value + mock_cmd.assert_called_with("AF?", target=1) + + +def test_axis_acceleration_feed_forward_none(): + """Set axis acceleration feed forward with `None` does nothing.""" + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + axis.acceleration_feed_forward = None + + +def test_axis_proportional_gain(mocker): + """Set / get axis proportional gain. + + Mock out `_newport_cmd` since tested elsewhere. + """ + value_ret = "42\r" + value = float(value_ret[:-1]) + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=value_ret) + axis.proportional_gain = value + mock_cmd.assert_called_with("KP", target=1, params=[float(value)]) + assert axis.proportional_gain == float(value) + mock_cmd.assert_called_with("KP?", target=1) + + +def test_axis_proportional_gain_none(): + """Set axis proportional gain with `None` does nothing.""" + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + axis.proportional_gain = None + + +def test_axis_derivative_gain(mocker): + """Set / get axis derivative gain. + + Mock out `_newport_cmd` since tested elsewhere. + """ + value_ret = "42" + value = float(value_ret) + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=value_ret) + axis.derivative_gain = value + mock_cmd.assert_called_with("KD", target=1, params=[float(value)]) + assert axis.derivative_gain == float(value) + mock_cmd.assert_called_with("KD?", target=1) + + +def test_axis_derivative_gain_none(): + """Set axis derivative gain with `None` does nothing.""" + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + axis.derivative_gain = None + + +def test_axis_integral_gain(mocker): + """Set / get axis integral gain. + + Mock out `_newport_cmd` since tested elsewhere. + """ + value_ret = "42" + value = float(value_ret) + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=value_ret) + axis.integral_gain = value + mock_cmd.assert_called_with("KI", target=1, params=[float(value)]) + assert axis.integral_gain == float(value) + mock_cmd.assert_called_with("KI?", target=1) + + +def test_axis_integral_gain_none(): + """Set axis integral gain with `None` does nothing.""" + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + axis.integral_gain = None + + +def test_axis_integral_saturation_gain(mocker): + """Set / get axis integral saturation gain. + + Mock out `_newport_cmd` since tested elsewhere. + """ + value_ret = "42" + value = float(value_ret) + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=value_ret) + axis.integral_saturation_gain = value + mock_cmd.assert_called_with("KS", target=1, params=[float(value)]) + assert axis.integral_saturation_gain == float(value) + mock_cmd.assert_called_with("KS?", target=1) + + +def test_axis_integral_saturation_gain_none(): + """Set axis integral saturation gain with `None` does nothing.""" + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + axis.integral_saturation_gain = None + + +def test_axis_encoder_position(mocker): + """Get encoder position. + + Mock out the getting and setting the units. These two routines are + tested separately, thus only assert that the correct calls are + issued. + Also mock out `_newport_cmd`. + """ + value = 42 + get_unit = ik.newport.newportesp301.NewportESP301.Units.millimeter + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_get = mocker.patch.object(axis, "_get_units", return_value=get_unit) + mock_set = mocker.patch.object(axis, "_set_units", return_value=None) + mock_cmd = mocker.patch.object(axis, "_newport_cmd", return_value=value) + assert axis.encoder_position == u.Quantity(value, u.count) + mock_get.assert_called() + mock_set.assert_called_with(get_unit) + mock_cmd.assert_called_with("TP?", target=1) + + +# AXIS METHODS # + + +@pytest.mark.parametrize("mode", ik.newport.newportesp301.NewportESP301.HomeSearchMode) +def test_axis_search_for_home(mocker, mode): + """Search for home. + + Mock out `search_for_home` of controller since already tested. + """ + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_search = mocker.patch.object(axis._controller, "search_for_home") + axis.search_for_home(search_mode=mode.value) + mock_search.assert_called_with(axis=1, search_mode=mode.value) + + +def test_axis_search_for_home_default(mocker): + """Search for home without a specified search mode. + + Mock out `search_for_home` of controller since already tested. + """ + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_search = mocker.patch.object(axis._controller, "search_for_home") + axis.search_for_home() + + default_mode = axis._controller.HomeSearchMode.zero_position_count.value + mock_search.assert_called_with(axis=1, search_mode=default_mode) + + +def test_axis_move_absolute(mocker): + """Make an absolute move (default) on the axis. + + No wait, no block. + Mock out `_newport_cmd` since tested elsewhere. + """ + position = 42 + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd") + axis.move(position) + mock_cmd.assert_called_with("PA", params=[position], target=1) + + +def test_axis_move_relative_wait(mocker): + """Make an relative move on the axis and wait. + + Do a wait but no block. + Mock out `_newport_cmd` since tested elsewhere. + """ + position = 42 + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd") + axis.move(position, absolute=False, wait=True) + calls = ( + mocker.call("PR", params=[position], target=1), + mocker.call("WP", target=1, params=[float(position)]), + ) + mock_cmd.assert_has_calls(calls) + + +def test_axis_move_relative_wait_block(mocker): + """Make an relative move on the axis and wait. + + Do a wait and lock, go once into while loop. + Mock out `_newport_cmd`, `time.sleep`, and `is_motion_done` since + tested elsewhere. + """ + position = 42 + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd") + mock_cmd.side_effect = [None, None, False, True] + axis.move(position, absolute=False, wait=True, block=True) + calls = ( + mocker.call("PR", params=[position], target=1), + mocker.call("WP", target=1, params=[float(position)]), + mocker.call("MD?", target=1), + mocker.call("MD?", target=1), + ) + mock_cmd.assert_has_calls(calls) + + +def test_axis_move_to_hardware_limit(mocker): + """Move to hardware limit. + + Mock out `_newport_cmd` since tested elsewhere. + """ + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd") + axis.move_to_hardware_limit() + mock_cmd.assert_called_with("MT", target=1) + + +def test_axis_move_indefinitely(mocker): + """Move indefinitely + + Mock out `_newport_cmd` since tested elsewhere. + """ + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd") + axis.move_indefinitely() + mock_cmd.assert_called_with("MV", target=1) + + +def test_axis_abort_motion(mocker): + """Abort motion. + + Mock out `_newport_cmd` since tested elsewhere. + """ + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd") + axis.abort_motion() + mock_cmd.assert_called_with("AB", target=1) + + +def test_axis_wait_for_stop(mocker): + """Wait for stop. + + Mock out `_newport_cmd` since tested elsewhere. + """ + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd") + axis.wait_for_stop() + mock_cmd.assert_called_with("WS", target=1) + + +def test_axis_stop_motion(mocker): + """Stop motion. + + Mock out `_newport_cmd` since tested elsewhere. + """ + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd") + axis.stop_motion() + mock_cmd.assert_called_with("ST", target=1) + + +def test_axis_wait_for_position(mocker): + """Wait for position. + + Mock out `_newport_cmd` since tested elsewhere. + """ + value = 42 + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd") + axis.wait_for_position(value) + mock_cmd.assert_called_with("WP", target=1, params=[float(value)]) + + +def test_axis_wait_for_motion_max_wait_zero(mocker): + """Wait for motion to finish. + + Motion is not stopped (mock that part) but maximum wait time is + zero. + """ + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mocker.patch.object(axis, "_newport_cmd", return_value="0") + + with pytest.raises(IOError) as err_info: + axis.wait_for_motion(max_wait=0.0) + err_msg = err_info.value.args[0] + assert err_msg == "Timed out waiting for motion to finish." + + +def test_axis_wait_for_motion_max_wait_some_time(mocker): + """Wait for motion to finish. + + Motion is stopped after several queries that first return `False`. + Mocking `time.time`, `time.sleep`, and `_newport_cmd`. Using + generators to create the appropriate times.. + """ + interval = 42.0 + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + # patch time and sleep + mock_time = mocker.patch.object(time, "time", return_value=None) + mock_time.side_effect = [0.0, 0.0, 0.1] + mock_sleep = mocker.patch.object(time, "sleep", return_value=None) + # get axis + axis = inst.axis[0] + # patch status + mock_status = mocker.patch.object(axis, "_newport_cmd", return_value=None) + mock_status.side_effect = ["0", "0", "1"] + assert axis.wait_for_motion(poll_interval=interval) is None + # make sure the routine has called sleep + mock_sleep.assert_called_with(interval) + + +def test_axis_enable(mocker): + """Enable axis. + + Mock out `_newport_cmd` since tested elsewhere. + """ + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd") + axis.enable() + mock_cmd.assert_called_with("MO", target=1) + + +def test_axis_disable(mocker): + """Disable axis. + + Mock out `_newport_cmd` since tested elsewhere. + """ + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd") + axis.disable() + mock_cmd.assert_called_with("MF", target=1) + + +def test_axis_setup_axis(mocker): + """Set up non-newport motor. + + Mock out `_newport_cmd` since tested elsewhere. + """ + motor_type = 2 # stepper motor + current = 1 + voltage = 2 + units = ik.newport.newportesp301.NewportESP301.Units.radian + encoder_resolution = 3.0 + max_velocity = 4 + max_base_velocity = 5 + homing_velocity = 6 + jog_high_velocity = 7 + jog_low_velocity = 8 + max_acceleration = 9 + acceleration = 10 + velocity = 11 + deceleration = 12 + estop_deceleration = 13 + jerk = 14 + error_threshold = 15 + proportional_gain = 16 + derivative_gain = 17 + integral_gain = 18 + integral_saturation_gain = 19 + trajectory = 20 + position_display_resolution = 21 + feedback_configuration = 22 + full_step_resolution = 23 + home = 24 + microstep_factor = 25 + acceleration_feed_forward = 26 + hardware_limit_configuration = 27 + + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd") + mocker.patch.object(axis, "read_setup", return_value=True) + ax_setup = axis.setup_axis( + motor_type=motor_type, + current=current, + voltage=voltage, + units=units, + encoder_resolution=encoder_resolution, + max_velocity=max_velocity, + max_base_velocity=max_base_velocity, + homing_velocity=homing_velocity, + jog_high_velocity=jog_high_velocity, + jog_low_velocity=jog_low_velocity, + max_acceleration=max_acceleration, + acceleration=acceleration, + velocity=velocity, + deceleration=deceleration, + estop_deceleration=estop_deceleration, + jerk=jerk, + error_threshold=error_threshold, + proportional_gain=proportional_gain, + derivative_gain=derivative_gain, + integral_gain=integral_gain, + integral_saturation_gain=integral_saturation_gain, + trajectory=trajectory, + position_display_resolution=position_display_resolution, + feedback_configuration=feedback_configuration, + full_step_resolution=full_step_resolution, + home=home, + microstep_factor=microstep_factor, + acceleration_feed_forward=acceleration_feed_forward, + hardware_limit_configuration=hardware_limit_configuration, + ) + assert ax_setup + + # assert mandatory calls in any order + calls_params = ( + mocker.call("QM", target=1, params=[int(motor_type)]), + mocker.call("ZB", target=1, params=[int(feedback_configuration)]), + mocker.call("FR", target=1, params=[float(full_step_resolution)]), + mocker.call("FP", target=1, params=[int(position_display_resolution)]), + mocker.call("QI", target=1, params=[float(current)]), + mocker.call("QV", target=1, params=[float(voltage)]), + mocker.call("SN", target=1, params=[units.value]), + mocker.call("SU", target=1, params=[float(encoder_resolution)]), + mocker.call("AU", target=1, params=[float(max_acceleration)]), + mocker.call("VU", target=1, params=[float(max_velocity)]), + mocker.call("VB", target=1, params=[float(max_base_velocity)]), + mocker.call("OH", target=1, params=[float(homing_velocity)]), + mocker.call("JH", target=1, params=[float(jog_high_velocity)]), + mocker.call("JW", target=1, params=[float(jog_low_velocity)]), + mocker.call("AC", target=1, params=[float(acceleration)]), + mocker.call("VA", target=1, params=[float(velocity)]), + mocker.call("AG", target=1, params=[float(deceleration)]), + mocker.call("AE", target=1, params=[float(estop_deceleration)]), + mocker.call("JK", target=1, params=[float(jerk)]), + mocker.call("FE", target=1, params=[float(error_threshold)]), + mocker.call("KP", target=1, params=[float(proportional_gain)]), + mocker.call("KD", target=1, params=[float(derivative_gain)]), + mocker.call("KI", target=1, params=[float(integral_gain)]), + mocker.call("KS", target=1, params=[float(integral_saturation_gain)]), + mocker.call("DH", target=1, params=[float(home)]), + mocker.call("QS", target=1, params=[float(microstep_factor)]), + mocker.call("AF", target=1, params=[float(acceleration_feed_forward)]), + mocker.call("TJ", target=1, params=[int(trajectory)]), + mocker.call("ZH", target=1, params=[int(hardware_limit_configuration)]), + ) + mock_cmd.assert_has_calls(calls_params, any_order=True) + + # assert final calls - in order + calls_final = ( + mocker.call("UF", target=1), + mocker.call("QD", target=1), + mocker.call("SM"), + ) + mock_cmd.assert_has_calls(calls_final) + mock_cmd.assert_called_with("SM") + + +def test_axis_setup_axis_torque(mocker): + """Set up non-newport motor with torque specifications. + + Mock out `_newport_cmd` since tested elsewhere. + """ + motor_type = 2 # stepper motor + current = 1 + voltage = 2 + units = ik.newport.newportesp301.NewportESP301.Units.radian + encoder_resolution = 3.0 + max_velocity = 4 + max_base_velocity = 5 + homing_velocity = 6 + jog_high_velocity = 7 + jog_low_velocity = 8 + max_acceleration = 9 + acceleration = 10 + velocity = 11 + deceleration = 12 + estop_deceleration = 13 + jerk = 14 + error_threshold = 15 + proportional_gain = 16 + derivative_gain = 17 + integral_gain = 18 + integral_saturation_gain = 19 + trajectory = 20 + position_display_resolution = 21 + feedback_configuration = 22 + full_step_resolution = 23 + home = 24 + microstep_factor = 25 + acceleration_feed_forward = 26 + hardware_limit_configuration = 27 + # special configs + rmt_time = 42 + rmt_perc = 13 + + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd") + mocker.patch.object(axis, "read_setup", return_value=True) + axis.setup_axis( + motor_type=motor_type, + current=current, + voltage=voltage, + units=units, + encoder_resolution=encoder_resolution, + max_velocity=max_velocity, + max_base_velocity=max_base_velocity, + homing_velocity=homing_velocity, + jog_high_velocity=jog_high_velocity, + jog_low_velocity=jog_low_velocity, + max_acceleration=max_acceleration, + acceleration=acceleration, + velocity=velocity, + deceleration=deceleration, + estop_deceleration=estop_deceleration, + jerk=jerk, + error_threshold=error_threshold, + proportional_gain=proportional_gain, + derivative_gain=derivative_gain, + integral_gain=integral_gain, + integral_saturation_gain=integral_saturation_gain, + trajectory=trajectory, + position_display_resolution=position_display_resolution, + feedback_configuration=feedback_configuration, + full_step_resolution=full_step_resolution, + home=home, + microstep_factor=microstep_factor, + acceleration_feed_forward=acceleration_feed_forward, + hardware_limit_configuration=hardware_limit_configuration, + reduce_motor_torque_time=rmt_time, + reduce_motor_torque_percentage=rmt_perc, + ) + # ensure the torque settings are set + call_torque = (mocker.call("QR", target=1, params=[rmt_time, rmt_perc]),) + + mock_cmd.assert_has_calls(call_torque) + + +@given(rmt_time=st.integers().filter(lambda x: x < 0 or x > 60000)) +def test_axis_setup_axis_torque_time_out_of_range(mocker, rmt_time): + """Raise ValueError when time is out of range. + + Mock out `_newport_cmd` since tested elsewhere. + """ + motor_type = 2 # stepper motor + current = 1 + voltage = 2 + units = ik.newport.newportesp301.NewportESP301.Units.radian + encoder_resolution = 3.0 + max_velocity = 4 + max_base_velocity = 5 + homing_velocity = 6 + jog_high_velocity = 7 + jog_low_velocity = 8 + max_acceleration = 9 + acceleration = 10 + velocity = 11 + deceleration = 12 + estop_deceleration = 13 + jerk = 14 + error_threshold = 15 + proportional_gain = 16 + derivative_gain = 17 + integral_gain = 18 + integral_saturation_gain = 19 + trajectory = 20 + position_display_resolution = 21 + feedback_configuration = 22 + full_step_resolution = 23 + home = 24 + microstep_factor = 25 + acceleration_feed_forward = 26 + hardware_limit_configuration = 27 + # special configs + rmt_perc = 13 + + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mocker.patch.object(axis, "_newport_cmd") + mocker.patch.object(axis, "read_setup", return_value=True) + with pytest.raises(ValueError) as err_info: + axis.setup_axis( + motor_type=motor_type, + current=current, + voltage=voltage, + units=units, + encoder_resolution=encoder_resolution, + max_velocity=max_velocity, + max_base_velocity=max_base_velocity, + homing_velocity=homing_velocity, + jog_high_velocity=jog_high_velocity, + jog_low_velocity=jog_low_velocity, + max_acceleration=max_acceleration, + acceleration=acceleration, + velocity=velocity, + deceleration=deceleration, + estop_deceleration=estop_deceleration, + jerk=jerk, + error_threshold=error_threshold, + proportional_gain=proportional_gain, + derivative_gain=derivative_gain, + integral_gain=integral_gain, + integral_saturation_gain=integral_saturation_gain, + trajectory=trajectory, + position_display_resolution=position_display_resolution, + feedback_configuration=feedback_configuration, + full_step_resolution=full_step_resolution, + home=home, + microstep_factor=microstep_factor, + acceleration_feed_forward=acceleration_feed_forward, + hardware_limit_configuration=hardware_limit_configuration, + reduce_motor_torque_time=rmt_time, + reduce_motor_torque_percentage=rmt_perc, + ) + err_msg = err_info.value.args[0] + assert err_msg == "Time must be between 0 and 60000 ms" + + +@given(rmt_perc=st.integers().filter(lambda x: x < 0 or x > 100)) +def test_axis_setup_axis_torque_percentage_out_of_range(mocker, rmt_perc): + """Raise ValueError when time is out of range. + + Mock out `_newport_cmd` since tested elsewhere. + """ + motor_type = 2 # stepper motor + current = 1 + voltage = 2 + units = ik.newport.newportesp301.NewportESP301.Units.radian + encoder_resolution = 3.0 + max_velocity = 4 + max_base_velocity = 5 + homing_velocity = 6 + jog_high_velocity = 7 + jog_low_velocity = 8 + max_acceleration = 9 + acceleration = 10 + velocity = 11 + deceleration = 12 + estop_deceleration = 13 + jerk = 14 + error_threshold = 15 + proportional_gain = 16 + derivative_gain = 17 + integral_gain = 18 + integral_saturation_gain = 19 + trajectory = 20 + position_display_resolution = 21 + feedback_configuration = 22 + full_step_resolution = 23 + home = 24 + microstep_factor = 25 + acceleration_feed_forward = 26 + hardware_limit_configuration = 27 + # special configs + rmt_time = 42 + + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mocker.patch.object(axis, "_newport_cmd") + mocker.patch.object(axis, "read_setup", return_value=True) + with pytest.raises(ValueError) as err_info: + axis.setup_axis( + motor_type=motor_type, + current=current, + voltage=voltage, + units=units, + encoder_resolution=encoder_resolution, + max_velocity=max_velocity, + max_base_velocity=max_base_velocity, + homing_velocity=homing_velocity, + jog_high_velocity=jog_high_velocity, + jog_low_velocity=jog_low_velocity, + max_acceleration=max_acceleration, + acceleration=acceleration, + velocity=velocity, + deceleration=deceleration, + estop_deceleration=estop_deceleration, + jerk=jerk, + error_threshold=error_threshold, + proportional_gain=proportional_gain, + derivative_gain=derivative_gain, + integral_gain=integral_gain, + integral_saturation_gain=integral_saturation_gain, + trajectory=trajectory, + position_display_resolution=position_display_resolution, + feedback_configuration=feedback_configuration, + full_step_resolution=full_step_resolution, + home=home, + microstep_factor=microstep_factor, + acceleration_feed_forward=acceleration_feed_forward, + hardware_limit_configuration=hardware_limit_configuration, + reduce_motor_torque_time=rmt_time, + reduce_motor_torque_percentage=rmt_perc, + ) + err_msg = err_info.value.args[0] + assert err_msg == r"Percentage must be between 0 and 100%" + + +def test_axis_read_setup(mocker): + """Read the axis setup and return it. + + Mock out `_newport_cmd` since tested elsewhere. + """ + config = { + "units": u.mm, + "motor_type": ik.newport.newportesp301.NewportESP301.MotorType.dc_servo, + "feedback_configuration": 1, # last 2 removed at return + "full_step_resolution": u.Quantity(2.0, u.mm), + "position_display_resolution": 3, + "current": u.Quantity(4.0, u.A), + "max_velocity": u.Quantity(5.0, u.mm / u.s), + "encoder_resolution": u.Quantity(6.0, u.mm), + "acceleration": u.Quantity(7.0, u.mm / u.s ** 2), + "deceleration": u.Quantity(8.0, u.mm / u.s ** 2), + "velocity": u.Quantity(9.0, u.mm / u.s), + "max_acceleration": u.Quantity(10.0, u.mm / u.s ** 2.0), + "homing_velocity": u.Quantity(11.0, u.mm / u.s), + "jog_high_velocity": u.Quantity(12.0, u.mm / u.s), + "jog_low_velocity": u.Quantity(13.0, u.mm / u.s), + "estop_deceleration": u.Quantity(14.0, u.mm / u.s ** 2.0), + "jerk": u.Quantity(14.0, u.mm / u.s ** 3.0), + "proportional_gain": 15.0, # last 1 removed at return + "derivative_gain": 16.0, + "integral_gain": 17.0, + "integral_saturation_gain": 18.0, + "home": u.Quantity(19.0, u.mm), + "microstep_factor": 20, + "acceleration_feed_forward": 21.0, + "trajectory": 22, + "hardware_limit_configuration": 23, # last 2 removed + } + + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd") + mock_cmd.side_effect = [ + ik.newport.newportesp301.NewportESP301.Units.millimeter.value, + config["motor_type"].value, + f"{config['feedback_configuration']}**", # 2 extra + config["full_step_resolution"].magnitude, + config["position_display_resolution"], + config["current"].magnitude, + config["max_velocity"].magnitude, + config["encoder_resolution"].magnitude, + config["acceleration"].magnitude, + config["deceleration"].magnitude, + config["velocity"].magnitude, + config["max_acceleration"].magnitude, + config["homing_velocity"].magnitude, + config["jog_high_velocity"].magnitude, + config["jog_low_velocity"].magnitude, + config["estop_deceleration"].magnitude, + config["jerk"].magnitude, + f"{config['proportional_gain']}*", # 1 extra + config["derivative_gain"], + config["integral_gain"], + config["integral_saturation_gain"], + config["home"].magnitude, + config["microstep_factor"], + config["acceleration_feed_forward"], + config["trajectory"], + f"{config['hardware_limit_configuration']}**", + ] + assert axis.read_setup() == config + + +def test_axis_get_status(mocker): + """Get an axis status. + + Mock out `_newport_cmd` since tested elsewhere. + """ + status = { + "units": u.mm, + "position": u.Quantity(1.0, u.mm), + "desired_position": u.Quantity(2.0, u.mm), + "desired_velocity": u.Quantity(3.0, u.mm / u.s), + "is_motion_done": True, + } + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + mock_cmd = mocker.patch.object(axis, "_newport_cmd") + mock_cmd.side_effect = [ + "2", + status["position"].magnitude, + status["desired_position"].magnitude, + status["desired_velocity"].magnitude, + "1", + ] + assert axis.get_status() == status + + +@pytest.mark.parametrize("num", ik.newport.NewportESP301.Axis._unit_dict) +def test_axis_get_pq_unit(num): + """Get units for specified axis.""" + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + assert axis._get_pq_unit(num) == axis._unit_dict[num] + + +@pytest.mark.parametrize("num", ik.newport.NewportESP301.Axis._unit_dict) +def test_axis_get_unit_num(num): + """Get unit number from dictionary. + + Skip number 1, since u.count appears twice in dictionary! + """ + if num == 1: + num = 0 # u.count twice + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + quant = axis._unit_dict[num] + print(quant) + assert axis._get_unit_num(quant) == num + + +def test_axis_get_unit_num_invalid_unit(): + """Raise KeyError if unit not valid.""" + invalid_unit = u.ly + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" + ) as inst: + axis = inst.axis[0] + with pytest.raises(KeyError) as err_info: + axis._get_unit_num(invalid_unit) + err_msg = err_info.value.args[0] + assert err_msg == f"{invalid_unit} is not a valid unit for Newport " f"Axis" + + +def test_axis_newport_cmd(mocker): + """Send command to parent class. + + Mock out parent classes `_newport_cmd` and assert call. + """ + cmd = 123 + some_keyword = "keyword" + with expected_protocol( + ik.newport.NewportESP301, [ax_init[0]], [ax_init[1]], sep="\r" ) as inst: axis = inst.axis[0] - assert isinstance(axis, ik.newport.NewportESP301Axis) is True + mock_cmd = mocker.patch.object(axis._controller, "_newport_cmd") + axis._newport_cmd(cmd, some_keyword=some_keyword) + mock_cmd.assert_called_with(cmd, some_keyword=some_keyword) diff --git a/instruments/tests/test_ondax/test_lm.py b/instruments/tests/test_ondax/test_lm.py index 93c89de29..119e92d66 100644 --- a/instruments/tests/test_ondax/test_lm.py +++ b/instruments/tests/test_ondax/test_lm.py @@ -1,508 +1,216 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Unit tests for the Ondax Laser Module """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from nose.tools import raises - -import quantities +import pytest from instruments import ondax from instruments.tests import expected_protocol +from instruments.units import ureg as u # TESTS ####################################################################### def test_acc_target(): - with expected_protocol( - ondax.LM, - [ - "rstli?" - ], - [ - "100" - ], - sep="\r" - ) as lm: - assert lm.acc.target == 100 * quantities.mA + with expected_protocol(ondax.LM, ["rstli?"], ["100"], sep="\r") as lm: + assert lm.acc.target == 100 * u.mA def test_acc_enable(): - with expected_protocol( - ondax.LM, - [ - "lcen" - ], - [ - "OK" - ], - sep="\r" - ) as lm: + with expected_protocol(ondax.LM, ["lcen"], ["OK"], sep="\r") as lm: lm.acc.enabled = True assert lm.acc.enabled def test_acc_disable(): - with expected_protocol( - ondax.LM, - [ - "lcdis" - ], - [ - "OK" - ], - sep="\r" - ) as lm: + with expected_protocol(ondax.LM, ["lcdis"], ["OK"], sep="\r") as lm: lm.acc.enabled = False assert not lm.acc.enabled -@raises(TypeError) def test_acc_enable_not_boolean(): - with expected_protocol( - ondax.LM, - [], - [], - sep="\r" - ) as lm: - lm.acc.enabled = "foobar" + with pytest.raises(TypeError): + with expected_protocol(ondax.LM, [], [], sep="\r") as lm: + lm.acc.enabled = "foobar" def test_acc_on(): - with expected_protocol( - ondax.LM, - [ - "lcon" - ], - [ - "OK" - ], - sep="\r" - ) as lm: + with expected_protocol(ondax.LM, ["lcon"], ["OK"], sep="\r") as lm: lm.acc.on() def test_acc_off(): - with expected_protocol( - ondax.LM, - [ - "lcoff" - ], - [ - "OK" - ], - sep="\r" - ) as lm: + with expected_protocol(ondax.LM, ["lcoff"], ["OK"], sep="\r") as lm: lm.acc.off() def test_apc_target(): - with expected_protocol( - ondax.LM, - [ - "rslp?" - ], - [ - "100" - ], - sep="\r" - ) as lm: - assert lm.apc.target == 100 * quantities.mW + with expected_protocol(ondax.LM, ["rslp?"], ["100"], sep="\r") as lm: + assert lm.apc.target == 100 * u.mW def test_apc_enable(): - with expected_protocol( - ondax.LM, - [ - "len" - ], - [ - "OK" - ], - sep="\r" - ) as lm: + with expected_protocol(ondax.LM, ["len"], ["OK"], sep="\r") as lm: lm.apc.enabled = True assert lm.apc.enabled def test_apc_disable(): - with expected_protocol( - ondax.LM, - [ - "ldis" - ], - [ - "OK" - ], - sep="\r" - ) as lm: + with expected_protocol(ondax.LM, ["ldis"], ["OK"], sep="\r") as lm: lm.apc.enabled = False assert not lm.apc.enabled -@raises(TypeError) def test_apc_enable_not_boolean(): - with expected_protocol( - ondax.LM, - [], - [], - sep="\r" - ) as lm: - lm.apc.enabled = "foobar" - + with pytest.raises(TypeError): + with expected_protocol(ondax.LM, [], [], sep="\r") as lm: + lm.apc.enabled = "foobar" def test_apc_start(): - with expected_protocol( - ondax.LM, - [ - "sps" - ], - [ - "OK" - ], - sep="\r" - ) as lm: + with expected_protocol(ondax.LM, ["sps"], ["OK"], sep="\r") as lm: lm.apc.start() def test_apc_stop(): - with expected_protocol( - ondax.LM, - [ - "cps" - ], - [ - "OK" - ], - sep="\r" - ) as lm: + with expected_protocol(ondax.LM, ["cps"], ["OK"], sep="\r") as lm: lm.apc.stop() def test_modulation_on_time(): with expected_protocol( - ondax.LM, - [ - "stsont?", - "stsont:20.0" - ], - [ - "10", - "OK" - ], - sep="\r" + ondax.LM, ["stsont?", "stsont:20"], ["10", "OK"], sep="\r" ) as lm: - assert lm.modulation.on_time == 10 * quantities.ms - lm.modulation.on_time = 20 * quantities.ms + assert lm.modulation.on_time == 10 * u.ms + lm.modulation.on_time = 20 * u.ms def test_modulation_off_time(): with expected_protocol( - ondax.LM, - [ - "stsofft?", - "stsofft:20.0" - ], - [ - "10", - "OK" - ], - sep="\r" + ondax.LM, ["stsofft?", "stsofft:20"], ["10", "OK"], sep="\r" ) as lm: - assert lm.modulation.off_time == 10 * quantities.ms - lm.modulation.off_time = 20 * quantities.ms + assert lm.modulation.off_time == 10 * u.ms + lm.modulation.off_time = 20 * u.ms def test_modulation_enabled(): - with expected_protocol( - ondax.LM, - [ - "stm" - ], - [ - "OK" - ], - sep="\r" - ) as lm: + with expected_protocol(ondax.LM, ["stm"], ["OK"], sep="\r") as lm: lm.modulation.enabled = True assert lm.modulation.enabled def test_modulation_disabled(): - with expected_protocol( - ondax.LM, - [ - "ctm" - ], - [ - "OK" - ], - sep="\r" - ) as lm: + with expected_protocol(ondax.LM, ["ctm"], ["OK"], sep="\r") as lm: lm.modulation.enabled = False assert not lm.modulation.enabled -@raises(TypeError) def test_modulation_enable_not_boolean(): - with expected_protocol( - ondax.LM, - [], - [], - sep="\r" - ) as lm: - lm.modulation.enabled = "foobar" + with pytest.raises(TypeError): + with expected_protocol(ondax.LM, [], [], sep="\r") as lm: + lm.modulation.enabled = "foobar" def test_tec_current(): - with expected_protocol( - ondax.LM, - [ - "rti?" - ], - [ - "100" - ], - sep="\r" - ) as lm: - assert lm.tec.current == 100 * quantities.mA + with expected_protocol(ondax.LM, ["rti?"], ["100"], sep="\r") as lm: + assert lm.tec.current == 100 * u.mA def test_tec_target(): - with expected_protocol( - ondax.LM, - [ - "rstt?" - ], - [ - "22" - ], - sep="\r" - ) as lm: - assert lm.tec.target == 22 * quantities.degC + with expected_protocol(ondax.LM, ["rstt?"], ["22"], sep="\r") as lm: + assert lm.tec.target == u.Quantity(22, u.degC) def test_tec_enable(): - with expected_protocol( - ondax.LM, - [ - "tecon" - ], - [ - "OK" - ], - sep="\r" - ) as lm: + with expected_protocol(ondax.LM, ["tecon"], ["OK"], sep="\r") as lm: lm.tec.enabled = True assert lm.tec.enabled def test_tec_disable(): - with expected_protocol( - ondax.LM, - [ - "tecoff" - ], - [ - "OK" - ], - sep="\r" - ) as lm: + with expected_protocol(ondax.LM, ["tecoff"], ["OK"], sep="\r") as lm: lm.tec.enabled = False assert not lm.tec.enabled -@raises(TypeError) def test_tec_enable_not_boolean(): - with expected_protocol( - ondax.LM, - [], - [], - sep="\r" - ) as lm: - lm.tec.enabled = "foobar" + with pytest.raises(TypeError): + with expected_protocol(ondax.LM, [], [], sep="\r") as lm: + lm.tec.enabled = "foobar" def test_firmware(): - with expected_protocol( - ondax.LM, - [ - "rsv?" - ], - [ - "3.27" - ], - sep="\r" - ) as lm: + with expected_protocol(ondax.LM, ["rsv?"], ["3.27"], sep="\r") as lm: assert lm.firmware == "3.27" def test_current(): with expected_protocol( - ondax.LM, - [ - "rli?", - "slc:100.0" - ], - [ - "120", - "OK" - ], - sep="\r" + ondax.LM, ["rli?", "slc:100"], ["120", "OK"], sep="\r" ) as lm: - assert lm.current == 120 * quantities.mA - lm.current = 100 * quantities.mA + assert lm.current == 120 * u.mA + lm.current = 100 * u.mA def test_maximum_current(): with expected_protocol( - ondax.LM, - [ - "rlcm?", - "smlc:100.0" - ], - [ - "120", - "OK" - ], - sep="\r" + ondax.LM, ["rlcm?", "smlc:100"], ["120", "OK"], sep="\r" ) as lm: - assert lm.maximum_current == 120 * quantities.mA - lm.maximum_current = 100 * quantities.mA + assert lm.maximum_current == 120 * u.mA + lm.maximum_current = 100 * u.mA def test_power(): with expected_protocol( - ondax.LM, - [ - "rlp?", - "slp:100.0" - ], - [ - "120", - "OK" - ], - sep="\r" + ondax.LM, ["rlp?", "slp:100"], ["120", "OK"], sep="\r" ) as lm: - assert lm.power == 120 * quantities.mW - lm.power = 100 * quantities.mW + assert lm.power == 120 * u.mW + lm.power = 100 * u.mW def test_serial_number(): - with expected_protocol( - ondax.LM, - [ - "rsn?" - ], - [ - "B099999" - ], - sep="\r" - ) as lm: + with expected_protocol(ondax.LM, ["rsn?"], ["B099999"], sep="\r") as lm: assert lm.serial_number == "B099999" def test_status(): - with expected_protocol( - ondax.LM, - [ - "rlrs?" - ], - [ - "1" - ], - sep="\r" - ) as lm: + with expected_protocol(ondax.LM, ["rlrs?"], ["1"], sep="\r") as lm: assert lm.status == lm.Status(1) def test_temperature(): - with expected_protocol( - ondax.LM, - [ - "rtt?", - "stt:40.0" - ], - [ - "35", - "OK" - ], - sep="\r" - ) as lm: - assert lm.temperature == 35 * quantities.degC - lm.temperature = 40 * quantities.degC + with expected_protocol(ondax.LM, ["rtt?", "stt:40"], ["35", "OK"], sep="\r") as lm: + assert lm.temperature == u.Quantity(35, u.degC) + lm.temperature = u.Quantity(40, u.degC) def test_enable(): - with expected_protocol( - ondax.LM, - [ - "lon" - ], - [ - "OK" - ], - sep="\r" - ) as lm: + with expected_protocol(ondax.LM, ["lon"], ["OK"], sep="\r") as lm: lm.enabled = True assert lm.enabled def test_disable(): - with expected_protocol( - ondax.LM, - [ - "loff" - ], - [ - "OK" - ], - sep="\r" - ) as lm: + with expected_protocol(ondax.LM, ["loff"], ["OK"], sep="\r") as lm: lm.enabled = False assert not lm.enabled -@raises(TypeError) def test_enable_not_boolean(): - with expected_protocol( - ondax.LM, - [], - [], - sep="\r" - ) as lm: - lm.enabled = "foobar" + with pytest.raises(TypeError): + with expected_protocol(ondax.LM, [], [], sep="\r") as lm: + lm.enabled = "foobar" def test_save(): - with expected_protocol( - ondax.LM, - [ - "ssc" - ], - [ - "OK" - ], - sep="\r" - ) as lm: + with expected_protocol(ondax.LM, ["ssc"], ["OK"], sep="\r") as lm: lm.save() def test_reset(): - with expected_protocol( - ondax.LM, - [ - "reset" - ], - [ - "OK" - ], - sep="\r" - ) as lm: + with expected_protocol(ondax.LM, ["reset"], ["OK"], sep="\r") as lm: lm.reset() diff --git a/instruments/tests/test_oxford/test_oxforditc503.py b/instruments/tests/test_oxford/test_oxforditc503.py index 73915bee6..f09a15b5c 100644 --- a/instruments/tests/test_oxford/test_oxforditc503.py +++ b/instruments/tests/test_oxford/test_oxforditc503.py @@ -1,45 +1,27 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Unit tests for the Oxford ITC 503 temperature controller """ # IMPORTS ##################################################################### -from __future__ import absolute_import - -import quantities as pq import instruments as ik from instruments.tests import expected_protocol +from instruments.units import ureg as u # TESTS ####################################################################### def test_sensor_returns_sensor_class(): - with expected_protocol( - ik.oxford.OxfordITC503, - [ - "C3" - ], - [], - sep="\r" - ) as inst: + with expected_protocol(ik.oxford.OxfordITC503, ["C3"], [], sep="\r") as inst: sensor = inst.sensor[0] assert isinstance(sensor, inst.Sensor) is True def test_sensor_temperature(): with expected_protocol( - ik.oxford.OxfordITC503, - [ - "C3", - "R1" - ], - [ - "R123" - ], - sep="\r" + ik.oxford.OxfordITC503, ["C3", "R1"], ["R123"], sep="\r" ) as inst: sensor = inst.sensor[0] - assert sensor.temperature == 123 * pq.kelvin + assert sensor.temperature == u.Quantity(123, u.kelvin) diff --git a/instruments/tests/test_package.py b/instruments/tests/test_package.py new file mode 100644 index 000000000..6388908f7 --- /dev/null +++ b/instruments/tests/test_package.py @@ -0,0 +1,15 @@ +""" +Module containing tests for the base instruments package +""" + +# IMPORTS #################################################################### + +import instruments._version as ik_version_file + + +# TEST CASES ################################################################# + + +def test_package_has_version(): + assert hasattr(ik_version_file, "version") + assert hasattr(ik_version_file, "version_tuple") diff --git a/instruments/tests/test_phasematrix/test_phasematrix_fsw0020.py b/instruments/tests/test_phasematrix/test_phasematrix_fsw0020.py index 220580fbc..a53a2edcb 100644 --- a/instruments/tests/test_phasematrix/test_phasematrix_fsw0020.py +++ b/instruments/tests/test_phasematrix/test_phasematrix_fsw0020.py @@ -1,123 +1,110 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Unit tests for the Phasematrix FSW0020 """ # IMPORTS ##################################################################### -from __future__ import absolute_import -import quantities as pq +import pytest + +from instruments.units import ureg as u import instruments as ik from instruments.tests import expected_protocol -from instruments.units import mHz, dBm, cBm # TESTS ####################################################################### def test_reset(): - with expected_protocol( - ik.phasematrix.PhaseMatrixFSW0020, - [ - "0E." - ], - [] - ) as inst: + with expected_protocol(ik.phasematrix.PhaseMatrixFSW0020, ["0E."], []) as inst: inst.reset() def test_frequency(): with expected_protocol( ik.phasematrix.PhaseMatrixFSW0020, - [ - "04.", - "0C{:012X}.".format(int((10 * pq.GHz).rescale(mHz).magnitude)) - ], - [ - "00E8D4A51000" - ] + ["04.", f"0C{int((10 * u.GHz).to(u.mHz).magnitude):012X}."], + ["00E8D4A51000"], ) as inst: - assert inst.frequency == 1 * pq.GHz - inst.frequency = 10 * pq.GHz + assert inst.frequency == 1.0000000000000002 * u.GHz + inst.frequency = 10 * u.GHz def test_power(): with expected_protocol( ik.phasematrix.PhaseMatrixFSW0020, - [ - "0D.", - "03{:04X}.".format(int((10 * dBm).rescale(cBm).magnitude)) - ], - [ - "-064" - ] + ["0D.", f"03{int(u.Quantity(10, u.dBm).to(u.cBm).magnitude):04X}."], + ["-064"], ) as inst: - assert inst.power == -10 * dBm - inst.power = 10 * dBm + assert inst.power == u.Quantity(-10, u.dBm) + inst.power = u.Quantity(10, u.dBm) + + +def test_phase(): + """Raise NotImplementedError when phase is set / got.""" + with expected_protocol(ik.phasematrix.PhaseMatrixFSW0020, [], []) as inst: + with pytest.raises(NotImplementedError): + _ = inst.phase + with pytest.raises(NotImplementedError): + inst.phase = 42 def test_blanking(): with expected_protocol( ik.phasematrix.PhaseMatrixFSW0020, - [ - "05{:02X}.".format(1), - "05{:02X}.".format(0) - ], - [] + [f"05{1:02X}.", f"05{0:02X}."], + [], ) as inst: inst.blanking = True inst.blanking = False + with pytest.raises(NotImplementedError): + _ = inst.blanking def test_ref_output(): with expected_protocol( ik.phasematrix.PhaseMatrixFSW0020, - [ - "08{:02X}.".format(1), - "08{:02X}.".format(0) - ], - [] + [f"08{1:02X}.", f"08{0:02X}."], + [], ) as inst: inst.ref_output = True inst.ref_output = False + with pytest.raises(NotImplementedError): + _ = inst.ref_output def test_output(): with expected_protocol( ik.phasematrix.PhaseMatrixFSW0020, - [ - "0F{:02X}.".format(1), - "0F{:02X}.".format(0) - ], - [] + [f"0F{1:02X}.", f"0F{0:02X}."], + [], ) as inst: inst.output = True inst.output = False + with pytest.raises(NotImplementedError): + _ = inst.output def test_pulse_modulation(): with expected_protocol( ik.phasematrix.PhaseMatrixFSW0020, - [ - "09{:02X}.".format(1), - "09{:02X}.".format(0) - ], - [] + [f"09{1:02X}.", f"09{0:02X}."], + [], ) as inst: inst.pulse_modulation = True inst.pulse_modulation = False + with pytest.raises(NotImplementedError): + _ = inst.pulse_modulation def test_am_modulation(): with expected_protocol( ik.phasematrix.PhaseMatrixFSW0020, - [ - "0A{:02X}.".format(1), - "0A{:02X}.".format(0) - ], - [] + [f"0A{1:02X}.", f"0A{0:02X}."], + [], ) as inst: inst.am_modulation = True inst.am_modulation = False + with pytest.raises(NotImplementedError): + _ = inst.am_modulation diff --git a/instruments/tests/test_picowatt/test_picowatt_avs47.py b/instruments/tests/test_picowatt/test_picowatt_avs47.py index 2d741be87..3fea16a0a 100644 --- a/instruments/tests/test_picowatt/test_picowatt_avs47.py +++ b/instruments/tests/test_picowatt/test_picowatt_avs47.py @@ -1,14 +1,12 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Unit tests for the Picowatt AVS47 """ # IMPORTS ##################################################################### -from __future__ import absolute_import -import quantities as pq +from instruments.units import ureg as u import instruments as ik from instruments.tests import expected_protocol @@ -22,67 +20,31 @@ def test_sensor_is_sensor_class(): def test_init(): - with expected_protocol( - ik.picowatt.PicowattAVS47, - [ - "HDR 0" - ], - [] - ) as _: + with expected_protocol(ik.picowatt.PicowattAVS47, ["HDR 0"], []): pass def test_sensor_resistance_same_channel(): with expected_protocol( - ik.picowatt.PicowattAVS47, - [ - "HDR 0", - "MUX?", - "ADC", - "RES?" - ], - [ - "0", - "123" - ] + ik.picowatt.PicowattAVS47, ["HDR 0", "MUX?", "ADC", "RES?"], ["0", "123"] ) as inst: - assert inst.sensor[0].resistance == 123 * pq.ohm + assert inst.sensor[0].resistance == 123 * u.ohm def test_sensor_resistance_different_channel(): with expected_protocol( ik.picowatt.PicowattAVS47, - [ - "HDR 0", - "MUX?", - "INP 0", - "MUX 0", - "INP 1", - "ADC", - "RES?" - ], - [ - "1", - "123" - ] + ["HDR 0", "MUX?", "INP 0", "MUX 0", "INP 1", "ADC", "RES?"], + ["1", "123"], ) as inst: - assert inst.sensor[0].resistance == 123 * pq.ohm + assert inst.sensor[0].resistance == 123 * u.ohm def test_remote(): with expected_protocol( ik.picowatt.PicowattAVS47, - [ - "HDR 0", - "REM?", - "REM?", - "REM 1", - "REM 0" - ], - [ - "0", - "1" - ] + ["HDR 0", "REM?", "REM?", "REM 1", "REM 0"], + ["0", "1"], ) as inst: assert inst.remote is False assert inst.remote is True @@ -93,14 +55,10 @@ def test_remote(): def test_input_source(): with expected_protocol( ik.picowatt.PicowattAVS47, - [ - "HDR 0", - "INP?", - "INP 1" - ], + ["HDR 0", "INP?", "INP 1"], [ "0", - ] + ], ) as inst: assert inst.input_source == inst.InputSource.ground inst.input_source = inst.InputSource.actual @@ -109,14 +67,10 @@ def test_input_source(): def test_mux_channel(): with expected_protocol( ik.picowatt.PicowattAVS47, - [ - "HDR 0", - "MUX?", - "MUX 1" - ], + ["HDR 0", "MUX?", "MUX 1"], [ "3", - ] + ], ) as inst: assert inst.mux_channel == 3 inst.mux_channel = 1 @@ -125,14 +79,10 @@ def test_mux_channel(): def test_excitation(): with expected_protocol( ik.picowatt.PicowattAVS47, - [ - "HDR 0", - "EXC?", - "EXC 1" - ], + ["HDR 0", "EXC?", "EXC 1"], [ "3", - ] + ], ) as inst: assert inst.excitation == 3 inst.excitation = 1 @@ -141,14 +91,10 @@ def test_excitation(): def test_display(): with expected_protocol( ik.picowatt.PicowattAVS47, - [ - "HDR 0", - "DIS?", - "DIS 1" - ], + ["HDR 0", "DIS?", "DIS 1"], [ "3", - ] + ], ) as inst: assert inst.display == 3 inst.display = 1 diff --git a/instruments/tests/test_property_factories/__init__.py b/instruments/tests/test_property_factories/__init__.py index 7e23dba58..997b51218 100644 --- a/instruments/tests/test_property_factories/__init__.py +++ b/instruments/tests/test_property_factories/__init__.py @@ -1,13 +1,10 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing common code for testing the property factories """ # IMPORTS #################################################################### -from __future__ import absolute_import -from __future__ import unicode_literals from io import StringIO @@ -16,7 +13,7 @@ # pylint: disable=missing-docstring -class MockInstrument(object): +class MockInstrument: """ Mock class that admits sendcmd/query but little else such that property @@ -32,7 +29,7 @@ def value(self): return self._buf.getvalue() def sendcmd(self, cmd): - self._buf.write("{}\n".format(cmd)) + self._buf.write(f"{cmd}\n") def query(self, cmd): self.sendcmd(cmd) diff --git a/instruments/tests/test_property_factories/test_bool_property.py b/instruments/tests/test_property_factories/test_bool_property.py index 9fc36fc19..a8cf266df 100644 --- a/instruments/tests/test_property_factories/test_bool_property.py +++ b/instruments/tests/test_property_factories/test_bool_property.py @@ -1,14 +1,12 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing tests for the bool property factories """ # IMPORTS #################################################################### -from __future__ import absolute_import -from nose.tools import raises, eq_ +import pytest from instruments.util_fns import bool_property from . import MockInstrument @@ -18,66 +16,81 @@ # pylint: disable=missing-docstring + def test_bool_property_basics(): class BoolMock(MockInstrument): - mock1 = bool_property('MOCK1', 'ON', 'OFF') - mock2 = bool_property('MOCK2', 'YES', 'NO') + mock1 = bool_property("MOCK1") + mock2 = bool_property("MOCK2", inst_true="YES", inst_false="NO") - mock_inst = BoolMock({'MOCK1?': 'OFF', 'MOCK2?': 'YES'}) + mock_inst = BoolMock({"MOCK1?": "OFF", "MOCK2?": "YES"}) - eq_(mock_inst.mock1, False) - eq_(mock_inst.mock2, True) + assert mock_inst.mock1 is False + assert mock_inst.mock2 is True mock_inst.mock1 = True mock_inst.mock2 = False - eq_(mock_inst.value, 'MOCK1?\nMOCK2?\nMOCK1 ON\nMOCK2 NO\n') + assert mock_inst.value == "MOCK1?\nMOCK2?\nMOCK1 ON\nMOCK2 NO\n" def test_bool_property_set_fmt(): class BoolMock(MockInstrument): - mock1 = bool_property('MOCK1', 'ON', 'OFF', set_fmt="{}={}") + mock1 = bool_property("MOCK1", set_fmt="{}={}") - mock_instrument = BoolMock({'MOCK1?': 'OFF'}) + mock_instrument = BoolMock({"MOCK1?": "OFF"}) mock_instrument.mock1 = True - eq_(mock_instrument.value, 'MOCK1=ON\n') + assert mock_instrument.value == "MOCK1=ON\n" -@raises(AttributeError) def test_bool_property_readonly_writing_fails(): - class BoolMock(MockInstrument): - mock1 = bool_property('MOCK1', 'ON', 'OFF', readonly=True) + with pytest.raises(AttributeError): - mock_instrument = BoolMock({'MOCK1?': 'OFF'}) + class BoolMock(MockInstrument): + mock1 = bool_property("MOCK1", readonly=True) - mock_instrument.mock1 = True + mock_instrument = BoolMock({"MOCK1?": "OFF"}) + + mock_instrument.mock1 = True def test_bool_property_readonly_reading_passes(): class BoolMock(MockInstrument): - mock1 = bool_property('MOCK1', 'ON', 'OFF', readonly=True) + mock1 = bool_property("MOCK1", readonly=True) - mock_instrument = BoolMock({'MOCK1?': 'OFF'}) + mock_instrument = BoolMock({"MOCK1?": "OFF"}) - eq_(mock_instrument.mock1, False) + assert mock_instrument.mock1 is False -@raises(AttributeError) def test_bool_property_writeonly_reading_fails(): - class BoolMock(MockInstrument): - mock1 = bool_property('MOCK1', 'ON', 'OFF', writeonly=True) + with pytest.raises(AttributeError): + + class BoolMock(MockInstrument): + mock1 = bool_property("MOCK1", writeonly=True) - mock_instrument = BoolMock({'MOCK1?': 'OFF'}) + mock_instrument = BoolMock({"MOCK1?": "OFF"}) - _ = mock_instrument.mock1 + _ = mock_instrument.mock1 def test_bool_property_writeonly_writing_passes(): class BoolMock(MockInstrument): - mock1 = bool_property('MOCK1', 'ON', 'OFF', writeonly=True) + mock1 = bool_property("MOCK1", writeonly=True) - mock_instrument = BoolMock({'MOCK1?': 'OFF'}) + mock_instrument = BoolMock({"MOCK1?": "OFF"}) mock_instrument.mock1 = False + + +def test_bool_property_set_cmd(): + class BoolMock(MockInstrument): + mock1 = bool_property("MOCK1", set_cmd="FOOBAR") + + mock_inst = BoolMock({"MOCK1?": "OFF"}) + + assert mock_inst.mock1 is False + mock_inst.mock1 = True + + assert mock_inst.value == "MOCK1?\nFOOBAR ON\n" diff --git a/instruments/tests/test_property_factories/test_bounded_unitful_property.py b/instruments/tests/test_property_factories/test_bounded_unitful_property.py index 543320c5c..bf777df5a 100644 --- a/instruments/tests/test_property_factories/test_bounded_unitful_property.py +++ b/instruments/tests/test_property_factories/test_bounded_unitful_property.py @@ -1,19 +1,17 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing tests for the bounded unitful property factories """ # IMPORTS #################################################################### -from __future__ import absolute_import -from nose.tools import raises, eq_ -import mock -import quantities as pq +import pytest +from instruments.units import ureg as u from instruments.util_fns import bounded_unitful_property from . import MockInstrument +from .. import mock # TEST CASES ################################################################# @@ -23,142 +21,119 @@ def test_bounded_unitful_property_basics(): class BoundedUnitfulMock(MockInstrument): property, property_min, property_max = bounded_unitful_property( - 'MOCK', - units=pq.hertz + "MOCK", units=u.hertz ) mock_inst = BoundedUnitfulMock( - {'MOCK?': '1000', 'MOCK:MIN?': '10', 'MOCK:MAX?': '9999'}) + {"MOCK?": "1000", "MOCK:MIN?": "10", "MOCK:MAX?": "9999"} + ) - eq_(mock_inst.property, 1000 * pq.hertz) - eq_(mock_inst.property_min, 10 * pq.hertz) - eq_(mock_inst.property_max, 9999 * pq.hertz) + assert mock_inst.property == 1000 * u.hertz + assert mock_inst.property_min == 10 * u.hertz + assert mock_inst.property_max == 9999 * u.hertz - mock_inst.property = 1000 * pq.hertz + mock_inst.property = 1000 * u.hertz -@raises(ValueError) def test_bounded_unitful_property_set_outside_max(): - class BoundedUnitfulMock(MockInstrument): - property, property_min, property_max = bounded_unitful_property( - 'MOCK', - units=pq.hertz - ) + with pytest.raises(ValueError): - mock_inst = BoundedUnitfulMock( - {'MOCK?': '1000', 'MOCK:MIN?': '10', 'MOCK:MAX?': '9999'}) + class BoundedUnitfulMock(MockInstrument): + property, property_min, property_max = bounded_unitful_property( + "MOCK", units=u.hertz + ) - mock_inst.property = 10000 * pq.hertz # Should raise ValueError + mock_inst = BoundedUnitfulMock( + {"MOCK?": "1000", "MOCK:MIN?": "10", "MOCK:MAX?": "9999"} + ) + + mock_inst.property = 10000 * u.hertz # Should raise ValueError -@raises(ValueError) def test_bounded_unitful_property_set_outside_min(): - class BoundedUnitfulMock(MockInstrument): - property, property_min, property_max = bounded_unitful_property( - 'MOCK', - units=pq.hertz - ) + with pytest.raises(ValueError): - mock_inst = BoundedUnitfulMock( - {'MOCK?': '1000', 'MOCK:MIN?': '10', 'MOCK:MAX?': '9999'}) + class BoundedUnitfulMock(MockInstrument): + property, property_min, property_max = bounded_unitful_property( + "MOCK", units=u.hertz + ) + + mock_inst = BoundedUnitfulMock( + {"MOCK?": "1000", "MOCK:MIN?": "10", "MOCK:MAX?": "9999"} + ) - mock_inst.property = 1 * pq.hertz # Should raise ValueError + mock_inst.property = 1 * u.hertz # Should raise ValueError def test_bounded_unitful_property_min_fmt_str(): class BoundedUnitfulMock(MockInstrument): property, property_min, property_max = bounded_unitful_property( - 'MOCK', - units=pq.hertz, - min_fmt_str="{} MIN?" + "MOCK", units=u.hertz, min_fmt_str="{} MIN?" ) - mock_inst = BoundedUnitfulMock({'MOCK MIN?': '10'}) + mock_inst = BoundedUnitfulMock({"MOCK MIN?": "10"}) - eq_(mock_inst.property_min, 10 * pq.Hz) - eq_(mock_inst.value, 'MOCK MIN?\n') + assert mock_inst.property_min == 10 * u.Hz + assert mock_inst.value == "MOCK MIN?\n" def test_bounded_unitful_property_max_fmt_str(): class BoundedUnitfulMock(MockInstrument): property, property_min, property_max = bounded_unitful_property( - 'MOCK', - units=pq.hertz, - max_fmt_str="{} MAX?" + "MOCK", units=u.hertz, max_fmt_str="{} MAX?" ) - mock_inst = BoundedUnitfulMock({'MOCK MAX?': '9999'}) + mock_inst = BoundedUnitfulMock({"MOCK MAX?": "9999"}) - eq_(mock_inst.property_max, 9999 * pq.Hz) - eq_(mock_inst.value, 'MOCK MAX?\n') + assert mock_inst.property_max == 9999 * u.Hz + assert mock_inst.value == "MOCK MAX?\n" def test_bounded_unitful_property_static_range(): class BoundedUnitfulMock(MockInstrument): property, property_min, property_max = bounded_unitful_property( - 'MOCK', - units=pq.hertz, - valid_range=(10, 9999) + "MOCK", units=u.hertz, valid_range=(10, 9999) ) mock_inst = BoundedUnitfulMock() - eq_(mock_inst.property_min, 10 * pq.Hz) - eq_(mock_inst.property_max, 9999 * pq.Hz) + assert mock_inst.property_min == 10 * u.Hz + assert mock_inst.property_max == 9999 * u.Hz def test_bounded_unitful_property_static_range_with_units(): class BoundedUnitfulMock(MockInstrument): property, property_min, property_max = bounded_unitful_property( - 'MOCK', - units=pq.hertz, - valid_range=(10 * pq.kilohertz, 9999 * pq.kilohertz) + "MOCK", units=u.hertz, valid_range=(10 * u.kilohertz, 9999 * u.kilohertz) ) mock_inst = BoundedUnitfulMock() - eq_(mock_inst.property_min, 10 * 1000 * pq.Hz) - eq_(mock_inst.property_max, 9999 * 1000 * pq.Hz) + assert mock_inst.property_min == 10 * 1000 * u.Hz + assert mock_inst.property_max == 9999 * 1000 * u.Hz @mock.patch("instruments.util_fns.unitful_property") def test_bounded_unitful_property_passes_kwargs(mock_unitful_property): - bounded_unitful_property( - name='MOCK', - units=pq.Hz, - derp="foobar" - ) + bounded_unitful_property(command="MOCK", units=u.Hz, derp="foobar") mock_unitful_property.assert_called_with( - 'MOCK', - pq.Hz, - derp="foobar", - valid_range=(mock.ANY, mock.ANY) + "MOCK", u.Hz, derp="foobar", valid_range=(mock.ANY, mock.ANY) ) @mock.patch("instruments.util_fns.unitful_property") def test_bounded_unitful_property_valid_range_none(mock_unitful_property): - bounded_unitful_property( - name='MOCK', - units=pq.Hz, - valid_range=(None, None) - ) - mock_unitful_property.assert_called_with( - 'MOCK', - pq.Hz, - valid_range=(None, None) - ) + bounded_unitful_property(command="MOCK", units=u.Hz, valid_range=(None, None)) + mock_unitful_property.assert_called_with("MOCK", u.Hz, valid_range=(None, None)) def test_bounded_unitful_property_returns_none(): class BoundedUnitfulMock(MockInstrument): property, property_min, property_max = bounded_unitful_property( - 'MOCK', - units=pq.hertz, - valid_range=(None, None) + "MOCK", units=u.hertz, valid_range=(None, None) ) mock_inst = BoundedUnitfulMock() - eq_(mock_inst.property_min, None) - eq_(mock_inst.property_max, None) + assert mock_inst.property_min is None + assert mock_inst.property_max is None diff --git a/instruments/tests/test_property_factories/test_enum_property.py b/instruments/tests/test_property_factories/test_enum_property.py index 6384e7305..e02ce1760 100644 --- a/instruments/tests/test_property_factories/test_enum_property.py +++ b/instruments/tests/test_property_factories/test_enum_property.py @@ -1,15 +1,13 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing tests for the enum property factories """ # IMPORTS #################################################################### -from __future__ import absolute_import from enum import Enum, IntEnum -from nose.tools import raises, eq_ +import pytest from instruments.util_fns import enum_property from . import MockInstrument @@ -21,71 +19,68 @@ def test_enum_property(): class SillyEnum(Enum): - a = 'aa' - b = 'bb' + a = "aa" + b = "bb" class EnumMock(MockInstrument): - a = enum_property('MOCK:A', SillyEnum) - b = enum_property('MOCK:B', SillyEnum) + a = enum_property("MOCK:A", SillyEnum) + b = enum_property("MOCK:B", SillyEnum) - mock_inst = EnumMock({'MOCK:A?': 'aa', 'MOCK:B?': 'bb'}) + mock_inst = EnumMock({"MOCK:A?": "aa", "MOCK:B?": "bb"}) - eq_(mock_inst.a, SillyEnum.a) - eq_(mock_inst.b, SillyEnum.b) + assert mock_inst.a == SillyEnum.a + assert mock_inst.b == SillyEnum.b # Test EnumValues, string values and string names. mock_inst.a = SillyEnum.b - mock_inst.b = 'a' - mock_inst.b = 'bb' + mock_inst.b = "a" + mock_inst.b = "bb" - eq_(mock_inst.value, 'MOCK:A?\nMOCK:B?\nMOCK:A bb\nMOCK:B aa\nMOCK:B bb\n') + assert mock_inst.value == "MOCK:A?\nMOCK:B?\nMOCK:A bb\nMOCK:B aa\nMOCK:B bb\n" -@raises(ValueError) def test_enum_property_invalid(): - class SillyEnum(Enum): - a = 'aa' - b = 'bb' + with pytest.raises(ValueError): - class EnumMock(MockInstrument): - a = enum_property('MOCK:A', SillyEnum) + class SillyEnum(Enum): + a = "aa" + b = "bb" + + class EnumMock(MockInstrument): + a = enum_property("MOCK:A", SillyEnum) - mock_inst = EnumMock({'MOCK:A?': 'aa', 'MOCK:B?': 'bb'}) + mock_inst = EnumMock({"MOCK:A?": "aa", "MOCK:B?": "bb"}) - mock_inst.a = 'c' + mock_inst.a = "c" def test_enum_property_set_fmt(): class SillyEnum(Enum): - a = 'aa' + a = "aa" class EnumMock(MockInstrument): - a = enum_property('MOCK:A', SillyEnum, set_fmt="{}={}") + a = enum_property("MOCK:A", SillyEnum, set_fmt="{}={}") mock_instrument = EnumMock() - mock_instrument.a = 'aa' - eq_(mock_instrument.value, 'MOCK:A=aa\n') + mock_instrument.a = "aa" + assert mock_instrument.value == "MOCK:A=aa\n" def test_enum_property_input_decoration(): class SillyEnum(Enum): - a = 'aa' + a = "aa" class EnumMock(MockInstrument): - @staticmethod def _input_decorator(_): - return 'aa' - a = enum_property( - 'MOCK:A', - SillyEnum, - input_decoration=_input_decorator - ) + return "aa" + + a = enum_property("MOCK:A", SillyEnum, input_decoration=_input_decorator) - mock_instrument = EnumMock({'MOCK:A?': 'garbage'}) + mock_instrument = EnumMock({"MOCK:A?": "garbage"}) - eq_(mock_instrument.a, SillyEnum.a) + assert mock_instrument.a == SillyEnum.a def test_enum_property_input_decoration_not_a_function(): @@ -94,105 +89,110 @@ class SillyEnum(IntEnum): class EnumMock(MockInstrument): - a = enum_property( - 'MOCK:A', - SillyEnum, - input_decoration=int - ) + a = enum_property("MOCK:A", SillyEnum, input_decoration=int) - mock_instrument = EnumMock({'MOCK:A?': '1'}) + mock_instrument = EnumMock({"MOCK:A?": "1"}) - eq_(mock_instrument.a, SillyEnum.a) + assert mock_instrument.a == SillyEnum.a def test_enum_property_output_decoration(): class SillyEnum(Enum): - a = 'aa' + a = "aa" class EnumMock(MockInstrument): - @staticmethod def _output_decorator(_): - return 'foobar' - a = enum_property( - 'MOCK:A', - SillyEnum, - output_decoration=_output_decorator - ) + return "foobar" + + a = enum_property("MOCK:A", SillyEnum, output_decoration=_output_decorator) mock_instrument = EnumMock() mock_instrument.a = SillyEnum.a - eq_(mock_instrument.value, 'MOCK:A foobar\n') + assert mock_instrument.value == "MOCK:A foobar\n" def test_enum_property_output_decoration_not_a_function(): class SillyEnum(Enum): - a = '.23' + a = ".23" class EnumMock(MockInstrument): - a = enum_property( - 'MOCK:A', - SillyEnum, - output_decoration=float - ) + a = enum_property("MOCK:A", SillyEnum, output_decoration=float) mock_instrument = EnumMock() mock_instrument.a = SillyEnum.a - eq_(mock_instrument.value, 'MOCK:A 0.23\n') + assert mock_instrument.value == "MOCK:A 0.23\n" -@raises(AttributeError) def test_enum_property_writeonly_reading_fails(): - class SillyEnum(Enum): - a = 'aa' + with pytest.raises(AttributeError): - class EnumMock(MockInstrument): - a = enum_property('MOCK:A', SillyEnum, writeonly=True) + class SillyEnum(Enum): + a = "aa" - mock_instrument = EnumMock() + class EnumMock(MockInstrument): + a = enum_property("MOCK:A", SillyEnum, writeonly=True) + + mock_instrument = EnumMock() - _ = mock_instrument.a + _ = mock_instrument.a def test_enum_property_writeonly_writing_passes(): class SillyEnum(Enum): - a = 'aa' + a = "aa" class EnumMock(MockInstrument): - a = enum_property('MOCK:A', SillyEnum, writeonly=True) + a = enum_property("MOCK:A", SillyEnum, writeonly=True) mock_instrument = EnumMock() mock_instrument.a = SillyEnum.a - eq_(mock_instrument.value, 'MOCK:A aa\n') + assert mock_instrument.value == "MOCK:A aa\n" -@raises(AttributeError) def test_enum_property_readonly_writing_fails(): + with pytest.raises(AttributeError): + + class SillyEnum(Enum): + a = "aa" + + class EnumMock(MockInstrument): + a = enum_property("MOCK:A", SillyEnum, readonly=True) + + mock_instrument = EnumMock({"MOCK:A?": "aa"}) + + mock_instrument.a = SillyEnum.a + + +def test_enum_property_readonly_reading_passes(): class SillyEnum(Enum): - a = 'aa' + a = "aa" class EnumMock(MockInstrument): - a = enum_property('MOCK:A', SillyEnum, readonly=True) + a = enum_property("MOCK:A", SillyEnum, readonly=True) - mock_instrument = EnumMock({'MOCK:A?': 'aa'}) + mock_instrument = EnumMock({"MOCK:A?": "aa"}) - mock_instrument.a = SillyEnum.a + assert mock_instrument.a == SillyEnum.a + assert mock_instrument.value == "MOCK:A?\n" -def test_enum_property_readonly_reading_passes(): +def test_enum_property_set_cmd(): class SillyEnum(Enum): - a = 'aa' + a = "aa" class EnumMock(MockInstrument): - a = enum_property('MOCK:A', SillyEnum, readonly=True) + a = enum_property("MOCK:A", SillyEnum, set_cmd="FOOBAR:A") + + mock_inst = EnumMock({"MOCK:A?": "aa"}) - mock_instrument = EnumMock({'MOCK:A?': 'aa'}) + assert mock_inst.a == SillyEnum.a + mock_inst.a = SillyEnum.a - eq_(mock_instrument.a, SillyEnum.a) - eq_(mock_instrument.value, 'MOCK:A?\n') + assert mock_inst.value == "MOCK:A?\nFOOBAR:A aa\n" diff --git a/instruments/tests/test_property_factories/test_int_property.py b/instruments/tests/test_property_factories/test_int_property.py index b0bec2064..c95fc6f62 100644 --- a/instruments/tests/test_property_factories/test_int_property.py +++ b/instruments/tests/test_property_factories/test_int_property.py @@ -1,14 +1,12 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing tests for the int property factories """ # IMPORTS #################################################################### -from __future__ import absolute_import -from nose.tools import raises, eq_ +import pytest from instruments.util_fns import int_property from . import MockInstrument @@ -18,82 +16,97 @@ # pylint: disable=missing-docstring -@raises(ValueError) def test_int_property_outside_valid_set(): - class IntMock(MockInstrument): - mock_property = int_property('MOCK', valid_set=set([1, 2])) + with pytest.raises(ValueError): - mock_inst = IntMock() - mock_inst.mock_property = 3 + class IntMock(MockInstrument): + mock_property = int_property("MOCK", valid_set={1, 2}) + + mock_inst = IntMock() + mock_inst.mock_property = 3 def test_int_property_valid_set(): class IntMock(MockInstrument): - int_property = int_property('MOCK', valid_set=set([1, 2])) + int_property = int_property("MOCK", valid_set={1, 2}) - mock_inst = IntMock({'MOCK?': '1'}) + mock_inst = IntMock({"MOCK?": "1"}) - eq_(mock_inst.int_property, 1) + assert mock_inst.int_property == 1 mock_inst.int_property = 2 - eq_(mock_inst.value, 'MOCK?\nMOCK 2\n') + assert mock_inst.value == "MOCK?\nMOCK 2\n" def test_int_property_no_set(): class IntMock(MockInstrument): - int_property = int_property('MOCK') + int_property = int_property("MOCK") mock_inst = IntMock() mock_inst.int_property = 1 - eq_(mock_inst.value, 'MOCK 1\n') + assert mock_inst.value == "MOCK 1\n" -@raises(AttributeError) def test_int_property_writeonly_reading_fails(): - class IntMock(MockInstrument): - int_property = int_property('MOCK', writeonly=True) + with pytest.raises(AttributeError): - mock_inst = IntMock() + class IntMock(MockInstrument): + int_property = int_property("MOCK", writeonly=True) + + mock_inst = IntMock() - _ = mock_inst.int_property + _ = mock_inst.int_property def test_int_property_writeonly_writing_passes(): class IntMock(MockInstrument): - int_property = int_property('MOCK', writeonly=True) + int_property = int_property("MOCK", writeonly=True) mock_inst = IntMock() mock_inst.int_property = 1 - eq_(mock_inst.value, 'MOCK {:d}\n'.format(1)) + assert mock_inst.value == f"MOCK {1:d}\n" -@raises(AttributeError) def test_int_property_readonly_writing_fails(): - class IntMock(MockInstrument): - int_property = int_property('MOCK', readonly=True) + with pytest.raises(AttributeError): - mock_inst = IntMock({'MOCK?': '1'}) + class IntMock(MockInstrument): + int_property = int_property("MOCK", readonly=True) - mock_inst.int_property = 1 + mock_inst = IntMock({"MOCK?": "1"}) + + mock_inst.int_property = 1 def test_int_property_readonly_reading_passes(): class IntMock(MockInstrument): - int_property = int_property('MOCK', readonly=True) + int_property = int_property("MOCK", readonly=True) - mock_inst = IntMock({'MOCK?': '1'}) + mock_inst = IntMock({"MOCK?": "1"}) - eq_(mock_inst.int_property, 1) + assert mock_inst.int_property == 1 def test_int_property_format_code(): class IntMock(MockInstrument): - int_property = int_property('MOCK', format_code='{:e}') + int_property = int_property("MOCK", format_code="{:e}") mock_inst = IntMock() mock_inst.int_property = 1 - eq_(mock_inst.value, 'MOCK {:e}\n'.format(1)) + assert mock_inst.value == f"MOCK {1:e}\n" + + +def test_int_property_set_cmd(): + class IntMock(MockInstrument): + int_property = int_property("MOCK", set_cmd="FOOBAR") + + mock_inst = IntMock({"MOCK?": "1"}) + + assert mock_inst.int_property == 1 + mock_inst.int_property = 1 + + assert mock_inst.value == "MOCK?\nFOOBAR 1\n" diff --git a/instruments/tests/test_property_factories/test_rproperty.py b/instruments/tests/test_property_factories/test_rproperty.py index 9e64235a8..8391fca6c 100644 --- a/instruments/tests/test_property_factories/test_rproperty.py +++ b/instruments/tests/test_property_factories/test_rproperty.py @@ -1,14 +1,12 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing tests for the property factories """ # IMPORTS #################################################################### -from __future__ import absolute_import -from nose.tools import raises, eq_ +import pytest from instruments.util_fns import rproperty from . import MockInstrument @@ -21,9 +19,8 @@ def test_rproperty_basic(): class Mock(MockInstrument): - def __init__(self): - super(Mock, self).__init__() + super().__init__() self._value = 0 def mockget(self): @@ -31,75 +28,78 @@ def mockget(self): def mockset(self, newval): self._value = newval + mockproperty = rproperty(fget=mockget, fset=mockset) mock_inst = Mock() mock_inst.mockproperty = 1 - eq_(mock_inst.mockproperty, 1) + assert mock_inst.mockproperty == 1 -@raises(AttributeError) def test_rproperty_readonly_writing_fails(): - class Mock(MockInstrument): + with pytest.raises(AttributeError): - def __init__(self): - super(Mock, self).__init__() - self._value = 0 + class Mock(MockInstrument): + def __init__(self): + super().__init__() + self._value = 0 - def mockset(self, newval): # pragma: no cover - self._value = newval - mockproperty = rproperty(fget=None, fset=mockset, readonly=True) + def mockset(self, newval): # pragma: no cover + self._value = newval - mock_inst = Mock() - mock_inst.mockproperty = 1 + mockproperty = rproperty(fget=None, fset=mockset, readonly=True) + + mock_inst = Mock() + mock_inst.mockproperty = 1 def test_rproperty_readonly_reading_passes(): class Mock(MockInstrument): - def __init__(self): - super(Mock, self).__init__() + super().__init__() self._value = 0 def mockget(self): return self._value + mockproperty = rproperty(fget=mockget, fset=None, readonly=True) mock_inst = Mock() - eq_(mock_inst.mockproperty, 0) + assert mock_inst.mockproperty == 0 -@raises(AttributeError) def test_rproperty_writeonly_reading_fails(): - class Mock(MockInstrument): + with pytest.raises(AttributeError): - def __init__(self): - super(Mock, self).__init__() - self._value = 0 + class Mock(MockInstrument): + def __init__(self): + super().__init__() + self._value = 0 - def mockget(self): # pragma: no cover - return self._value - mockproperty = rproperty(fget=mockget, fset=None, writeonly=True) + def mockget(self): # pragma: no cover + return self._value - mock_inst = Mock() - eq_(mock_inst.mockproperty, 0) + mockproperty = rproperty(fget=mockget, fset=None, writeonly=True) + + mock_inst = Mock() + assert mock_inst.mockproperty == 0 def test_rproperty_writeonly_writing_passes(): class Mock(MockInstrument): - def __init__(self): - super(Mock, self).__init__() + super().__init__() self._value = 0 def mockset(self, newval): self._value = newval + mockproperty = rproperty(fget=None, fset=mockset, writeonly=True) mock_inst = Mock() mock_inst.mockproperty = 1 -@raises(ValueError) def test_rproperty_readonly_and_writeonly(): - _ = rproperty(readonly=True, writeonly=True) + with pytest.raises(ValueError): + _ = rproperty(readonly=True, writeonly=True) diff --git a/instruments/tests/test_property_factories/test_string_property.py b/instruments/tests/test_property_factories/test_string_property.py index d1c94554d..e57ef45be 100644 --- a/instruments/tests/test_property_factories/test_string_property.py +++ b/instruments/tests/test_property_factories/test_string_property.py @@ -1,14 +1,10 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing tests for the string property factories """ # IMPORTS #################################################################### -from __future__ import absolute_import - -from nose.tools import eq_ from instruments.util_fns import string_property from . import MockInstrument @@ -20,35 +16,47 @@ def test_string_property_basics(): class StringMock(MockInstrument): - mock_property = string_property('MOCK') + mock_property = string_property("MOCK") - mock_inst = StringMock({'MOCK?': '"foobar"'}) + mock_inst = StringMock({"MOCK?": '"foobar"'}) - eq_(mock_inst.mock_property, 'foobar') + assert mock_inst.mock_property == "foobar" - mock_inst.mock_property = 'foo' - eq_(mock_inst.value, 'MOCK?\nMOCK "foo"\n') + mock_inst.mock_property = "foo" + assert mock_inst.value == 'MOCK?\nMOCK "foo"\n' def test_string_property_different_bookmark_symbol(): class StringMock(MockInstrument): - mock_property = string_property('MOCK', bookmark_symbol='%^') + mock_property = string_property("MOCK", bookmark_symbol="%^") - mock_inst = StringMock({'MOCK?': '%^foobar%^'}) + mock_inst = StringMock({"MOCK?": "%^foobar%^"}) - eq_(mock_inst.mock_property, 'foobar') + assert mock_inst.mock_property == "foobar" - mock_inst.mock_property = 'foo' - eq_(mock_inst.value, 'MOCK?\nMOCK %^foo%^\n') + mock_inst.mock_property = "foo" + assert mock_inst.value == "MOCK?\nMOCK %^foo%^\n" def test_string_property_no_bookmark_symbol(): class StringMock(MockInstrument): - mock_property = string_property('MOCK', bookmark_symbol='') + mock_property = string_property("MOCK", bookmark_symbol="") + + mock_inst = StringMock({"MOCK?": "foobar"}) + + assert mock_inst.mock_property == "foobar" + + mock_inst.mock_property = "foo" + assert mock_inst.value == "MOCK?\nMOCK foo\n" + + +def test_string_property_set_cmd(): + class StringMock(MockInstrument): + mock_property = string_property("MOCK", set_cmd="FOOBAR") - mock_inst = StringMock({'MOCK?': 'foobar'}) + mock_inst = StringMock({"MOCK?": '"derp"'}) - eq_(mock_inst.mock_property, 'foobar') + assert mock_inst.mock_property == "derp" - mock_inst.mock_property = 'foo' - eq_(mock_inst.value, 'MOCK?\nMOCK foo\n') + mock_inst.mock_property = "qwerty" + assert mock_inst.value == 'MOCK?\nFOOBAR "qwerty"\n' diff --git a/instruments/tests/test_property_factories/test_unitful_property.py b/instruments/tests/test_property_factories/test_unitful_property.py index 551d28d42..49d58d038 100644 --- a/instruments/tests/test_property_factories/test_unitful_property.py +++ b/instruments/tests/test_property_factories/test_unitful_property.py @@ -1,17 +1,16 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing tests for the unitful property factories """ # IMPORTS #################################################################### -from __future__ import absolute_import -from nose.tools import raises, eq_ -import quantities as pq +import pytest +import pint from instruments.util_fns import unitful_property +from instruments.units import ureg as u from . import MockInstrument # TEST CASES ################################################################# @@ -21,112 +20,112 @@ def test_unitful_property_basics(): class UnitfulMock(MockInstrument): - unitful_property = unitful_property('MOCK', units=pq.hertz) + unitful_property = unitful_property("MOCK", units=u.hertz) - mock_inst = UnitfulMock({'MOCK?': '1000'}) + mock_inst = UnitfulMock({"MOCK?": "1000"}) - eq_(mock_inst.unitful_property, 1000 * pq.hertz) + assert mock_inst.unitful_property == 1000 * u.hertz - mock_inst.unitful_property = 1000 * pq.hertz - eq_(mock_inst.value, 'MOCK?\nMOCK {:e}\n'.format(1000)) + mock_inst.unitful_property = 1000 * u.hertz + assert mock_inst.value == f"MOCK?\nMOCK {1000:e}\n" def test_unitful_property_format_code(): class UnitfulMock(MockInstrument): - unitful_property = unitful_property( - 'MOCK', pq.hertz, format_code='{:f}') + unitful_property = unitful_property("MOCK", u.hertz, format_code="{:f}") mock_inst = UnitfulMock() - mock_inst.unitful_property = 1000 * pq.hertz - eq_(mock_inst.value, 'MOCK {:f}\n'.format(1000)) + mock_inst.unitful_property = 1000 * u.hertz + assert mock_inst.value == f"MOCK {1000:f}\n" def test_unitful_property_rescale_units(): class UnitfulMock(MockInstrument): - unitful_property = unitful_property('MOCK', pq.hertz) + unitful_property = unitful_property("MOCK", u.hertz) mock_inst = UnitfulMock() - mock_inst.unitful_property = 1 * pq.kilohertz - eq_(mock_inst.value, 'MOCK {:e}\n'.format(1000)) + mock_inst.unitful_property = 1 * u.kilohertz + assert mock_inst.value == f"MOCK {1000:e}\n" def test_unitful_property_no_units_on_set(): class UnitfulMock(MockInstrument): - unitful_property = unitful_property('MOCK', pq.hertz) + unitful_property = unitful_property("MOCK", u.hertz) mock_inst = UnitfulMock() mock_inst.unitful_property = 1000 - eq_(mock_inst.value, 'MOCK {:e}\n'.format(1000)) + assert mock_inst.value == f"MOCK {1000:e}\n" -@raises(ValueError) def test_unitful_property_wrong_units(): - class UnitfulMock(MockInstrument): - unitful_property = unitful_property('MOCK', pq.hertz) + with pytest.raises(pint.errors.DimensionalityError): - mock_inst = UnitfulMock() + class UnitfulMock(MockInstrument): + unitful_property = unitful_property("MOCK", u.hertz) - mock_inst.unitful_property = 1 * pq.volt + mock_inst = UnitfulMock() + + mock_inst.unitful_property = 1 * u.volt -@raises(AttributeError) def test_unitful_property_writeonly_reading_fails(): - class UnitfulMock(MockInstrument): - unitful_property = unitful_property('MOCK', pq.hertz, writeonly=True) + with pytest.raises(AttributeError): - mock_inst = UnitfulMock() + class UnitfulMock(MockInstrument): + unitful_property = unitful_property("MOCK", u.hertz, writeonly=True) + + mock_inst = UnitfulMock() - _ = mock_inst.unitful_property + _ = mock_inst.unitful_property def test_unitful_property_writeonly_writing_passes(): class UnitfulMock(MockInstrument): - unitful_property = unitful_property('MOCK', pq.hertz, writeonly=True) + unitful_property = unitful_property("MOCK", u.hertz, writeonly=True) mock_inst = UnitfulMock() - mock_inst.unitful_property = 1 * pq.hertz - eq_(mock_inst.value, 'MOCK {:e}\n'.format(1)) + mock_inst.unitful_property = 1 * u.hertz + assert mock_inst.value == f"MOCK {1:e}\n" -@raises(AttributeError) def test_unitful_property_readonly_writing_fails(): - class UnitfulMock(MockInstrument): - unitful_property = unitful_property('MOCK', pq.hertz, readonly=True) + with pytest.raises(AttributeError): + + class UnitfulMock(MockInstrument): + unitful_property = unitful_property("MOCK", u.hertz, readonly=True) - mock_inst = UnitfulMock({'MOCK?': '1'}) + mock_inst = UnitfulMock({"MOCK?": "1"}) - mock_inst.unitful_property = 1 * pq.hertz + mock_inst.unitful_property = 1 * u.hertz def test_unitful_property_readonly_reading_passes(): class UnitfulMock(MockInstrument): - unitful_property = unitful_property('MOCK', pq.hertz, readonly=True) + unitful_property = unitful_property("MOCK", u.hertz, readonly=True) - mock_inst = UnitfulMock({'MOCK?': '1'}) + mock_inst = UnitfulMock({"MOCK?": "1"}) - eq_(mock_inst.unitful_property, 1 * pq.hertz) + assert mock_inst.unitful_property == 1 * u.hertz def test_unitful_property_valid_range(): class UnitfulMock(MockInstrument): - unitful_property = unitful_property( - 'MOCK', pq.hertz, valid_range=(0, 10)) + unitful_property = unitful_property("MOCK", u.hertz, valid_range=(0, 10)) mock_inst = UnitfulMock() mock_inst.unitful_property = 0 mock_inst.unitful_property = 10 - eq_(mock_inst.value, 'MOCK {:e}\nMOCK {:e}\n'.format(0, 10)) + assert mock_inst.value == f"MOCK {0:e}\nMOCK {10:e}\n" def test_unitful_property_valid_range_functions(): class UnitfulMock(MockInstrument): - def min_value(self): return 0 @@ -134,111 +133,106 @@ def max_value(self): return 10 unitful_property = unitful_property( - 'MOCK', pq.hertz, valid_range=(min_value, max_value)) + "MOCK", u.hertz, valid_range=(min_value, max_value) + ) mock_inst = UnitfulMock() mock_inst.unitful_property = 0 mock_inst.unitful_property = 10 - eq_(mock_inst.value, 'MOCK {:e}\nMOCK {:e}\n'.format(0, 10)) + assert mock_inst.value == f"MOCK {0:e}\nMOCK {10:e}\n" -@raises(ValueError) def test_unitful_property_minimum_value(): - class UnitfulMock(MockInstrument): - unitful_property = unitful_property( - 'MOCK', pq.hertz, valid_range=(0, 10)) + with pytest.raises(ValueError): - mock_inst = UnitfulMock() + class UnitfulMock(MockInstrument): + unitful_property = unitful_property("MOCK", u.hertz, valid_range=(0, 10)) - mock_inst.unitful_property = -1 + mock_inst = UnitfulMock() + + mock_inst.unitful_property = -1 -@raises(ValueError) def test_unitful_property_maximum_value(): - class UnitfulMock(MockInstrument): - unitful_property = unitful_property( - 'MOCK', pq.hertz, valid_range=(0, 10)) + with pytest.raises(ValueError): - mock_inst = UnitfulMock() + class UnitfulMock(MockInstrument): + unitful_property = unitful_property("MOCK", u.hertz, valid_range=(0, 10)) + + mock_inst = UnitfulMock() - mock_inst.unitful_property = 11 + mock_inst.unitful_property = 11 def test_unitful_property_input_decoration(): class UnitfulMock(MockInstrument): - @staticmethod def _input_decorator(_): - return '1' - a = unitful_property( - 'MOCK:A', - pq.hertz, - input_decoration=_input_decorator - ) + return "1" - mock_instrument = UnitfulMock({'MOCK:A?': 'garbage'}) + a = unitful_property("MOCK:A", u.hertz, input_decoration=_input_decorator) - eq_(mock_instrument.a, 1 * pq.Hz) + mock_instrument = UnitfulMock({"MOCK:A?": "garbage"}) + + assert mock_instrument.a == 1 * u.Hz def test_unitful_property_input_decoration_not_a_function(): class UnitfulMock(MockInstrument): - a = unitful_property( - 'MOCK:A', - pq.hertz, - input_decoration=float - ) + a = unitful_property("MOCK:A", u.hertz, input_decoration=float) - mock_instrument = UnitfulMock({'MOCK:A?': '.123'}) + mock_instrument = UnitfulMock({"MOCK:A?": ".123"}) - eq_(mock_instrument.a, 0.123 * pq.Hz) + assert mock_instrument.a == 0.123 * u.Hz def test_unitful_property_output_decoration(): class UnitfulMock(MockInstrument): - @staticmethod def _output_decorator(_): - return '1' - a = unitful_property( - 'MOCK:A', - pq.hertz, - output_decoration=_output_decorator - ) + return "1" + + a = unitful_property("MOCK:A", u.hertz, output_decoration=_output_decorator) mock_instrument = UnitfulMock() - mock_instrument.a = 345 * pq.hertz + mock_instrument.a = 345 * u.hertz - eq_(mock_instrument.value, 'MOCK:A 1\n') + assert mock_instrument.value == "MOCK:A 1\n" def test_unitful_property_output_decoration_not_a_function(): class UnitfulMock(MockInstrument): - a = unitful_property( - 'MOCK:A', - pq.hertz, - output_decoration=bool - ) + a = unitful_property("MOCK:A", u.hertz, output_decoration=bool) mock_instrument = UnitfulMock() - mock_instrument.a = 1 * pq.hertz + mock_instrument.a = 1 * u.hertz - eq_(mock_instrument.value, 'MOCK:A True\n') + assert mock_instrument.value == "MOCK:A True\n" def test_unitful_property_split_str(): class UnitfulMock(MockInstrument): - unitful_property = unitful_property( - 'MOCK', pq.hertz, valid_range=(0, 10)) + unitful_property = unitful_property("MOCK", u.hertz, valid_range=(0, 10)) mock_inst = UnitfulMock({"MOCK?": "1 kHz"}) value = mock_inst.unitful_property assert value.magnitude == 1000 - assert value.units == pq.hertz + assert value.units == u.hertz + + +def test_unitful_property_name_read_not_none(): + class UnitfulMock(MockInstrument): + a = unitful_property("MOCK", units=u.hertz, set_cmd="FOOBAR") + + mock_inst = UnitfulMock({"MOCK?": "1000"}) + assert mock_inst.a == 1000 * u.hertz + mock_inst.a = 1000 * u.hertz + + assert mock_inst.value == f"MOCK?\nFOOBAR {1000:e}\n" diff --git a/instruments/tests/test_property_factories/test_unitless_property.py b/instruments/tests/test_property_factories/test_unitless_property.py index 16654d3b5..6efa4d1bb 100644 --- a/instruments/tests/test_property_factories/test_unitless_property.py +++ b/instruments/tests/test_property_factories/test_unitless_property.py @@ -1,15 +1,13 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing tests for the unitless property factory """ # IMPORTS #################################################################### -from __future__ import absolute_import -from nose.tools import raises, eq_ -import quantities as pq +import pytest +from instruments.units import ureg as u from instruments.util_fns import unitless_property from . import MockInstrument @@ -21,70 +19,85 @@ def test_unitless_property_basics(): class UnitlessMock(MockInstrument): - mock_property = unitless_property('MOCK') + mock_property = unitless_property("MOCK") - mock_inst = UnitlessMock({'MOCK?': '1'}) + mock_inst = UnitlessMock({"MOCK?": "1"}) - eq_(mock_inst.mock_property, 1) + assert mock_inst.mock_property == 1 mock_inst.mock_property = 1 - eq_(mock_inst.value, 'MOCK?\nMOCK {:e}\n'.format(1)) + assert mock_inst.value == f"MOCK?\nMOCK {1:e}\n" -@raises(ValueError) def test_unitless_property_units(): - class UnitlessMock(MockInstrument): - mock_property = unitless_property('MOCK') + with pytest.raises(ValueError): + + class UnitlessMock(MockInstrument): + mock_property = unitless_property("MOCK") - mock_inst = UnitlessMock({'MOCK?': '1'}) + mock_inst = UnitlessMock({"MOCK?": "1"}) - mock_inst.mock_property = 1 * pq.volt + mock_inst.mock_property = 1 * u.volt def test_unitless_property_format_code(): class UnitlessMock(MockInstrument): - mock_property = unitless_property('MOCK', format_code='{:f}') + mock_property = unitless_property("MOCK", format_code="{:f}") mock_inst = UnitlessMock() mock_inst.mock_property = 1 - eq_(mock_inst.value, 'MOCK {:f}\n'.format(1)) + assert mock_inst.value == f"MOCK {1:f}\n" -@raises(AttributeError) def test_unitless_property_writeonly_reading_fails(): - class UnitlessMock(MockInstrument): - mock_property = unitless_property('MOCK', writeonly=True) + with pytest.raises(AttributeError): - mock_inst = UnitlessMock() + class UnitlessMock(MockInstrument): + mock_property = unitless_property("MOCK", writeonly=True) - _ = mock_inst.mock_property + mock_inst = UnitlessMock() + + _ = mock_inst.mock_property def test_unitless_property_writeonly_writing_passes(): class UnitlessMock(MockInstrument): - mock_property = unitless_property('MOCK', writeonly=True) + mock_property = unitless_property("MOCK", writeonly=True) mock_inst = UnitlessMock() mock_inst.mock_property = 1 - eq_(mock_inst.value, 'MOCK {:e}\n'.format(1)) + assert mock_inst.value == f"MOCK {1:e}\n" -@raises(AttributeError) def test_unitless_property_readonly_writing_fails(): - class UnitlessMock(MockInstrument): - mock_property = unitless_property('MOCK', readonly=True) + with pytest.raises(AttributeError): - mock_inst = UnitlessMock({'MOCK?': '1'}) + class UnitlessMock(MockInstrument): + mock_property = unitless_property("MOCK", readonly=True) - mock_inst.mock_property = 1 + mock_inst = UnitlessMock({"MOCK?": "1"}) + + mock_inst.mock_property = 1 def test_unitless_property_readonly_reading_passes(): class UnitlessMock(MockInstrument): - mock_property = unitless_property('MOCK', readonly=True) + mock_property = unitless_property("MOCK", readonly=True) - mock_inst = UnitlessMock({'MOCK?': '1'}) + mock_inst = UnitlessMock({"MOCK?": "1"}) + + assert mock_inst.mock_property == 1 + + +def test_unitless_property_set_cmd(): + class UnitlessMock(MockInstrument): + mock_property = unitless_property("MOCK", set_cmd="FOOBAR") + + mock_inst = UnitlessMock({"MOCK?": "1"}) + + assert mock_inst.mock_property == 1 + mock_inst.mock_property = 1 - eq_(mock_inst.mock_property, 1) + assert mock_inst.value == f"MOCK?\nFOOBAR {1:e}\n" diff --git a/instruments/tests/test_qubitekk/test_qubitekk_cc1.py b/instruments/tests/test_qubitekk/test_qubitekk_cc1.py index e5d366fbc..5ff565daf 100644 --- a/instruments/tests/test_qubitekk/test_qubitekk_cc1.py +++ b/instruments/tests/test_qubitekk/test_qubitekk_cc1.py @@ -1,15 +1,13 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing tests for the Qubitekk CC1 """ # IMPORTS #################################################################### -from __future__ import absolute_import - -from nose.tools import raises -import quantities as pq +from io import BytesIO +import pytest +from instruments.units import ureg as u import instruments as ik from instruments.tests import expected_protocol, unit_eq @@ -18,58 +16,63 @@ # TESTS ###################################################################### +def test_init_os_error(mocker): + """Initialize with acknowledgements already turned off. + + This raises an OSError in the read which must pass without an issue. + """ + stdout = BytesIO(b":ACKN OF\nFIRM?\n") + stdin = BytesIO(b"Firmware v2.010\n") + mock_read = mocker.patch.object(ik.qubitekk.CC1, "read") + mock_read.side_effect = OSError + _ = ik.qubitekk.CC1.open_test(stdin, stdout) + mock_read.assert_called_with(-1) + + def test_cc1_count(): with expected_protocol( ik.qubitekk.CC1, - [ - ":ACKN OF", - "FIRM?", - "COUN:C1?" - ], - [ - "", - "Firmware v2.010", - "20" - ], - sep="\n" + [":ACKN OF", "FIRM?", "COUN:C1?"], + ["", "Firmware v2.010", "20"], + sep="\n", ) as cc: assert cc.channel[0].count == 20.0 +def test_cc1_count_valule_error(): + with expected_protocol( + ik.qubitekk.CC1, + [":ACKN OF", "FIRM?", "COUN:C1?"], + ["", "Firmware v2.010", "bad_count", "try1" "try2" "try3" "try4" "try5"], + sep="\n", + ) as cc: + with pytest.raises(IOError) as err_info: + _ = cc.channel[0].count + err_msg = err_info.value.args[0] + assert err_msg == "Could not read the count of channel C1." + + def test_cc1_window(): with expected_protocol( ik.qubitekk.CC1, - [ - ":ACKN OF", - "FIRM?", - "WIND?", - ":WIND 7" - ], + [":ACKN OF", "FIRM?", "WIND?", ":WIND 7"], [ "", "Firmware v2.010", "2", ], - sep="\n" + sep="\n", ) as cc: - unit_eq(cc.window, pq.Quantity(2, "ns")) + unit_eq(cc.window, u.Quantity(2, "ns")) cc.window = 7 -@raises(ValueError) def test_cc1_window_error(): - with expected_protocol( + with pytest.raises(ValueError), expected_protocol( ik.qubitekk.CC1, - [ - ":ACKN OF", - "FIRM?", - ":WIND 10" - ], - [ - "", - "Firmware v2.010" - ], - sep="\n" + [":ACKN OF", "FIRM?", ":WIND 10"], + ["", "Firmware v2.010"], + sep="\n", ) as cc: cc.window = 10 @@ -77,56 +80,30 @@ def test_cc1_window_error(): def test_cc1_delay(): with expected_protocol( ik.qubitekk.CC1, - [ - ":ACKN OF", - "FIRM?", - "DELA?", - ":DELA 2" - ], - [ - "", - "Firmware v2.010", - "8", - "" - ], - sep="\n" + [":ACKN OF", "FIRM?", "DELA?", ":DELA 2"], + ["", "Firmware v2.010", "8", ""], + sep="\n", ) as cc: - unit_eq(cc.delay, pq.Quantity(8, "ns")) + unit_eq(cc.delay, u.Quantity(8, "ns")) cc.delay = 2 -@raises(ValueError) def test_cc1_delay_error1(): - with expected_protocol( + with pytest.raises(ValueError), expected_protocol( ik.qubitekk.CC1, - [ - ":ACKN OF", - "FIRM?", - ":DELA -1" - ], - [ - "", - "Firmware v2.010" - ], - sep="\n" + [":ACKN OF", "FIRM?", ":DELA -1"], + ["", "Firmware v2.010"], + sep="\n", ) as cc: cc.delay = -1 -@raises(ValueError) def test_cc1_delay_error2(): - with expected_protocol( + with pytest.raises(ValueError), expected_protocol( ik.qubitekk.CC1, - [ - ":ACKN OF", - "FIRM?", - ":DELA 1" - ], - [ - "", - "Firmware v2.010" - ], - sep="\n" + [":ACKN OF", "FIRM?", ":DELA 1"], + ["", "Firmware v2.010"], + sep="\n", ) as cc: cc.delay = 1 @@ -134,74 +111,38 @@ def test_cc1_delay_error2(): def test_cc1_dwell_old_firmware(): with expected_protocol( ik.qubitekk.CC1, - [ - ":ACKN OF", - "FIRM?", - "DWEL?", - ":DWEL 2" - ], - [ - "Unknown Command", - "Firmware v2.001", - "8000", - "" - ], - sep="\n" + [":ACKN OF", "FIRM?", "DWEL?", ":DWEL 2"], + ["Unknown Command", "Firmware v2.001", "8000", ""], + sep="\n", ) as cc: - unit_eq(cc.dwell_time, pq.Quantity(8, "s")) + unit_eq(cc.dwell_time, u.Quantity(8, "s")) cc.dwell_time = 2 def test_cc1_dwell_new_firmware(): with expected_protocol( ik.qubitekk.CC1, - [ - ":ACKN OF", - "FIRM?", - "DWEL?", - ":DWEL 2" - ], - [ - "", - "Firmware v2.010", - "8" - ], - sep="\n" + [":ACKN OF", "FIRM?", "DWEL?", ":DWEL 2"], + ["", "Firmware v2.010", "8"], + sep="\n", ) as cc: - unit_eq(cc.dwell_time, pq.Quantity(8, "s")) + unit_eq(cc.dwell_time, u.Quantity(8, "s")) cc.dwell_time = 2 -@raises(ValueError) def test_cc1_dwell_time_error(): - with expected_protocol( + with pytest.raises(ValueError), expected_protocol( ik.qubitekk.CC1, - [ - ":ACKN OF", - "FIRM?", - ":DWEL -1" - ], - [ - "", - "Firmware v2.010" - ], - sep="\n" + [":ACKN OF", "FIRM?", ":DWEL -1"], + ["", "Firmware v2.010"], + sep="\n", ) as cc: cc.dwell_time = -1 def test_cc1_firmware(): with expected_protocol( - ik.qubitekk.CC1, - [ - ":ACKN OF", - "FIRM?" - ], - [ - "", - "Firmware v2.010" - ], - sep="\n" + ik.qubitekk.CC1, [":ACKN OF", "FIRM?"], ["", "Firmware v2.010"], sep="\n" ) as cc: assert cc.firmware == (2, 10, 0) @@ -209,15 +150,9 @@ def test_cc1_firmware(): def test_cc1_firmware_2(): with expected_protocol( ik.qubitekk.CC1, - [ - ":ACKN OF", - "FIRM?" - ], - [ - "Unknown Command", - "Firmware v2" - ], - sep="\n" + [":ACKN OF", "FIRM?"], + ["Unknown Command", "Firmware v2"], + sep="\n", ) as cc: assert cc.firmware == (2, 0, 0) @@ -225,15 +160,9 @@ def test_cc1_firmware_2(): def test_cc1_firmware_3(): with expected_protocol( ik.qubitekk.CC1, - [ - ":ACKN OF", - "FIRM?" - ], - [ - "Unknown Command", - "Firmware v2.010.1" - ], - sep="\n" + [":ACKN OF", "FIRM?"], + ["Unknown Command", "Firmware v2.010.1"], + sep="\n", ) as cc: assert cc.firmware == (2, 10, 1) @@ -241,17 +170,9 @@ def test_cc1_firmware_3(): def test_cc1_firmware_repeat_query(): with expected_protocol( ik.qubitekk.CC1, - [ - ":ACKN OF", - "FIRM?", - "FIRM?" - ], - [ - "Unknown Command", - "Unknown", - "Firmware v2.010" - ], - sep="\n" + [":ACKN OF", "FIRM?", "FIRM?"], + ["Unknown Command", "Unknown", "Firmware v2.010"], + sep="\n", ) as cc: assert cc.firmware == (2, 10, 0) @@ -259,19 +180,8 @@ def test_cc1_firmware_repeat_query(): def test_cc1_gate_new_firmware(): with expected_protocol( ik.qubitekk.CC1, - [ - ":ACKN OF", - "FIRM?", - "GATE?", - ":GATE:ON", - ":GATE:OFF" - - ], - [ - "", - "Firmware v2.010", - "ON" - ], + [":ACKN OF", "FIRM?", "GATE?", ":GATE:ON", ":GATE:OFF"], + ["", "Firmware v2.010", "ON"], ) as cc: assert cc.gate is True cc.gate = True @@ -281,42 +191,21 @@ def test_cc1_gate_new_firmware(): def test_cc1_gate_old_firmware(): with expected_protocol( ik.qubitekk.CC1, - [ - ":ACKN OF", - "FIRM?", - "GATE?", - ":GATE 1", - ":GATE 0" - - ], - [ - "Unknown Command", - "Firmware v2.001", - "1", - "", - "" - ], - sep="\n" + [":ACKN OF", "FIRM?", "GATE?", ":GATE 1", ":GATE 0"], + ["Unknown Command", "Firmware v2.001", "1", "", ""], + sep="\n", ) as cc: assert cc.gate is True cc.gate = True cc.gate = False -@raises(TypeError) def test_cc1_gate_error(): - with expected_protocol( + with pytest.raises(TypeError), expected_protocol( ik.qubitekk.CC1, - [ - ":ACKN OF", - "FIRM?", - ":GATE blo" - ], - [ - "", - "Firmware v2.010" - ], - sep="\n" + [":ACKN OF", "FIRM?", ":GATE blo"], + ["", "Firmware v2.010"], + sep="\n", ) as cc: cc.gate = "blo" @@ -324,105 +213,52 @@ def test_cc1_gate_error(): def test_cc1_subtract_new_firmware(): with expected_protocol( ik.qubitekk.CC1, - [ - ":ACKN OF", - "FIRM?", - "SUBT?", - ":SUBT:ON", - ":SUBT:OFF" - - ], - [ - "", - "Firmware v2.010", - "ON", - ":SUBT:OFF" - ], - sep="\n" + [":ACKN OF", "FIRM?", "SUBT?", ":SUBT:ON", ":SUBT:OFF"], + ["", "Firmware v2.010", "ON", ":SUBT:OFF"], + sep="\n", ) as cc: assert cc.subtract is True cc.subtract = True cc.subtract = False -@raises(TypeError) def test_cc1_subtract_error(): - with expected_protocol( + with pytest.raises(TypeError), expected_protocol( ik.qubitekk.CC1, - [ - ":ACKN OF", - "FIRM?", - ":SUBT blo" - - ], - [ - "", - "Firmware v2.010" - ], - sep="\n" + [":ACKN OF", "FIRM?", ":SUBT blo"], + ["", "Firmware v2.010"], + sep="\n", ) as cc: cc.subtract = "blo" -def test_cc1_trigger_mode(): # pylint: disable=redefined-variable-type +def test_cc1_trigger_mode(): with expected_protocol( ik.qubitekk.CC1, - [ - ":ACKN OF", - "FIRM?", - "TRIG?", - ":TRIG:MODE CONT", - ":TRIG:MODE STOP" - ], - [ - "", - "Firmware v2.010", - "MODE STOP" - ], - sep="\n" + [":ACKN OF", "FIRM?", "TRIG?", ":TRIG:MODE CONT", ":TRIG:MODE STOP"], + ["", "Firmware v2.010", "MODE STOP"], + sep="\n", ) as cc: assert cc.trigger_mode is cc.TriggerMode.start_stop cc.trigger_mode = cc.TriggerMode.continuous cc.trigger_mode = cc.TriggerMode.start_stop -def test_cc1_trigger_mode_old_firmware(): # pylint: disable=redefined-variable-type +def test_cc1_trigger_mode_old_firmware(): with expected_protocol( ik.qubitekk.CC1, - [ - ":ACKN OF", - "FIRM?", - "TRIG?", - ":TRIG 0", - ":TRIG 1" - ], - [ - "Unknown Command", - "Firmware v2.001", - "1", - "", - "" - ], - sep="\n" + [":ACKN OF", "FIRM?", "TRIG?", ":TRIG 0", ":TRIG 1"], + ["Unknown Command", "Firmware v2.001", "1", "", ""], + sep="\n", ) as cc: assert cc.trigger_mode == cc.TriggerMode.start_stop cc.trigger_mode = cc.TriggerMode.continuous cc.trigger_mode = cc.TriggerMode.start_stop -@raises(ValueError) def test_cc1_trigger_mode_error(): - with expected_protocol( - ik.qubitekk.CC1, - [ - ":ACKN OF", - "FIRM?" - ], - [ - "", - "Firmware v2.010" - ], - sep="\n" + with pytest.raises(ValueError), expected_protocol( + ik.qubitekk.CC1, [":ACKN OF", "FIRM?"], ["", "Firmware v2.010"], sep="\n" ) as cc: cc.trigger_mode = "blo" @@ -430,16 +266,9 @@ def test_cc1_trigger_mode_error(): def test_cc1_clear(): with expected_protocol( ik.qubitekk.CC1, - [ - ":ACKN OF", - "FIRM?", - "CLEA" - ], - [ - "", - "Firmware v2.010" - ], - sep="\n" + [":ACKN OF", "FIRM?", "CLEA"], + ["", "Firmware v2.010"], + sep="\n", ) as cc: cc.clear_counts() @@ -447,22 +276,9 @@ def test_cc1_clear(): def test_acknowledge(): with expected_protocol( ik.qubitekk.CC1, - [ - ":ACKN OF", - "FIRM?", - ":ACKN ON", - "CLEA", - ":ACKN OF", - "CLEA" - ], - [ - "", - "Firmware v2.010", - "CLEA", - ":ACKN OF" - - ], - sep="\n" + [":ACKN OF", "FIRM?", ":ACKN ON", "CLEA", ":ACKN OF", "CLEA"], + ["", "Firmware v2.010", "CLEA", ":ACKN OF"], + sep="\n", ) as cc: assert not cc.acknowledge cc.acknowledge = True @@ -473,37 +289,21 @@ def test_acknowledge(): cc.clear_counts() -@raises(NotImplementedError) def test_acknowledge_notimplementederror(): - with expected_protocol( + with pytest.raises(NotImplementedError), expected_protocol( ik.qubitekk.CC1, - [ - ":ACKN OF", - "FIRM?" - ], - [ - "Unknown Command", - "Firmware v2.001" - - ], - sep="\n" + [":ACKN OF", "FIRM?"], + ["Unknown Command", "Firmware v2.001"], + sep="\n", ) as cc: cc.acknowledge = True -@raises(NotImplementedError) def test_acknowledge_not_implemented_error(): # pylint: disable=protected-access - with expected_protocol( + with pytest.raises(NotImplementedError), expected_protocol( ik.qubitekk.CC1, - [ - ":ACKN OF", - "FIRM?" - ], - [ - "Unknown Command", - "Firmware v2.001" - - ], - sep="\n" + [":ACKN OF", "FIRM?"], + ["Unknown Command", "Firmware v2.001"], + sep="\n", ) as cc: cc.acknowledge = True diff --git a/instruments/tests/test_qubitekk/test_qubitekk_mc1.py b/instruments/tests/test_qubitekk/test_qubitekk_mc1.py index eff278cf6..f2445dae3 100644 --- a/instruments/tests/test_qubitekk/test_qubitekk_mc1.py +++ b/instruments/tests/test_qubitekk/test_qubitekk_mc1.py @@ -1,16 +1,14 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing tests for the Qubitekk MC1 """ # IMPORTS #################################################################### -from __future__ import absolute_import -from nose.tools import raises +import pytest -import quantities as pq +from instruments.units import ureg as u import instruments as ik from instruments.tests import expected_protocol @@ -18,17 +16,30 @@ # TESTS ###################################################################### +def test_mc1_increment(): + with expected_protocol(ik.qubitekk.MC1, [], [], sep="\r") as mc: + assert mc.increment == 1 * u.ms + mc.increment = 3 * u.ms + assert mc.increment == 3 * u.ms + + +def test_mc1_lower_limit(): + with expected_protocol(ik.qubitekk.MC1, [], [], sep="\r") as mc: + assert mc.lower_limit == -300 * u.ms + mc.lower_limit = -400 * u.ms + assert mc.lower_limit == -400 * u.ms + + +def test_mc1_upper_limit(): + with expected_protocol(ik.qubitekk.MC1, [], [], sep="\r") as mc: + assert mc.upper_limit == 300 * u.ms + mc.upper_limit = 400 * u.ms + assert mc.upper_limit == 400 * u.ms + + def test_mc1_setting(): with expected_protocol( - ik.qubitekk.MC1, - [ - "OUTP?", - ":OUTP 0" - ], - [ - "1" - ], - sep="\r" + ik.qubitekk.MC1, ["OUTP?", ":OUTP 0"], ["1"], sep="\r" ) as mc: assert mc.setting == 1 mc.setting = 0 @@ -36,177 +47,84 @@ def test_mc1_setting(): def test_mc1_internal_position(): with expected_protocol( - ik.qubitekk.MC1, - [ - "POSI?", - "STEP?" - - ], - [ - "-100", - "1" - ], - sep="\r" + ik.qubitekk.MC1, ["POSI?", "STEP?"], ["-100", "1"], sep="\r" ) as mc: - assert mc.internal_position == -100*pq.ms + assert mc.internal_position == -100 * u.ms def test_mc1_metric_position(): - with expected_protocol( - ik.qubitekk.MC1, - [ - "METR?" - ], - [ - "-3.14159" - ], - sep="\r" - ) as mc: - assert mc.metric_position == -3.14159*pq.mm + with expected_protocol(ik.qubitekk.MC1, ["METR?"], ["-3.14159"], sep="\r") as mc: + assert mc.metric_position == -3.14159 * u.mm def test_mc1_direction(): - with expected_protocol( - ik.qubitekk.MC1, - [ - "DIRE?" - ], - [ - "-100" - ], - sep="\r" - ) as mc: - assert mc.direction == -100 + with expected_protocol(ik.qubitekk.MC1, ["DIRE?"], ["-100"], sep="\r") as mc: + assert mc.direction == -100 * u.ms def test_mc1_firmware(): - with expected_protocol( - ik.qubitekk.MC1, - [ - "FIRM?" - ], - [ - "1.0.1" - ], - sep="\r" - ) as mc: + with expected_protocol(ik.qubitekk.MC1, ["FIRM?"], ["1.0.1"], sep="\r") as mc: assert mc.firmware == (1, 0, 1) +def test_mc1_firmware_no_patch_info(): + with expected_protocol(ik.qubitekk.MC1, ["FIRM?"], ["1.0"], sep="\r") as mc: + assert mc.firmware == (1, 0, 0) + + def test_mc1_inertia(): - with expected_protocol( - ik.qubitekk.MC1, - [ - "INER?" - ], - [ - "20" - ], - sep="\r" - ) as mc: - assert mc.inertia == 20 + with expected_protocol(ik.qubitekk.MC1, ["INER?"], ["20"], sep="\r") as mc: + assert mc.inertia == 20 * u.ms def test_mc1_step(): - with expected_protocol( - ik.qubitekk.MC1, - [ - "STEP?" - ], - [ - "20" - ], - sep="\r" - ) as mc: - assert mc.step_size == 20*pq.ms + with expected_protocol(ik.qubitekk.MC1, ["STEP?"], ["20"], sep="\r") as mc: + assert mc.step_size == 20 * u.ms def test_mc1_motor(): - with expected_protocol( - ik.qubitekk.MC1, - [ - "MOTO?" - ], - [ - "Radio" - ], - sep="\r" - ) as mc: + with expected_protocol(ik.qubitekk.MC1, ["MOTO?"], ["Radio"], sep="\r") as mc: assert mc.controller == mc.MotorType.radio def test_mc1_move_timeout(): with expected_protocol( - ik.qubitekk.MC1, - [ - "TIME?", - "STEP?" - ], - [ - "200", - "1" - ], - sep="\r" + ik.qubitekk.MC1, ["TIME?", "STEP?"], ["200", "1"], sep="\r" ) as mc: - assert mc.move_timeout == 200*pq.ms + assert mc.move_timeout == 200 * u.ms def test_mc1_is_centering(): - with expected_protocol( - ik.qubitekk.MC1, - ["CENT?"], - ["1"], - sep="\r" - ) as mc: + with expected_protocol(ik.qubitekk.MC1, ["CENT?"], ["1"], sep="\r") as mc: assert mc.is_centering() is True def test_mc1_is_centering_false(): - with expected_protocol( - ik.qubitekk.MC1, - ["CENT?"], - ["0"], - sep="\r" - ) as mc: + with expected_protocol(ik.qubitekk.MC1, ["CENT?"], ["0"], sep="\r") as mc: assert mc.is_centering() is False def test_mc1_center(): - with expected_protocol( - ik.qubitekk.MC1, - [":CENT"], - [""], - sep="\r" - ) as mc: + with expected_protocol(ik.qubitekk.MC1, [":CENT"], [""], sep="\r") as mc: mc.center() def test_mc1_reset(): - with expected_protocol( - ik.qubitekk.MC1, - [":RESE"], - [""], - sep="\r" - ) as mc: + with expected_protocol(ik.qubitekk.MC1, [":RESE"], [""], sep="\r") as mc: mc.reset() def test_mc1_move(): with expected_protocol( - ik.qubitekk.MC1, - ["STEP?", ":MOVE 0"], - ["1"], - sep="\r" + ik.qubitekk.MC1, ["STEP?", ":MOVE 0"], ["1"], sep="\r" ) as mc: mc.move(0) -@raises(ValueError) def test_mc1_move_value_error(): - with expected_protocol( - ik.qubitekk.MC1, - [":MOVE -1000"], - [""], - sep="\r" + with pytest.raises(ValueError) as exc_info, expected_protocol( + ik.qubitekk.MC1, [], [], sep="\r" ) as mc: mc.move(-1000) + exc_msg = exc_info.value.args[0] + assert exc_msg == "Location out of range" diff --git a/instruments/tests/test_rigol/test_rigolds1000.py b/instruments/tests/test_rigol/test_rigolds1000.py new file mode 100644 index 000000000..eec06a655 --- /dev/null +++ b/instruments/tests/test_rigol/test_rigolds1000.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python +""" +Module containing tests for the Rigol DS1000 +""" + +# IMPORTS #################################################################### + +import pytest + +import instruments as ik +from instruments.optional_dep_finder import numpy +from instruments.tests import ( + expected_protocol, + iterable_eq, + make_name_test, +) + +# TESTS ###################################################################### + +# pylint: disable=protected-access + + +test_rigolds1000_name = make_name_test(ik.rigol.RigolDS1000Series) + + +# TEST CHANNEL # + + +def test_channel_initialization(): + """Ensure correct initialization of channel object.""" + with expected_protocol(ik.rigol.RigolDS1000Series, [], []) as osc: + channel = osc.channel[0] + assert channel._parent is osc + assert channel._idx == 1 + + +def test_channel_coupling(): + """Get / set channel coupling.""" + with expected_protocol( + ik.rigol.RigolDS1000Series, [":CHAN1:COUP?", ":CHAN2:COUP DC"], ["AC"] + ) as osc: + assert osc.channel[0].coupling == osc.channel[0].Coupling.ac + osc.channel[1].coupling = osc.channel[1].Coupling.dc + + +def test_channel_bw_limit(): + """Get / set instrument bw limit.""" + with expected_protocol( + ik.rigol.RigolDS1000Series, [":CHAN2:BWL?", ":CHAN1:BWL ON"], ["OFF"] + ) as osc: + assert not osc.channel[1].bw_limit + osc.channel[0].bw_limit = True + + +def test_channel_display(): + """Get / set instrument display.""" + with expected_protocol( + ik.rigol.RigolDS1000Series, [":CHAN2:DISP?", ":CHAN1:DISP ON"], ["OFF"] + ) as osc: + assert not osc.channel[1].display + osc.channel[0].display = True + + +def test_channel_invert(): + """Get / set instrument invert.""" + with expected_protocol( + ik.rigol.RigolDS1000Series, [":CHAN2:INV?", ":CHAN1:INV ON"], ["OFF"] + ) as osc: + assert not osc.channel[1].invert + osc.channel[0].invert = True + + +def test_channel_filter(): + """Get / set instrument filter.""" + with expected_protocol( + ik.rigol.RigolDS1000Series, [":CHAN2:FILT?", ":CHAN1:FILT ON"], ["OFF"] + ) as osc: + assert not osc.channel[1].filter + osc.channel[0].filter = True + + +def test_channel_vernier(): + """Get / set instrument vernier.""" + with expected_protocol( + ik.rigol.RigolDS1000Series, [":CHAN2:VERN?", ":CHAN1:VERN ON"], ["OFF"] + ) as osc: + assert not osc.channel[1].vernier + osc.channel[0].vernier = True + + +def test_channel_name(): + """Get channel name - DataSource property.""" + with expected_protocol(ik.rigol.RigolDS1000Series, [], []) as osc: + assert osc.channel[0].name == "CHAN1" + + +def test_channel_read_waveform(): + """Read waveform of channel object.""" + with expected_protocol( + ik.rigol.RigolDS1000Series, + [":WAV:DATA? CHAN2"], + [b"#210" + bytes.fromhex("00000001000200030004") + b"0"], + ) as osc: + expected = (0, 1, 2, 3, 4) + if numpy: + expected = numpy.array(expected) + iterable_eq(osc.channel[1].read_waveform(), expected) + + +# TEST MATH # + + +def test_math_name(): + """Ensure correct naming of math object.""" + with expected_protocol(ik.rigol.RigolDS1000Series, [], []) as osc: + assert osc.math.name == "MATH" + + +def test_math_read_waveform(): + """Read waveform of of math object.""" + with expected_protocol( + ik.rigol.RigolDS1000Series, + [":WAV:DATA? MATH"], + [b"#210" + bytes.fromhex("00000001000200030004") + b"0"], + ) as osc: + expected = (0, 1, 2, 3, 4) + if numpy: + expected = numpy.array(expected) + iterable_eq(osc.math.read_waveform(), expected) + + +# TEST REF DATASOURCE # + + +def test_ref_name(): + """Ensure correct naming of ref object.""" + with expected_protocol(ik.rigol.RigolDS1000Series, [], []) as osc: + assert osc.ref.name == "REF" + + +def test_ref_read_waveform_raises_error(): + """Ensure error raising when reading waveform of REF channel.""" + with expected_protocol(ik.rigol.RigolDS1000Series, [], []) as osc: + with pytest.raises(NotImplementedError): + osc.ref.read_waveform() + + +# TEST FURTHER PROPERTIES AND METHODS # + + +def test_acquire_type(): + """Get / Set acquire type.""" + with expected_protocol( + ik.rigol.RigolDS1000Series, [":ACQ:TYPE?", ":ACQ:TYPE PEAK"], ["NORM"] + ) as osc: + assert osc.acquire_type == osc.AcquisitionType.normal + osc.acquire_type = osc.AcquisitionType.peak_detect + + +def test_acquire_averages(): + """Get / Set acquire averages.""" + with expected_protocol( + ik.rigol.RigolDS1000Series, [":ACQ:AVER?", ":ACQ:AVER 128"], ["16"] + ) as osc: + assert osc.acquire_averages == 16 + osc.acquire_averages = 128 + + +def test_acquire_averages_bad_values(): + """Raise error when bad values encountered.""" + with expected_protocol(ik.rigol.RigolDS1000Series, [], []) as osc: + with pytest.raises(ValueError): + osc.acquire_averages = 0 + with pytest.raises(ValueError): + osc.acquire_averages = 1 + with pytest.raises(ValueError): + osc.acquire_averages = 42 + with pytest.raises(ValueError): + osc.acquire_averages = 257 + with pytest.raises(ValueError): + osc.acquire_averages = 512 + + +def test_force_trigger(): + """Force a trigger.""" + with expected_protocol(ik.rigol.RigolDS1000Series, [":FORC"], []) as osc: + osc.force_trigger() + + +def test_run(): + """Run the instrument.""" + with expected_protocol(ik.rigol.RigolDS1000Series, [":RUN"], []) as osc: + osc.run() + + +def test_stop(): + """Stop the instrument.""" + with expected_protocol(ik.rigol.RigolDS1000Series, [":STOP"], []) as osc: + osc.stop() + + +def test_panel_locked(): + """Get / set the panel_locked bool property.""" + with expected_protocol( + ik.rigol.RigolDS1000Series, [":KEY:LOCK?", ":KEY:LOCK DIS"], ["ENAB"] + ) as osc: + assert osc.panel_locked + osc.panel_locked = False + + +def test_release_panel(): + """Get / set the panel_locked bool property.""" + with expected_protocol(ik.rigol.RigolDS1000Series, [":KEY:FORC"], []) as osc: + osc.release_panel() diff --git a/instruments/tests/test_split_str.py b/instruments/tests/test_split_str.py index 3154e2620..367f0b1e9 100644 --- a/instruments/tests/test_split_str.py +++ b/instruments/tests/test_split_str.py @@ -1,20 +1,15 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing tests for the util_fns.split_unit_str utility function """ # IMPORTS #################################################################### -from __future__ import absolute_import -import quantities as pq +import pytest -from nose.tools import raises, eq_ - -from instruments.util_fns import ( - split_unit_str -) +from instruments.units import ureg as u +from instruments.util_fns import split_unit_str # TEST CASES ################################################################# @@ -27,8 +22,8 @@ def test_split_unit_str_magnitude_and_units(): This checks that "[val] [units]" works where val is a non-scientific number """ mag, units = split_unit_str("42 foobars") - eq_(mag, 42) - eq_(units, "foobars") + assert mag == 42 + assert units == "foobars" def test_split_unit_str_magnitude_and_default_units(): @@ -40,8 +35,8 @@ def test_split_unit_str_magnitude_and_default_units(): default_units as the units. """ mag, units = split_unit_str("42", default_units="foobars") - eq_(mag, 42) - eq_(units, "foobars") + assert mag == 42 + assert units == "foobars" def test_split_unit_str_ignore_default_units(): @@ -53,8 +48,8 @@ def test_split_unit_str_ignore_default_units(): are ignored. """ mag, units = split_unit_str("42 snafus", default_units="foobars") - eq_(mag, 42) - eq_(units, "snafus") + assert mag == 42 + assert units == "snafus" def test_split_unit_str_lookups(): @@ -65,13 +60,10 @@ def test_split_unit_str_lookups(): This checks that the unit lookup parameter is correctly called, which can be used to map between units as string and their pyquantities equivalent. """ - unit_dict = { - "FOO": "foobars", - "SNA": "snafus" - } + unit_dict = {"FOO": "foobars", "SNA": "snafus"} mag, units = split_unit_str("42 FOO", lookup=unit_dict.__getitem__) - eq_(mag, 42) - eq_(units, "foobars") + assert mag == 42 + assert units == "foobars" def test_split_unit_str_scientific_notation(): @@ -84,46 +76,46 @@ def test_split_unit_str_scientific_notation(): """ # No signs, no units mag, units = split_unit_str("123E1") - eq_(mag, 1230) - eq_(units, pq.dimensionless) + assert mag == 1230 + assert units == u.dimensionless # Negative exponential, no units mag, units = split_unit_str("123E-1") - eq_(mag, 12.3) - eq_(units, pq.dimensionless) + assert mag == 12.3 + assert units == u.dimensionless # Negative magnitude, no units mag, units = split_unit_str("-123E1") - eq_(mag, -1230) - eq_(units, pq.dimensionless) + assert mag == -1230 + assert units == u.dimensionless # No signs, with units mag, units = split_unit_str("123E1 foobars") - eq_(mag, 1230) - eq_(units, "foobars") + assert mag == 1230 + assert units == "foobars" # Signs everywhere, with units mag, units = split_unit_str("-123E-1 foobars") - eq_(mag, -12.3) - eq_(units, "foobars") + assert mag == -12.3 + assert units == "foobars" # Lower case e mag, units = split_unit_str("123e1") - eq_(mag, 1230) - eq_(units, pq.dimensionless) + assert mag == 1230 + assert units == u.dimensionless -@raises(ValueError) def test_split_unit_str_empty_string(): """ split_unit_str: Given an empty string, I expect the function to raise a ValueError. """ - _ = split_unit_str("") + with pytest.raises(ValueError): + _ = split_unit_str("") -@raises(ValueError) def test_split_unit_str_only_exponential(): """ split_unit_str: Given a string with only an exponential, I expect the function to raise a ValueError. """ - _ = split_unit_str("E3") + with pytest.raises(ValueError): + _ = split_unit_str("E3") def test_split_unit_str_magnitude_with_decimal(): @@ -133,18 +125,18 @@ def test_split_unit_str_magnitude_with_decimal(): """ # Decimal and units mag, units = split_unit_str("123.4 foobars") - eq_(mag, 123.4) - eq_(units, "foobars") + assert mag == 123.4 + assert units == "foobars" # Decimal, units, and exponential mag, units = split_unit_str("123.4E1 foobars") - eq_(mag, 1234) - eq_(units, "foobars") + assert mag == 1234 + assert units == "foobars" -@raises(ValueError) def test_split_unit_str_only_units(): """ split_unit_str: Given a bad string containing only units (ie, no numbers), I expect the function to raise a ValueError. """ - _ = split_unit_str("foobars") + with pytest.raises(ValueError): + _ = split_unit_str("foobars") diff --git a/instruments/tests/test_srs/test_srs345.py b/instruments/tests/test_srs/test_srs345.py index bacfa5882..936e46efb 100644 --- a/instruments/tests/test_srs/test_srs345.py +++ b/instruments/tests/test_srs/test_srs345.py @@ -1,18 +1,17 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Unit tests for the SRS 345 function generator """ # IMPORTS ##################################################################### -from __future__ import absolute_import - -import quantities as pq -import numpy as np import instruments as ik -from instruments.tests import expected_protocol +from instruments.tests import ( + expected_protocol, + iterable_eq, +) +from instruments.units import ureg as u # TESTS ####################################################################### @@ -20,20 +19,14 @@ def test_amplitude(): with expected_protocol( ik.srs.SRS345, - [ - "AMPL?", - "AMPL 0.1VP", - "AMPL 0.1VR" - ], + ["AMPL?", "AMPL 0.1VP", "AMPL 0.1VR"], [ "1.234VP", - ] + ], ) as inst: - np.testing.assert_array_equal( - inst.amplitude, (1.234 * pq.V, inst.VoltageMode.peak_to_peak) - ) - inst.amplitude = 0.1 * pq.V - inst.amplitude = (0.1 * pq.V, inst.VoltageMode.rms) + iterable_eq(inst.amplitude, (1.234 * u.V, inst.VoltageMode.peak_to_peak)) + inst.amplitude = 0.1 * u.V + inst.amplitude = (0.1 * u.V, inst.VoltageMode.rms) def test_frequency(): @@ -41,26 +34,23 @@ def test_frequency(): ik.srs.SRS345, [ "FREQ?", - "FREQ {:e}".format(0.1), + f"FREQ {0.1:e}", ], [ "1.234", - ] + ], ) as inst: - assert inst.frequency == 1.234 * pq.Hz - inst.frequency = 0.1 * pq.Hz + assert inst.frequency == 1.234 * u.Hz + inst.frequency = 0.1 * u.Hz def test_function(): with expected_protocol( ik.srs.SRS345, - [ - "FUNC?", - "FUNC 0" - ], + ["FUNC?", "FUNC 0"], [ "1", - ] + ], ) as inst: assert inst.function == inst.Function.square inst.function = inst.Function.sinusoid @@ -71,14 +61,14 @@ def test_offset(): ik.srs.SRS345, [ "OFFS?", - "OFFS {:e}".format(0.1), + f"OFFS {0.1:e}", ], [ "1.234", - ] + ], ) as inst: - assert inst.offset == 1.234 * pq.V - inst.offset = 0.1 * pq.V + assert inst.offset == 1.234 * u.V + inst.offset = 0.1 * u.V def test_phase(): @@ -86,11 +76,11 @@ def test_phase(): ik.srs.SRS345, [ "PHSE?", - "PHSE {:e}".format(0.1), + f"PHSE {0.1:e}", ], [ "1.234", - ] + ], ) as inst: - assert inst.phase == 1.234 * pq.degree - inst.phase = 0.1 * pq.degree + assert inst.phase == 1.234 * u.degree + inst.phase = 0.1 * u.degree diff --git a/instruments/tests/test_srs/test_srs830.py b/instruments/tests/test_srs/test_srs830.py index d94128726..7cd49beba 100644 --- a/instruments/tests/test_srs/test_srs830.py +++ b/instruments/tests/test_srs/test_srs830.py @@ -1,33 +1,82 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Unit tests for the SRS 830 lock-in amplifier """ # IMPORTS ##################################################################### -from __future__ import absolute_import +import time -import quantities as pq -import numpy as np -from nose.tools import raises +import pytest +import serial import instruments as ik -from instruments.tests import expected_protocol +from instruments.abstract_instruments.comm import ( + FileCommunicator, + GPIBCommunicator, + LoopbackCommunicator, + SerialCommunicator, +) +from instruments.optional_dep_finder import numpy +from instruments.tests import ( + expected_protocol, + iterable_eq, +) +from instruments.units import ureg as u # TESTS ####################################################################### +@pytest.fixture(autouse=True) +def time_mock(mocker): + """Mock out sleep such that test runs fast.""" + return mocker.patch.object(time, "sleep", return_value=None) + + +@pytest.mark.parametrize("mode", (1, 2)) +def test_init_mode_given(mocker, mode): + """Test initialization with a given mode.""" + comm = LoopbackCommunicator() + send_spy = mocker.spy(comm, "sendcmd") + ik.srs.SRS830(comm, outx_mode=mode) + send_spy.assert_called_with(f"OUTX {mode}") + + +def test_init_mode_gpibcomm(mocker): + """Test initialization with GPIBCommunicator""" + mock_gpib = mocker.MagicMock() + comm = GPIBCommunicator(mock_gpib, 1) + mock_send = mocker.patch.object(comm, "sendcmd") + ik.srs.SRS830(comm) + mock_send.assert_called_with("OUTX 1") + + +def test_init_mode_serial_comm(mocker): + """Test initialization with SerialCommunicator""" + comm = SerialCommunicator(serial.Serial()) + mock_send = mocker.patch.object(comm, "sendcmd") + ik.srs.SRS830(comm) + mock_send.assert_called_with("OUTX 2") + + +def test_init_mode_invalid(): + """Test initialization with invalid communicator.""" + comm = FileCommunicator(None) + with pytest.warns(UserWarning) as wrn_info: + ik.srs.SRS830(comm) + wrn_msg = wrn_info[0].message.args[0] + assert ( + wrn_msg == "OUTX command has not been set. Instrument behaviour " "is unknown." + ) + + def test_frequency_source(): with expected_protocol( ik.srs.SRS830, - [ - "FMOD?", - "FMOD 0" - ], + ["FMOD?", "FMOD 0"], [ "1", - ] + ], ) as inst: assert inst.frequency_source == inst.FreqSource.internal inst.frequency_source = inst.FreqSource.external @@ -36,58 +85,46 @@ def test_frequency_source(): def test_frequency(): with expected_protocol( ik.srs.SRS830, - [ - "FREQ?", - "FREQ {:e}".format(1000) - ], + ["FREQ?", f"FREQ {1000:e}"], [ "12.34", - ] + ], ) as inst: - assert inst.frequency == 12.34 * pq.Hz - inst.frequency = 1 * pq.kHz + assert inst.frequency == 12.34 * u.Hz + inst.frequency = 1 * u.kHz def test_phase(): with expected_protocol( ik.srs.SRS830, - [ - "PHAS?", - "PHAS {:e}".format(10) - ], + ["PHAS?", f"PHAS {10:e}"], [ "-45", - ] + ], ) as inst: - assert inst.phase == -45 * pq.degrees - inst.phase = 10 * pq.degrees + assert inst.phase == -45 * u.degrees + inst.phase = 10 * u.degrees def test_amplitude(): with expected_protocol( ik.srs.SRS830, - [ - "SLVL?", - "SLVL {:e}".format(1) - ], + ["SLVL?", f"SLVL {1:e}"], [ "0.1", - ] + ], ) as inst: - assert inst.amplitude == 0.1 * pq.V - inst.amplitude = 1 * pq.V + assert inst.amplitude == 0.1 * u.V + inst.amplitude = 1 * u.V def test_input_shield_ground(): with expected_protocol( ik.srs.SRS830, - [ - "IGND?", - "IGND 1" - ], + ["IGND?", "IGND 1"], [ "0", - ] + ], ) as inst: assert inst.input_shield_ground is False inst.input_shield_ground = True @@ -96,13 +133,10 @@ def test_input_shield_ground(): def test_coupling(): with expected_protocol( ik.srs.SRS830, - [ - "ICPL?", - "ICPL 0" - ], + ["ICPL?", "ICPL 0"], [ "1", - ] + ], ) as inst: assert inst.coupling == inst.Coupling.dc inst.coupling = inst.Coupling.ac @@ -110,44 +144,26 @@ def test_coupling(): def test_sample_rate(): # sends index of VALID_SAMPLE_RATES with expected_protocol( - ik.srs.SRS830, - [ - "SRAT?", - "SRAT?", - "SRAT {:d}".format(5), - "SRAT 14" - ], - [ - "8", - "14" - ] + ik.srs.SRS830, ["SRAT?", "SRAT?", f"SRAT {5:d}", "SRAT 14"], ["8", "14"] ) as inst: - assert inst.sample_rate == 16 * pq.Hz + assert inst.sample_rate == 16 * u.Hz assert inst.sample_rate == "trigger" inst.sample_rate = 2 - inst.sample_rate = "trigger" # pylint: disable=redefined-variable-type + inst.sample_rate = "trigger" -@raises(ValueError) def test_sample_rate_invalid(): - with expected_protocol( - ik.srs.SRS830, - [], - [] - ) as inst: + with pytest.raises(ValueError), expected_protocol(ik.srs.SRS830, [], []) as inst: inst.sample_rate = "foobar" def test_buffer_mode(): with expected_protocol( ik.srs.SRS830, - [ - "SEND?", - "SEND 1" - ], + ["SEND?", "SEND 1"], [ "0", - ] + ], ) as inst: assert inst.buffer_mode == inst.BufferMode.one_shot inst.buffer_mode = inst.BufferMode.loop @@ -156,89 +172,68 @@ def test_buffer_mode(): def test_num_data_points(): with expected_protocol( ik.srs.SRS830, - [ - "SPTS?" - ], + ["SPTS?"], [ "5", - ] + ], ) as inst: assert inst.num_data_points == 5 +def test_num_data_points_no_answer(): + """Raise IOError after no answer 10 times.""" + answer = "" + with expected_protocol(ik.srs.SRS830, ["SPTS?"] * 10, [answer] * 10) as inst: + with pytest.raises(IOError) as err_info: + _ = inst.num_data_points + err_msg = err_info.value.args[0] + assert ( + err_msg == f"Expected integer response from instrument, got " + f"{repr(answer)}" + ) + + def test_data_transfer(): with expected_protocol( ik.srs.SRS830, - [ - "FAST?", - "FAST 2" - ], + ["FAST?", "FAST 2"], [ "0", - ] + ], ) as inst: assert inst.data_transfer is False inst.data_transfer = True def test_auto_offset(): - with expected_protocol( - ik.srs.SRS830, - [ - "AOFF 1", - "AOFF 1" - ], - [] - ) as inst: + with expected_protocol(ik.srs.SRS830, ["AOFF 1", "AOFF 1"], []) as inst: inst.auto_offset(inst.Mode.x) inst.auto_offset("x") -@raises(ValueError) def test_auto_offset_invalid(): - with expected_protocol( + with pytest.raises(ValueError), expected_protocol( ik.srs.SRS830, [ "AOFF 1", ], - [] + [], ) as inst: inst.auto_offset(inst.Mode.theta) def test_auto_phase(): - with expected_protocol( - ik.srs.SRS830, - [ - "APHS" - ], - [] - ) as inst: + with expected_protocol(ik.srs.SRS830, ["APHS"], []) as inst: inst.auto_phase() def test_init(): - with expected_protocol( - ik.srs.SRS830, - [ - "REST", - "SRAT 5", - "SEND 1" - ], - [] - ) as inst: + with expected_protocol(ik.srs.SRS830, ["REST", "SRAT 5", "SEND 1"], []) as inst: inst.init(sample_rate=2, buffer_mode=inst.BufferMode.loop) def test_start_data_transfer(): - with expected_protocol( - ik.srs.SRS830, - [ - "FAST 2", - "STRD" - ], - [] - ) as inst: + with expected_protocol(ik.srs.SRS830, ["FAST 2", "STRD"], []) as inst: inst.start_data_transfer() @@ -256,306 +251,181 @@ def test_take_measurement(): "SPTS?", "TRCA?1,0,2", "SPTS?", - "TRCA?2,0,2" + "TRCA?2,0,2", ], - [ - "2", - "2", - "1.234,5.678", - "2", - "0.456,5.321" - ] + ["2", "2", "1.234,5.678", "2", "0.456,5.321"], ) as inst: resp = inst.take_measurement(sample_rate=1, num_samples=2) - np.testing.assert_array_equal(resp, [[1.234, 5.678], [0.456, 5.321]]) + expected = ((1.234, 5.678), (0.456, 5.321)) + if numpy: + expected = numpy.array(expected) + iterable_eq(resp, expected) -@raises(ValueError) -def test_take_measurement_invalid_num_samples(): +def test_take_measurement_num_dat_points_fails(): + """Simulate the failure of num_data_points. + + This is the way it is currently implemented. + """ with expected_protocol( ik.srs.SRS830, - [], - [] + ["REST", "SRAT 4", "SEND 0", "FAST 2", "STRD", "PAUS"] + + ["SPTS?"] * 11 + + ["TRCA?1,0,2", "SPTS?", "TRCA?2,0,2"], + [ + "", + ] + * 10 + + ["2", "1.234,5.678", "2", "0.456,5.321"], ) as inst: + resp = inst.take_measurement(sample_rate=1, num_samples=2) + expected = ((1.234, 5.678), (0.456, 5.321)) + if numpy: + expected = numpy.array(expected) + iterable_eq(resp, expected) + + +def test_take_measurement_invalid_num_samples(): + with pytest.raises(ValueError), expected_protocol(ik.srs.SRS830, [], []) as inst: _ = inst.take_measurement(sample_rate=1, num_samples=16384) def test_set_offset_expand(): - with expected_protocol( - ik.srs.SRS830, - [ - "OEXP 1,0,0" - ], - [] - ) as inst: + with expected_protocol(ik.srs.SRS830, ["OEXP 1,0,0"], []) as inst: inst.set_offset_expand(mode=inst.Mode.x, offset=0, expand=1) def test_set_offset_expand_mode_as_str(): - with expected_protocol( - ik.srs.SRS830, - [ - "OEXP 1,0,0" - ], - [] - ) as inst: + with expected_protocol(ik.srs.SRS830, ["OEXP 1,0,0"], []) as inst: inst.set_offset_expand(mode="x", offset=0, expand=1) -@raises(ValueError) def test_set_offset_expand_invalid_mode(): - with expected_protocol( - ik.srs.SRS830, - [], - [] - ) as inst: + with pytest.raises(ValueError), expected_protocol(ik.srs.SRS830, [], []) as inst: inst.set_offset_expand(mode=inst.Mode.theta, offset=0, expand=1) -@raises(ValueError) def test_set_offset_expand_invalid_offset(): - with expected_protocol( - ik.srs.SRS830, - [], - [] - ) as inst: + with pytest.raises(ValueError), expected_protocol(ik.srs.SRS830, [], []) as inst: inst.set_offset_expand(mode=inst.Mode.x, offset=106, expand=1) -@raises(ValueError) def test_set_offset_expand_invalid_expand(): - with expected_protocol( - ik.srs.SRS830, - [], - [] - ) as inst: + with pytest.raises(ValueError), expected_protocol(ik.srs.SRS830, [], []) as inst: inst.set_offset_expand(mode=inst.Mode.x, offset=0, expand=5) -@raises(TypeError) def test_set_offset_expand_invalid_type_offset(): - with expected_protocol( - ik.srs.SRS830, - [], - [] - ) as inst: + with pytest.raises(TypeError), expected_protocol(ik.srs.SRS830, [], []) as inst: inst.set_offset_expand(mode=inst.Mode.x, offset="derp", expand=1) -@raises(TypeError) def test_set_offset_expand_invalid_type_expand(): - with expected_protocol( - ik.srs.SRS830, - [], - [] - ) as inst: + with pytest.raises(TypeError), expected_protocol(ik.srs.SRS830, [], []) as inst: inst.set_offset_expand(mode=inst.Mode.x, offset=0, expand="derp") def test_start_scan(): - with expected_protocol( - ik.srs.SRS830, - [ - "STRD" - ], - [] - ) as inst: + with expected_protocol(ik.srs.SRS830, ["STRD"], []) as inst: inst.start_scan() def test_pause(): - with expected_protocol( - ik.srs.SRS830, - [ - "PAUS" - ], - [] - ) as inst: + with expected_protocol(ik.srs.SRS830, ["PAUS"], []) as inst: inst.pause() def test_data_snap(): - with expected_protocol( - ik.srs.SRS830, - [ - "SNAP? 1,2" - ], - [ - "1.234,9.876" - ] - ) as inst: + with expected_protocol(ik.srs.SRS830, ["SNAP? 1,2"], ["1.234,9.876"]) as inst: data = inst.data_snap(mode1=inst.Mode.x, mode2=inst.Mode.y) expected = [1.234, 9.876] - np.testing.assert_array_equal(data, expected) + iterable_eq(data, expected) def test_data_snap_mode_as_str(): - with expected_protocol( - ik.srs.SRS830, - [ - "SNAP? 1,2" - ], - [ - "1.234,9.876" - ] - ) as inst: - data = inst.data_snap(mode1='x', mode2='y') + with expected_protocol(ik.srs.SRS830, ["SNAP? 1,2"], ["1.234,9.876"]) as inst: + data = inst.data_snap(mode1="x", mode2="y") expected = [1.234, 9.876] - np.testing.assert_array_equal(data, expected) + iterable_eq(data, expected) -@raises(ValueError) def test_data_snap_invalid_snap_mode1(): - with expected_protocol( - ik.srs.SRS830, - [], - [] - ) as inst: + with pytest.raises(ValueError), expected_protocol(ik.srs.SRS830, [], []) as inst: _ = inst.data_snap(mode1=inst.Mode.xnoise, mode2=inst.Mode.y) -@raises(ValueError) def test_data_snap_invalid_snap_mode2(): - with expected_protocol( - ik.srs.SRS830, - [], - [] - ) as inst: + with pytest.raises(ValueError), expected_protocol(ik.srs.SRS830, [], []) as inst: _ = inst.data_snap(mode1=inst.Mode.x, mode2=inst.Mode.ynoise) -@raises(ValueError) def test_data_snap_identical_modes(): - with expected_protocol( - ik.srs.SRS830, - [], - [] - ) as inst: + with pytest.raises(ValueError), expected_protocol(ik.srs.SRS830, [], []) as inst: _ = inst.data_snap(mode1=inst.Mode.x, mode2=inst.Mode.x) def test_read_data_buffer(): with expected_protocol( - ik.srs.SRS830, - [ - "SPTS?", - "TRCA?1,0,2" - ], - [ - "2", - "1.234,9.876" - ] + ik.srs.SRS830, ["SPTS?", "TRCA?1,0,2"], ["2", "1.234,9.876"] ) as inst: data = inst.read_data_buffer(channel=inst.Mode.ch1) - expected = [1.234, 9.876] - np.testing.assert_array_equal(data, expected) + expected = (1.234, 9.876) + if numpy: + expected = numpy.array(expected) + iterable_eq(data, expected) def test_read_data_buffer_mode_as_str(): with expected_protocol( - ik.srs.SRS830, - [ - "SPTS?", - "TRCA?1,0,2" - ], - [ - "2", - "1.234,9.876" - ] + ik.srs.SRS830, ["SPTS?", "TRCA?1,0,2"], ["2", "1.234,9.876"] ) as inst: data = inst.read_data_buffer(channel="ch1") - expected = [1.234, 9.876] - np.testing.assert_array_equal(data, expected) + expected = (1.234, 9.876) + if numpy: + expected = numpy.array(expected) + iterable_eq(data, expected) -@raises(ValueError) def test_read_data_buffer_invalid_mode(): - with expected_protocol( - ik.srs.SRS830, - [], - [] - ) as inst: + with pytest.raises(ValueError), expected_protocol(ik.srs.SRS830, [], []) as inst: _ = inst.read_data_buffer(channel=inst.Mode.x) def test_clear_data_buffer(): - with expected_protocol( - ik.srs.SRS830, - [ - "REST" - ], - [] - ) as inst: + with expected_protocol(ik.srs.SRS830, ["REST"], []) as inst: inst.clear_data_buffer() def test_set_channel_display(): - with expected_protocol( - ik.srs.SRS830, - [ - "DDEF 1,0,0" - ], - [] - ) as inst: + with expected_protocol(ik.srs.SRS830, ["DDEF 1,0,0"], []) as inst: inst.set_channel_display( - channel=inst.Mode.ch1, - display=inst.Mode.x, - ratio=inst.Mode.none + channel=inst.Mode.ch1, display=inst.Mode.x, ratio=inst.Mode.none ) def test_set_channel_display_params_as_str(): - with expected_protocol( - ik.srs.SRS830, - [ - "DDEF 1,0,0" - ], - [] - ) as inst: - inst.set_channel_display( - channel="ch1", - display="x", - ratio="none" - ) + with expected_protocol(ik.srs.SRS830, ["DDEF 1,0,0"], []) as inst: + inst.set_channel_display(channel="ch1", display="x", ratio="none") -@raises(ValueError) def test_set_channel_display_invalid_channel(): - with expected_protocol( - ik.srs.SRS830, - [], - [] - ) as inst: + with pytest.raises(ValueError), expected_protocol(ik.srs.SRS830, [], []) as inst: inst.set_channel_display( - channel=inst.Mode.x, - display=inst.Mode.x, - ratio=inst.Mode.none + channel=inst.Mode.x, display=inst.Mode.x, ratio=inst.Mode.none ) -@raises(ValueError) def test_set_channel_display_invalid_display(): - with expected_protocol( - ik.srs.SRS830, - [], - [] - ) as inst: + with pytest.raises(ValueError), expected_protocol(ik.srs.SRS830, [], []) as inst: inst.set_channel_display( channel=inst.Mode.ch1, display=inst.Mode.y, # y is only valid for ch2, not ch1! - ratio=inst.Mode.none + ratio=inst.Mode.none, ) -@raises(ValueError) def test_set_channel_display_invalid_ratio(): - with expected_protocol( - ik.srs.SRS830, - [], - [] - ) as inst: + with pytest.raises(ValueError), expected_protocol(ik.srs.SRS830, [], []) as inst: inst.set_channel_display( - channel=inst.Mode.ch1, - display=inst.Mode.x, - ratio=inst.Mode.xnoise + channel=inst.Mode.ch1, display=inst.Mode.x, ratio=inst.Mode.xnoise ) diff --git a/instruments/tests/test_srs/test_srsctc100.py b/instruments/tests/test_srs/test_srsctc100.py new file mode 100644 index 000000000..c322eff69 --- /dev/null +++ b/instruments/tests/test_srs/test_srsctc100.py @@ -0,0 +1,503 @@ +#!/usr/bin/env python +""" +Module containing tests for the SRS CTC-100 +""" + +# IMPORTS #################################################################### + +from hypothesis import ( + given, + strategies as st, +) +import pytest + +import instruments as ik +from instruments.optional_dep_finder import numpy +from instruments.tests import ( + expected_protocol, + iterable_eq, +) +from instruments.units import ureg as u + +# TESTS ###################################################################### + + +# pylint: disable=protected-access + + +# SETUP # + + +# Create one channel name for every possible unit for parametrized testing +ch_units = list(ik.srs.SRSCTC100._UNIT_NAMES.keys()) +ch_names = [f"CH {it}" for it in range(len(ch_units))] +ch_name_unit_dict = dict(zip(ch_names, ch_units)) + + +# string that is returned when initializing channels: +ch_names_query = "getOutput.names?" +ch_names_str = ",".join(ch_names) + + +# CHANNELS # + + +@pytest.mark.parametrize("channel", ch_names) +def test_srsctc100_channel_init(channel): + """Initialize a channel.""" + with expected_protocol(ik.srs.SRSCTC100, [ch_names_query], [ch_names_str]) as inst: + with inst._error_checking_disabled(): + ch = inst.channel[channel] + assert ch._ctc is inst + assert ch._chan_name == channel + assert ch._rem_name == channel.replace(" ", "") + + +def test_srsctc100_channel_name(): + """Get / set the channel name.""" + old_name = ch_names[0] + new_name = "New channel" + with expected_protocol( + ik.srs.SRSCTC100, + [ch_names_query, f"{old_name.replace(' ', '')}.name = \"{new_name}\""], + [ch_names_str], + ) as inst: + with inst._error_checking_disabled(): + ch = inst.channel[ch_names[0]] + # assert old name is set + assert ch.name == ch_names[0] + # set a new name + ch.name = new_name + assert ch.name == new_name + assert ch._rem_name == new_name.replace(" ", "") + + +@pytest.mark.parametrize("channel", ch_names) +def test_srsctc100_channel_get(channel): + """Query a given channel. + + Ensure proper functionality for all available channels. + """ + cmd = "COMMAND" + answ = "ANSWER" + with expected_protocol( + ik.srs.SRSCTC100, + [ch_names_query, f"{channel.replace(' ', '')}.{cmd}?"], + [ch_names_str, answ], + ) as inst: + with inst._error_checking_disabled(): + assert inst.channel[channel]._get(cmd) == answ + + +@pytest.mark.parametrize("channel", ch_names) +def test_srsctc100_channel_set(channel): + """Send a command to a given channel. + + Ensure proper functionality for all available channels. + """ + cmd = "COMMAND" + newval = "NEWVAL" + with expected_protocol( + ik.srs.SRSCTC100, + [ch_names_query, f"{channel.replace(' ', '')}.{cmd} = \"{newval}\""], + [ch_names_str], + ) as inst: + with inst._error_checking_disabled(): + inst.channel[channel]._set(cmd, newval) + + +def test_srsctc100_channel_value(): + """Get value and unit from a given channel.""" + channel = ch_names[0] + unit = ik.srs.SRSCTC100._UNIT_NAMES[ch_units[0]] + value = 42 + + with expected_protocol( + ik.srs.SRSCTC100, + [ + ch_names_query, + f"{channel.replace(' ', '')}.value?", + "getOutput.units?", + ch_names_query, + ], + [ + ch_names_str, + f"{value}", + ",".join(ch_units), + ch_names_str, + ], + ) as inst: + with inst._error_checking_disabled(): + assert inst.channel[channel].value == u.Quantity(value, unit) + + +def test_srsctc100_channel_units_single(): + """Get unit for one given channel.""" + channel = ch_names[0] + unit = ik.srs.SRSCTC100._UNIT_NAMES[ch_units[0]] + with expected_protocol( + ik.srs.SRSCTC100, + [ch_names_query, "getOutput.units?", ch_names_query], + [ + ch_names_str, + ",".join(ch_units), + ch_names_str, + ], + ) as inst: + with inst._error_checking_disabled(): + ch = inst.channel[channel] + assert ch.units == unit + + +@pytest.mark.parametrize("sensor", ik.srs.SRSCTC100.SensorType) +def test_srsctc100_channel_sensor_type(sensor): + """Get type of sensor attached to specified channel.""" + channel = ch_names[0] + with expected_protocol( + ik.srs.SRSCTC100, + [ + ch_names_query, + f"{channel.replace(' ', '')}.sensor?", + ], + [ch_names_str, f"{sensor.value}"], + ) as inst: + with inst._error_checking_disabled(): + assert inst.channel[channel].sensor_type == sensor + + +@pytest.mark.parametrize("newval", (True, False)) +def test_srsctc100_channel_stats_enabled(newval): + """Get / set enabling statistics for specified channel.""" + channel = ch_names[0] + value_inst = "On" if newval else "Off" + with expected_protocol( + ik.srs.SRSCTC100, + [ + ch_names_query, + f"{channel.replace(' ', '')}.stats = \"{value_inst}\"", + f"{channel.replace(' ', '')}.stats?", + ], + [ch_names_str, f"{value_inst}"], + ) as inst: + with inst._error_checking_disabled(): + ch = inst.channel[channel] + ch.stats_enabled = newval + assert ch.stats_enabled == newval + + +@given(points=st.integers(min_value=2, max_value=6000)) +def test_srsctc100_channel_stats_points(points): + """Get / set stats points in valid range.""" + channel = ch_names[0] + with expected_protocol( + ik.srs.SRSCTC100, + [ + ch_names_query, + f"{channel.replace(' ', '')}.points = \"{points}\"", + f"{channel.replace(' ', '')}.points?", + ], + [ch_names_str, f"{points}"], + ) as inst: + with inst._error_checking_disabled(): + ch = inst.channel[channel] + ch.stats_points = points + assert ch.stats_points == points + + +def test_srsctc100_channel_average(): + """Get average measurement for given channel, unitful.""" + channel = ch_names[0] + unit = ik.srs.SRSCTC100._UNIT_NAMES[ch_units[0]] + value = 42 + with expected_protocol( + ik.srs.SRSCTC100, + [ + ch_names_query, + f"{channel.replace(' ', '')}.average?", + "getOutput.units?", + ch_names_query, + ], + [ + ch_names_str, + f"{value}", + ",".join(ch_units), + ch_names_str, + ], + ) as inst: + with inst._error_checking_disabled(): + assert inst.channel[channel].average == u.Quantity(value, unit) + + +def test_srsctc100_channel_std_dev(): + """Get standard deviation for given channel, unitful.""" + channel = ch_names[0] + unit = ik.srs.SRSCTC100._UNIT_NAMES[ch_units[0]] + value = 42 + with expected_protocol( + ik.srs.SRSCTC100, + [ + ch_names_query, + f"{channel.replace(' ', '')}.SD?", + "getOutput.units?", + ch_names_query, + ], + [ + ch_names_str, + f"{value}", + ",".join(ch_units), + ch_names_str, + ], + ) as inst: + with inst._error_checking_disabled(): + assert inst.channel[channel].std_dev == u.Quantity(value, unit) + + +@pytest.mark.parametrize("channel", ch_names) +def test_get_log_point(channel): + """Get a log point and include a unit query.""" + channel = ch_names[0] + unit = ik.srs.SRSCTC100._UNIT_NAMES[ch_name_unit_dict[channel]] + values = (13, 42) + which = "first" + values_out = ( + u.Quantity(float(values[0]), u.ms), + u.Quantity(float(values[1]), unit), + ) + with expected_protocol( + ik.srs.SRSCTC100, + [ + ch_names_query, + "getOutput.units?", + ch_names_query, + f"getLog.xy {channel}, {which}", + ], + [ch_names_str, ",".join(ch_units), ch_names_str, f"{values[0]},{values[1]}"], + ) as inst: + with inst._error_checking_disabled(): + assert inst.channel[channel].get_log_point(which=which) == values_out + + +def test_get_log_point_with_unit(): + """Get a log point and include a unit query.""" + channel = ch_names[0] + unit = ik.srs.SRSCTC100._UNIT_NAMES[ch_units[0]] + values = (13, 42) + which = "first" + values_out = ( + u.Quantity(float(values[0]), u.ms), + u.Quantity(float(values[1]), unit), + ) + with expected_protocol( + ik.srs.SRSCTC100, + [ch_names_query, f"getLog.xy {channel}, {which}"], + [ch_names_str, f"{values[0]},{values[1]}"], + ) as inst: + with inst._error_checking_disabled(): + assert ( + inst.channel[channel].get_log_point(which=which, units=unit) + == values_out + ) + + +@pytest.mark.parametrize("channel", ch_names) +def test_channel_get_log(channel): + """Get the full log of a channel. + + Leave error checking activated, because it is run at the end. + """ + # make some data + times = [0, 1, 2, 3] + values = [1.3, 2.4, 3.5, 4.6] + + # variables + units = ik.srs.SRSCTC100._UNIT_NAMES[ch_name_unit_dict[channel]] + n_points = len(values) + + # strings for error checking, sending and receiving + err_check_send = "geterror?" + err_check_reci = "0,NO ERROR" + + # stich together strings to read all the values + str_log_next_send = "\n".join( + [f"getLog.xy {channel}, next" for it in range(1, n_points)] + ) + str_log_next_reci = "\n".join( + [f"{times[it]},{values[it]}" for it in range(1, n_points)] + ) + + # make data to compare with + if numpy: + ts = u.Quantity(numpy.empty((n_points,)), u.ms) + temps = u.Quantity(numpy.empty((n_points,)), units) + else: + ts = [u.Quantity(0, u.ms)] * n_points + temps = [u.Quantity(0, units)] * n_points + for it, time in enumerate(times): + ts[it] = u.Quantity(time, u.ms) + temps[it] = u.Quantity(values[it], units) + + if not numpy: + ts = tuple(ts) + temps = tuple(temps) + + with expected_protocol( + ik.srs.SRSCTC100, + [ + ch_names_query, + err_check_send, + "getOutput.units?", + err_check_send, + ch_names_query, + err_check_send, + f"getLog.xy? {channel}", + err_check_send, + f"getLog.xy {channel}, first", # query first point + str_log_next_send, + err_check_send, + ], + [ + ch_names_str, + err_check_reci, + ",".join(ch_units), + err_check_reci, + ch_names_str, + err_check_reci, + f"{n_points}", + err_check_reci, + f"{times[0]},{values[0]}", + str_log_next_reci, + err_check_reci, + ], + ) as inst: + ch = inst.channel[channel] + ts_read, temps_read = ch.get_log() + # assert the data is correct + iterable_eq(ts, ts_read) + iterable_eq(temps, temps_read) + + +# INSTRUMENT # + + +def test_srsctc100_init(): + """Initialize the SRS CTC-100 instrument.""" + with expected_protocol(ik.srs.SRSCTC100, [], []) as inst: + assert inst._do_errcheck + + +def test_srsctc100_channel_names(): + """Get current channel names from instrument.""" + with expected_protocol(ik.srs.SRSCTC100, [ch_names_query], [ch_names_str]) as inst: + with inst._error_checking_disabled(): + assert inst._channel_names() == ch_names + + +def test_srsctc100_channel_units_all(): + """Get units for all channels.""" + with expected_protocol( + ik.srs.SRSCTC100, + ["getOutput.units?", ch_names_query], + [",".join(ch_units), ch_names_str], + ) as inst: + with inst._error_checking_disabled(): + # create a unit dictionary to compare the return to + unit_dict = { + chan_name: ik.srs.SRSCTC100._UNIT_NAMES[unit_str] + for chan_name, unit_str in zip(ch_names, ch_units) + } + assert inst.channel_units() == unit_dict + + +def test_srsctc100_errcheck(): + """Error check - no error returned.""" + with expected_protocol(ik.srs.SRSCTC100, ["geterror?"], ["0,NO ERROR"]) as inst: + assert inst.errcheck() == 0 + + +def test_srsctc100_errcheck_error_raised(): + """Error check - error raises.""" + with expected_protocol(ik.srs.SRSCTC100, ["geterror?"], ["42,THE ANSWER"]) as inst: + with pytest.raises(IOError) as exc_info: + inst.errcheck() + exc_msg = exc_info.value.args[0] + assert exc_msg == "THE ANSWER" + + +def test_srsctc100_error_checking_disabled_context(): + """Context dialogue to disable error checking.""" + with expected_protocol(ik.srs.SRSCTC100, [], []) as inst: + # by default, error checking enabled + with inst._error_checking_disabled(): + assert not inst._do_errcheck + + # default enabled again + assert inst._do_errcheck + + +@given(figures=st.integers(min_value=0, max_value=6)) +def test_srsctc100_display_figures(figures): + """Get / set significant figures of display.""" + with expected_protocol( + ik.srs.SRSCTC100, + [f"system.display.figures = {figures}", "system.display.figures?"], + [f"{figures}"], + ) as inst: + with inst._error_checking_disabled(): + inst.display_figures = figures + assert inst.display_figures == figures + + +@given(figures=st.integers().filter(lambda x: x < 0 or x > 6)) +def test_srsctc100_display_figures_value_error(figures): + """Raise ValueError when setting an invalid number of figures.""" + with expected_protocol(ik.srs.SRSCTC100, [], []) as inst: + with inst._error_checking_disabled(): + with pytest.raises(ValueError) as exc_info: + inst.display_figures = figures + exc_msg = exc_info.value.args[0] + assert ( + exc_msg == "Number of display figures must be an " + "integer from 0 to 6, inclusive." + ) + + +@pytest.mark.parametrize("newval", (True, False)) +def test_srsctc100_error_check_toggle(newval): + """Get / set error check bool.""" + with expected_protocol(ik.srs.SRSCTC100, [], []) as inst: + inst.error_check_toggle = newval + assert inst.error_check_toggle == newval + + +def test_srsctc100_error_check_toggle_type_error(): + """Raise type error when error check toggle set with non-bool.""" + newval = 42 + with expected_protocol(ik.srs.SRSCTC100, [], []) as inst: + with pytest.raises(TypeError): + inst.error_check_toggle = newval + + +def test_srsctc100_sendcmd(): + """Send a command and error check.""" + cmd = "COMMAND" + with expected_protocol( + ik.srs.SRSCTC100, [cmd, "geterror?"], ["0,NO ERROR"] + ) as inst: + inst.sendcmd("COMMAND") + + +def test_srsctc100_query(): + """Send a query and error check.""" + cmd = "COMMAND" + answ = "ANSWER" + with expected_protocol( + ik.srs.SRSCTC100, [cmd, "geterror?"], [answ, "0,NO ERROR"] + ) as inst: + assert inst.query("COMMAND") == answ + + +def test_srsctc100_clear_log(): + """Clear the log.""" + with expected_protocol(ik.srs.SRSCTC100, ["System.Log.Clear yes"], []) as inst: + with inst._error_checking_disabled(): + inst.clear_log() diff --git a/instruments/tests/test_srs/test_srsdg645.py b/instruments/tests/test_srs/test_srsdg645.py index 000142556..e87be33de 100644 --- a/instruments/tests/test_srs/test_srsdg645.py +++ b/instruments/tests/test_srs/test_srsdg645.py @@ -1,26 +1,76 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing tests for the SRS DG645 """ # IMPORTS #################################################################### -from __future__ import absolute_import - -import quantities as pq +import pytest import instruments as ik +from instruments.abstract_instruments.comm import GPIBCommunicator +from instruments.units import ureg as u from instruments.tests import expected_protocol, make_name_test, unit_eq # TESTS ###################################################################### +# pylint: disable=no-member,protected-access + test_srsdg645_name = make_name_test(ik.srs.SRSDG645) +# CHANNELS # + + +def test_srsdg645_channel_init(): + """ + _SRSDG645Channel: Ensure correct errors are raised during + initialization if not coming from a DG class. + """ + with pytest.raises(TypeError): + ik.srs.srsdg645.SRSDG645.Channel(42, 0) + + +def test_srsdg645_channel_init_channel_value(): + """ + _SRSDG645Channel: Ensure the correct channel value is used when + passing on a SRSDG645.Channels instance as the `chan` value. + """ + ddg = ik.srs.SRSDG645.open_test() # test connection + chan = ik.srs.srsdg645.SRSDG645.Channels.B # select a channel manually + assert ik.srs.srsdg645.SRSDG645.Channel(ddg, chan)._chan == 3 + + +def test_srsdg645_channel_delay(): + """ + SRSDG645: Get / set delay. + """ + with expected_protocol( + ik.srs.SRSDG645, + ["DLAY?2", "DLAY 3,2,60", "DLAY 5,4,10"], + ["0,42"], + ) as ddg: + ref, t = ddg.channel["A"].delay + assert ref == ddg.Channels.T0 + assert abs((t - u.Quantity(42, "s")).magnitude) < 1e5 + ddg.channel["B"].delay = (ddg.channel["A"], u.Quantity(1, "minute")) + ddg.channel["D"].delay = (ddg.channel["C"], 10) + + +# DG645 # + + +def test_srsdg645_init_gpib(mocker): + """Initialize SRSDG645 with GPIB Communicator.""" + mock_gpib = mocker.MagicMock() + comm = GPIBCommunicator(mock_gpib, 1) + ik.srs.SRSDG645(comm) + assert comm.strip == 2 + + def test_srsdg645_output_level(): """ - SRSDG645: Checks getting/setting unitful ouput level. + SRSDG645: Checks getting/setting unitful output level. """ with expected_protocol( ik.srs.SRSDG645, @@ -28,35 +78,147 @@ def test_srsdg645_output_level(): "LAMP? 1", "LAMP 1,4.0", ], - [ - "3.2" - ] + ["3.2"], ) as ddg: - unit_eq(ddg.output['AB'].level_amplitude, pq.Quantity(3.2, "V")) - ddg.output['AB'].level_amplitude = 4.0 + unit_eq(ddg.output["AB"].level_amplitude, u.Quantity(3.2, "V")) + ddg.output["AB"].level_amplitude = 4.0 -def test_srsdg645_output_polarity(): +def test_srsdg645_output_offset(): """ - SRSDG645: Checks getting/setting + SRSDG645: Checks getting/setting unitful output offset. """ with expected_protocol( ik.srs.SRSDG645, [ - "LPOL? 1", - "LPOL 2,0" + "LOFF? 1", + "LOFF 1,2.0", ], - [ - "1" - ] + ["1.2"], ) as ddg: - assert ddg.output['AB'].polarity == ddg.LevelPolarity.positive - ddg.output['CD'].polarity = ddg.LevelPolarity.negative + unit_eq(ddg.output["AB"].level_offset, u.Quantity(1.2, "V")) + ddg.output["AB"].level_offset = 2.0 + + +def test_srsdg645_output_polarity(): + """ + SRSDG645: Checks getting/setting + """ + with expected_protocol(ik.srs.SRSDG645, ["LPOL? 1", "LPOL 2,0"], ["1"]) as ddg: + assert ddg.output["AB"].polarity == ddg.LevelPolarity.positive + ddg.output["CD"].polarity = ddg.LevelPolarity.negative + + +def test_srsdg645_output_polarity_raise_type_error(): + """ + SRSDG645: Polarity setter with wrong input - raise type error. + """ + with expected_protocol(ik.srs.SRSDG645, [], []) as ddg: + with pytest.raises(TypeError): + ddg.output["AB"].polarity = 1 + + +def test_srsdg645_display(): + """ + SRSDG645: Set / get display mode. + """ + with expected_protocol(ik.srs.SRSDG645, ["DISP?", "DISP 0,0"], ["12,3"]) as ddg: + assert ddg.display == (ddg.DisplayMode.channel_levels, ddg.Channels.B) + ddg.display = (ddg.DisplayMode.trigger_rate, ddg.Channels.T0) + + +def test_srsdg645_enable_adv_triggering(): + """ + SRSDG645: Set / get if advanced triggering is enabled. + """ + with expected_protocol(ik.srs.SRSDG645, ["ADVT?", "ADVT 1"], ["0"]) as ddg: + assert not ddg.enable_adv_triggering + ddg.enable_adv_triggering = True + + +def test_srsdg645_trigger_rate(): + """ + SRSDG645: Set / get trigger rate. + """ + with expected_protocol( + ik.srs.SRSDG645, ["TRAT?", "TRAT 10000", "TRAT 1000"], ["+1000.000000"] + ) as ddg: + assert ddg.trigger_rate == u.Quantity(1000, u.Hz) + ddg.trigger_rate = 10000 + ddg.trigger_rate = u.Quantity(1000, u.Hz) # unitful send def test_srsdg645_trigger_source(): - with expected_protocol(ik.srs.SRSDG645, "DLAY?2\nDLAY 3,2,60.0\n", "0,42\n") as ddg: - ref, t = ddg.channel['A'].delay - assert ref == ddg.Channels.T0 - assert abs((t - pq.Quantity(42, 's')).magnitude) < 1e5 - ddg.channel['B'].delay = (ddg.channel['A'], pq.Quantity(1, "minute")) + """ + SRSDG645: Set / get trigger source. + """ + with expected_protocol(ik.srs.SRSDG645, ["TSRC?", "TSRC 1"], ["0"]) as ddg: + assert ddg.trigger_source == ddg.TriggerSource.internal + ddg.trigger_source = ddg.TriggerSource.external_rising + + +def test_srsdg645_holdoff(): + """ + SRSDG645: Set / get hold off. + """ + with expected_protocol( + ik.srs.SRSDG645, ["HOLD?", "HOLD 0", "HOLD 0.01"], ["+0.001001000000"] + ) as ddg: + assert u.Quantity(1001, u.us) == ddg.holdoff + ddg.holdoff = 0 + ddg.holdoff = u.Quantity(10, u.ms) # unitful hold off + + +def test_srsdg645_enable_burst_mode(): + """ + SRSDG645: Checks getting/setting of enabling burst mode. + """ + with expected_protocol(ik.srs.SRSDG645, ["BURM?", "BURM 1"], ["0"]) as ddg: + assert ddg.enable_burst_mode is False + ddg.enable_burst_mode = True + + +def test_srsdg645_enable_burst_t0_first(): + """ + SRSDG645: Checks getting/setting of enabling T0 output on first + in burst mode. + """ + with expected_protocol(ik.srs.SRSDG645, ["BURT?", "BURT 1"], ["0"]) as ddg: + assert ddg.enable_burst_t0_first is False + ddg.enable_burst_t0_first = True + + +def test_srsdg645_burst_count(): + """ + SRSDG645: Checks getting/setting of enabling T0 output on first + in burst mode. + """ + with expected_protocol(ik.srs.SRSDG645, ["BURC?", "BURC 42"], ["10"]) as ddg: + assert ddg.burst_count == 10 + ddg.burst_count = 42 + + +def test_srsdg645_burst_period(): + """ + SRSDG645: Checks getting/setting of enabling T0 output on first + in burst mode. + """ + with expected_protocol( + ik.srs.SRSDG645, ["BURP?", "BURP 13", "BURP 0.1"], ["100E-9"] + ) as ddg: + unit_eq(ddg.burst_period, u.Quantity(100, "ns").to(u.sec)) + ddg.burst_period = u.Quantity(13, "s") + ddg.burst_period = 0.1 + + +def test_srsdg645_burst_delay(): + """ + SRSDG645: Checks getting/setting of enabling T0 output on first + in burst mode. + """ + with expected_protocol( + ik.srs.SRSDG645, ["BURD?", "BURD 42", "BURD 0.1"], ["0"] + ) as ddg: + unit_eq(ddg.burst_delay, u.Quantity(0, "s")) + ddg.burst_delay = u.Quantity(42, "s") + ddg.burst_delay = 0.1 diff --git a/instruments/tests/test_tektronix/test_tekawg2000.py b/instruments/tests/test_tektronix/test_tekawg2000.py new file mode 100644 index 000000000..434a88b39 --- /dev/null +++ b/instruments/tests/test_tektronix/test_tekawg2000.py @@ -0,0 +1,289 @@ +#!/usr/bin/env python +""" +Unit tests for the Tektronix AGG2000 arbitrary wave generators. +""" + +# IMPORTS ##################################################################### + +from hypothesis import ( + given, + strategies as st, +) +import pytest + +import instruments as ik +from instruments.optional_dep_finder import numpy +from instruments.tests import expected_protocol, make_name_test +from instruments.units import ureg as u + + +# TESTS ####################################################################### + +# pylint: disable=protected-access + + +test_tekawg2000_name = make_name_test(ik.tektronix.TekAWG2000) + + +# CHANNEL # + + +channels_to_try = range(2) +channels_to_try_id = [f"CH{it}" for it in channels_to_try] + + +@pytest.mark.parametrize("channel", channels_to_try, ids=channels_to_try_id) +def test_channel_init(channel): + """Channel initialization.""" + with expected_protocol(ik.tektronix.TekAWG2000, [], []) as inst: + assert inst.channel[channel]._tek is inst + assert inst.channel[channel]._name == f"CH{channel + 1}" + assert inst.channel[channel]._old_dsrc is None + + +@pytest.mark.parametrize("channel", channels_to_try, ids=channels_to_try_id) +def test_channel_name(channel): + """Get the name of the channel.""" + with expected_protocol(ik.tektronix.TekAWG2000, [], []) as inst: + assert inst.channel[channel].name == f"CH{channel + 1}" + + +@pytest.mark.parametrize("channel", channels_to_try, ids=channels_to_try_id) +@given( + val_read=st.floats(min_value=0.02, max_value=2), + val_unitless=st.floats(min_value=0.02, max_value=2), + val_millivolt=st.floats(min_value=0.02, max_value=2000), +) +def test_channel_amplitude(channel, val_read, val_unitless, val_millivolt): + """Get / set amplitude.""" + val_read = u.Quantity(val_read, u.V) + val_unitful = u.Quantity(val_millivolt, u.mV) + with expected_protocol( + ik.tektronix.TekAWG2000, + [ + f"FG:CH{channel+1}:AMPL?", + f"FG:CH{channel+1}:AMPL {val_unitless}", + f"FG:CH{channel+1}:AMPL {val_unitful.to(u.V).magnitude}", + ], + [f"{val_read.magnitude}"], + ) as inst: + assert inst.channel[channel].amplitude == val_read + inst.channel[channel].amplitude = val_unitless + inst.channel[channel].amplitude = val_unitful + + +@pytest.mark.parametrize("channel", channels_to_try, ids=channels_to_try_id) +@given( + val_read=st.floats(min_value=0.02, max_value=2), + val_unitless=st.floats(min_value=0.02, max_value=2), + val_millivolt=st.floats(min_value=0.02, max_value=2000), +) +def test_channel_offset(channel, val_read, val_unitless, val_millivolt): + """Get / set offset.""" + val_read = u.Quantity(val_read, u.V) + val_unitful = u.Quantity(val_millivolt, u.mV) + with expected_protocol( + ik.tektronix.TekAWG2000, + [ + f"FG:CH{channel+1}:OFFS?", + f"FG:CH{channel+1}:OFFS {val_unitless}", + f"FG:CH{channel+1}:OFFS {val_unitful.to(u.V).magnitude}", + ], + [f"{val_read.magnitude}"], + ) as inst: + assert inst.channel[channel].offset == val_read + inst.channel[channel].offset = val_unitless + inst.channel[channel].offset = val_unitful + + +@pytest.mark.parametrize("channel", channels_to_try, ids=channels_to_try_id) +@given( + val_read=st.floats(min_value=1, max_value=200000), + val_unitless=st.floats(min_value=1, max_value=200000), + val_kilohertz=st.floats(min_value=1, max_value=200), +) +def test_channel_frequency(channel, val_read, val_unitless, val_kilohertz): + """Get / set offset.""" + val_read = u.Quantity(val_read, u.Hz) + val_unitful = u.Quantity(val_kilohertz, u.kHz) + with expected_protocol( + ik.tektronix.TekAWG2000, + [ + f"FG:FREQ?", + f"FG:FREQ {val_unitless}HZ", + f"FG:FREQ {val_unitful.to(u.Hz).magnitude}HZ", + ], + [f"{val_read.magnitude}"], + ) as inst: + assert inst.channel[channel].frequency == val_read + inst.channel[channel].frequency = val_unitless + inst.channel[channel].frequency = val_unitful + + +@pytest.mark.parametrize("channel", channels_to_try, ids=channels_to_try_id) +@given(polarity=st.sampled_from(ik.tektronix.TekAWG2000.Polarity)) +def test_channel_polarity(channel, polarity): + """Get / set polarity.""" + with expected_protocol( + ik.tektronix.TekAWG2000, + [f"FG:CH{channel+1}:POL?", f"FG:CH{channel+1}:POL {polarity.value}"], + [f"{polarity.value}"], + ) as inst: + assert inst.channel[channel].polarity == polarity + inst.channel[channel].polarity = polarity + + +@pytest.mark.parametrize("channel", channels_to_try, ids=channels_to_try_id) +def test_channel_polarity_type_mismatch(channel): + """Raise a TypeError if a wrong type is selected as the polarity.""" + wrong_type = 42 + with expected_protocol(ik.tektronix.TekAWG2000, [], []) as inst: + with pytest.raises(TypeError) as exc_info: + inst.channel[channel].polarity = wrong_type + exc_msg = exc_info.value.args[0] + assert ( + exc_msg == f"Polarity settings must be a `TekAWG2000.Polarity` " + f"value, got {type(wrong_type)} instead." + ) + + +@pytest.mark.parametrize("channel", channels_to_try, ids=channels_to_try_id) +@given(shape=st.sampled_from(ik.tektronix.TekAWG2000.Shape)) +def test_channel_shape(channel, shape): + """Get / set shape.""" + with expected_protocol( + ik.tektronix.TekAWG2000, + [f"FG:CH{channel+1}:SHAP?", f"FG:CH{channel+1}:SHAP {shape.value}"], + [f"{shape.value}, 0"], # pulse duty cycle + ) as inst: + assert inst.channel[channel].shape == shape + inst.channel[channel].shape = shape + + +@pytest.mark.parametrize("channel", channels_to_try, ids=channels_to_try_id) +def test_channel_shape_type_mismatch(channel): + """Raise a TypeError if a wrong type is selected as the shape.""" + wrong_type = 42 + with expected_protocol(ik.tektronix.TekAWG2000, [], []) as inst: + with pytest.raises(TypeError) as exc_info: + inst.channel[channel].shape = wrong_type + exc_msg = exc_info.value.args[0] + assert ( + exc_msg == f"Shape settings must be a `TekAWG2000.Shape` " + f"value, got {type(wrong_type)} instead." + ) + + +# INSTRUMENT # + + +def test_waveform_name(): + """Get / set the waveform name.""" + file_name = "test_file" + with expected_protocol( + ik.tektronix.TekAWG2000, + ["DATA:DEST?", f'DATA:DEST "{file_name}"'], + [f"{file_name}"], + ) as inst: + assert inst.waveform_name == file_name + inst.waveform_name = file_name + + +def test_waveform_name_type_mismatch(): + """Raise a TypeError when something else than a string is given.""" + wrong_type = 42 + with expected_protocol(ik.tektronix.TekAWG2000, [], []) as inst: + with pytest.raises(TypeError) as exc_info: + inst.waveform_name = wrong_type + exc_msg = exc_info.value.args[0] + assert exc_msg == "Waveform name must be specified as a string." + + +@pytest.mark.skipif(numpy is None, reason="Numpy required for this test") +@given( + yzero=st.floats(min_value=-5, max_value=5), + ymult=st.floats(min_value=0.00001), + xincr=st.floats(min_value=5e-8, max_value=1e-1), + waveform=st.lists(st.floats(min_value=0, max_value=1), min_size=1), +) +def test_upload_waveform(yzero, ymult, xincr, waveform): + """Upload a waveform from the PC to the instrument.""" + # prep waveform + waveform = numpy.array(waveform) + waveform_send = waveform * (2 ** 12 - 1) + waveform_send = waveform_send.astype("h' for decoding + yoffs = 0 # already tested with hypothesis + # values packing + ptcnt = len(values) + values_packed = b"".join(struct.pack(">h", value) for value in values) + values_len = str(len(values_packed)).encode() + values_len_of_len = str(len(values_len)).encode() + with expected_protocol( + ik.tektronix.TekDPO4104, + [ + "DAT:SOU?", # old data source + f"DAT:SOU CH{channel+1}", + "DAT:STOP?", + f"DAT:STOP {10**7}", + "DAT:ENC RIB", # set encoding + "DATA:WIDTH?", # query data width + "CURVE?", # get the data (in bin format) + "WFMP:YOF?", # query yoffs + "WFMP:YMU?", # query ymult + "WFMP:YZE?", # query yzero + "WFMP:XZE?", # query x zero + "WFMP:XIN?", # retrieve x increments + "WFMP:NR_P?", # retrieve number of points + f"DAT:STOP {old_dat_stop}", + f"DAT:SOU CH{old_dat_source + 1}", # set back old data source + ], + [ + f"CH{old_dat_source+1}", + f"{old_dat_stop}", + f"{data_width}", + b"#" + values_len_of_len + values_len + values_packed, + f"{yoffs}", + f"{ymult}", + f"{yzero}", + f"{xzero}", + f"{xincr}", + f"{ptcnt}", + ], + ) as inst: + x_read, y_read = inst.channel[channel].read_waveform() + if numpy: + x_calc = numpy.arange(ptcnt) * xincr + xzero + y_calc = ((numpy.array(values) - yoffs) * ymult) + yzero + else: + x_calc = tuple(float(val) * xincr + xzero for val in range(ptcnt)) + y_calc = tuple(((float(val) - yoffs) * ymult) + yzero for val in values) + iterable_eq(x_read, x_calc) + iterable_eq(y_read, y_calc) + + +@given( + values=st.lists(st.integers(min_value=-32768, max_value=32767), min_size=1), + ymult=st.integers(min_value=1, max_value=65536), + yzero=st.floats(min_value=-100, max_value=100), + xzero=st.floats(min_value=-10, max_value=10), + xincr=st.floats(min_value=1e-9, max_value=1), +) +def test_data_source_read_waveform_ascii(values, ymult, yzero, xzero, xincr): + """Read waveform back in ASCII format.""" + old_dat_source = 3 + old_dat_stop = 100 # "previous" setting + # new values + channel = 0 + yoffs = 0 # already tested with hypothesis + # transform values to strings + values_str = ",".join([str(value) for value in values]) + # calculated values + ptcnt = len(values) + with expected_protocol( + ik.tektronix.TekDPO4104, + [ + "DAT:SOU?", # old data source + f"DAT:SOU CH{channel + 1}", + "DAT:STOP?", + f"DAT:STOP {10**7}", + "DAT:ENC ASCI", # set encoding + "CURVE?", # get the data (in bin format) + "WFMP:YOF?", + "WFMP:YMU?", # query y-offset + "WFMP:YZE?", # query y zero + "WFMP:XZE?", # query x zero + "WFMP:XIN?", # retrieve x increments + "WFMP:NR_P?", # retrieve number of points + f"DAT:STOP {old_dat_stop}", + f"DAT:SOU CH{old_dat_source + 1}", # set back old data source + ], + [ + f"CH{old_dat_source + 1}", + f"{old_dat_stop}", + f"{values_str}", + f"{yoffs}", + f"{ymult}", + f"{yzero}", + f"{xzero}", + f"{xincr}", + f"{ptcnt}", + ], + ) as inst: + # get the values from the instrument + x_read, y_read = inst.channel[channel].read_waveform(bin_format=False) + + # manually calculate the values + if numpy: + raw = numpy.array(values_str.split(","), dtype=numpy.float) + x_calc = numpy.arange(ptcnt) * xincr + xzero + y_calc = (raw - yoffs) * ymult + yzero + else: + x_calc = tuple(float(val) * xincr + xzero for val in range(ptcnt)) + y_calc = tuple(((float(val) - yoffs) * ymult) + yzero for val in values) + + # assert arrays are equal + iterable_eq(x_read, x_calc) + iterable_eq(y_read, y_calc) + + +@given(offset=st.floats(min_value=-100, max_value=100)) +def test_data_source_y_offset_get(offset): + """Get y-offset from parent property.""" + old_dat_source = 2 + channel = 0 + with expected_protocol( + ik.tektronix.TekDPO4104, + [ + "DAT:SOU?", # old data source + f"DAT:SOU CH{channel + 1}", + "WFMP:YOF?", + f"DAT:SOU CH{old_dat_source + 1}", # set back old data source + ], + [f"CH{old_dat_source + 1}", f"{offset}"], + ) as inst: + assert inst.channel[channel].y_offset == offset + + +@given(offset=st.floats(min_value=-100, max_value=100)) +def test_data_source_y_offset_set(offset): + """Set y-offset from parent property.""" + old_dat_source = 2 + channel = 0 + with expected_protocol( + ik.tektronix.TekDPO4104, + [ + "DAT:SOU?", # old data source + f"DAT:SOU CH{channel + 1}", + f"WFMP:YOF {offset}", + f"DAT:SOU CH{old_dat_source + 1}", # set back old data source + ], + [ + f"CH{old_dat_source + 1}", + ], + ) as inst: + inst.channel[channel].y_offset = offset + + +def test_data_source_y_offset_set_old_data_source_same(): + """Set y-offset from parent property, old data source same. + + Test one case of setting a data source where the old data source + and the new one is the same. Use y_offset for this test. + """ + offset = 0 + old_dat_source = 0 + channel = 0 + with expected_protocol( + ik.tektronix.TekDPO4104, + [ + "DAT:SOU?", # old data source + f"WFMP:YOF {offset}", + ], + [ + f"CH{old_dat_source + 1}", + ], + ) as inst: + inst.channel[channel].y_offset = offset diff --git a/instruments/tests/test_tektronix/test_tekdpo70000.py b/instruments/tests/test_tektronix/test_tekdpo70000.py new file mode 100644 index 000000000..9a515d8f9 --- /dev/null +++ b/instruments/tests/test_tektronix/test_tekdpo70000.py @@ -0,0 +1,1541 @@ +#!/usr/bin/env python +""" +Tests for the Tektronix DPO 70000 oscilloscope. +""" + +# IMPORTS ##################################################################### + +import struct +import time + +from hypothesis import ( + given, + strategies as st, +) +import pytest + +import instruments as ik +from instruments.optional_dep_finder import numpy +from instruments.tests import ( + expected_protocol, + iterable_eq, + make_name_test, + unit_eq, +) +from instruments.units import ureg as u + + +# TESTS ####################################################################### + +# pylint: disable=too-many-lines,protected-access + + +test_tekdpo70000_name = make_name_test(ik.tektronix.TekDPO70000) + + +# STATIC METHOD # + + +@pytest.mark.parametrize("binary_format", ik.tektronix.TekDPO70000.BinaryFormat) +@pytest.mark.parametrize("byte_order", ik.tektronix.TekDPO70000.ByteOrder) +@pytest.mark.parametrize("n_bytes", (1, 2, 4, 8)) +def test_dtype(binary_format, byte_order, n_bytes): + """Return the formatted format name, depending on settings.""" + binary_format_dict = { + ik.tektronix.TekDPO70000.BinaryFormat.int: "i", + ik.tektronix.TekDPO70000.BinaryFormat.uint: "u", + ik.tektronix.TekDPO70000.BinaryFormat.float: "f", + } + byte_order_dict = { + ik.tektronix.TekDPO70000.ByteOrder.big_endian: ">", + ik.tektronix.TekDPO70000.ByteOrder.little_endian: "<", + } + value_expected = ( + f"{byte_order_dict[byte_order]}" + f"{n_bytes}" + f"{binary_format_dict[binary_format]}" + ) + with expected_protocol(ik.tektronix.TekDPO70000, [], []) as inst: + assert inst._dtype(binary_format, byte_order, n_bytes) == value_expected + + +# DATA SOURCE - TESTED WITH CHANNELS # + + +def test_data_source_name(): + """Query the name of a data source.""" + channel = 0 + with expected_protocol(ik.tektronix.TekDPO70000, [], []) as inst: + assert inst.channel[channel].name == f"CH{channel+1}" + + +@pytest.mark.parametrize("channel", [it for it in range(4)]) +@given( + values=st.lists( + st.integers(min_value=-2147483648, max_value=2147483647), min_size=1 + ) +) +def test_data_source_read_waveform(channel, values): + """Read waveform from data source, binary format only!""" + # select one set to test for: + binary_format = ik.tektronix.TekDPO70000.BinaryFormat.int # go w/ values + byte_order = ik.tektronix.TekDPO70000.ByteOrder.big_endian + n_bytes = 4 + # get the dtype + dtype_set = ik.tektronix.TekDPO70000._dtype(binary_format, byte_order, n_bytes=None) + + # pack the values + values_packed = b"".join(struct.pack(dtype_set, value) for value in values) + values_len = str(len(values_packed)).encode() + values_len_of_len = str(len(values_len)).encode() + # scale the values + scale = 1.0 + position = 0.0 + offset = 0.0 + scaled_values = [ + scale + * ((ik.tektronix.TekDPO70000.VERT_DIVS / 2) * float(v) / (2 ** 15) - position) + + offset + for v in values + ] + if numpy: + values = numpy.array(values) + scaled_values = ( + scale + * ( + (ik.tektronix.TekDPO70000.VERT_DIVS / 2) + * values.astype(float) + / (2 ** 15) + - position + ) + + offset + ) + + # run through the instrument + with expected_protocol( + ik.tektronix.TekDPO70000, + [ + "DAT:SOU?", # query data source + "DAT:ENC FAS", # fastest encoding + "WFMO:BYT_N?", # get n_bytes + "WFMO:BN_F?", # outgoing binary format + "WFMO:BYT_O?", # outgoing byte order + "CURV?", # query data + f"CH{channel + 1}:SCALE?", # scale raw data + f"CH{channel + 1}:POS?", + f"CH{channel + 1}:OFFS?", + ], + [ + f"CH{channel+1}", + f"{n_bytes}", + f"{binary_format.value}", + f"{byte_order.value}", + b"#" + values_len_of_len + values_len + values_packed, + f"{scale}", + f"{position}", + f"{offset}", + ], + ) as inst: + # query waveform + actual_waveform = inst.channel[channel].read_waveform() + expected_waveform = tuple(v * u.V for v in scaled_values) + if numpy: + expected_waveform = scaled_values * u.V + iterable_eq(actual_waveform, expected_waveform) + + +def test_data_source_read_waveform_with_old_data_source(): + """Read waveform from data, old data source present!""" + channel = 0 # multiple channels already tested above + # select one set to test for: + binary_format = ik.tektronix.TekDPO70000.BinaryFormat.int # go w/ values + byte_order = ik.tektronix.TekDPO70000.ByteOrder.big_endian + n_bytes = 4 + # get the dtype + dtype_set = ik.tektronix.TekDPO70000._dtype(binary_format, byte_order, n_bytes=None) + + # pack the values + values = range(10) + if numpy: + values = numpy.arange(10) + values_packed = b"".join(struct.pack(dtype_set, value) for value in values) + values_len = str(len(values_packed)).encode() + values_len_of_len = str(len(values_len)).encode() + # scale the values + scale = 1.0 + position = 0.0 + offset = 0.0 + scaled_values = [ + scale + * ((ik.tektronix.TekDPO70000.VERT_DIVS / 2) * float(v) / (2 ** 15) - position) + + offset + for v in values + ] + if numpy: + scaled_values = ( + scale + * ( + (ik.tektronix.TekDPO70000.VERT_DIVS / 2) + * values.astype(float) + / (2 ** 15) + - position + ) + + offset + ) + + # old data source to set manually - ensure it is set back later + old_dsrc = "MATH1" + + # run through the instrument + with expected_protocol( + ik.tektronix.TekDPO70000, + [ + "DAT:SOU?", # query data source + f"DAT:SOU CH{channel + 1}", # set current data source + "DAT:ENC FAS", # fastest encoding + "WFMO:BYT_N?", # get n_bytes + "WFMO:BN_F?", # outgoing binary format + "WFMO:BYT_O?", # outgoing byte order + "CURV?", # query data + f"CH{channel + 1}:SCALE?", # scale raw data + f"CH{channel + 1}:POS?", + f"CH{channel + 1}:OFFS?", + f"DAT:SOU {old_dsrc}", + ], + [ + old_dsrc, + f"{n_bytes}", + f"{binary_format.value}", + f"{byte_order.value}", + b"#" + values_len_of_len + values_len + values_packed, + f"{scale}", + f"{position}", + f"{offset}", + ], + ) as inst: + # query waveform + actual_waveform = inst.channel[channel].read_waveform() + expected_waveform = tuple(v * u.V for v in scaled_values) + if numpy: + expected_waveform = scaled_values * u.V + iterable_eq(actual_waveform, expected_waveform) + + +# MATH # + + +@pytest.mark.parametrize("math", [it for it in range(4)]) +def test_math_init(math): + """Initialize a math channel.""" + with expected_protocol(ik.tektronix.TekDPO70000, [], []) as inst: + assert inst.math[math]._parent is inst + assert inst.math[math]._idx == math + 1 + + +@pytest.mark.parametrize("math", [it for it in range(4)]) +def test_math_sendcmd(math): + """Send a command from a math channel.""" + cmd = "TEST" + with expected_protocol( + ik.tektronix.TekDPO70000, [f"MATH{math+1}:{cmd}"], [] + ) as inst: + inst.math[math].sendcmd(cmd) + + +@pytest.mark.parametrize("math", [it for it in range(4)]) +def test_math_query(math): + """Query from a math channel.""" + cmd = "TEST" + answ = "ANSWER" + with expected_protocol( + ik.tektronix.TekDPO70000, [f"MATH{math+1}:{cmd}"], [answ] + ) as inst: + assert inst.math[math].query(cmd) == answ + + +@given( + value=st.text( + alphabet=st.characters(blacklist_characters="\n", blacklist_categories=("Cs",)) + ) +) +def test_math_define(value): + """Get / set a string operation from the Math mode.""" + math = 0 + cmd = "DEF" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f'MATH{math+1}:{cmd} "{value}"', f"MATH{math+1}:{cmd}?"], + [f'"{value}"'], + ) as inst: + inst.math[math].define = value + assert inst.math[math].define == value + + +@pytest.mark.parametrize("value", ik.tektronix.TekDPO70000.Math.FilterMode) +def test_math_filter_mode(value): + """Get / set filter mode.""" + math = 0 + cmd = "FILT:MOD" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"MATH{math + 1}:{cmd} {value.value}", f"MATH{math + 1}:{cmd}?"], + [f"{value.value}"], + ) as inst: + inst.math[math].filter_mode = value + assert inst.math[math].filter_mode == value + + +@given(value=st.floats(min_value=0)) +def test_math_filter_risetime(value): + """Get / set filter risetime.""" + math = 0 + cmd = "FILT:RIS" + value_unitful = u.Quantity(value, u.s) + with expected_protocol( + ik.tektronix.TekDPO70000, + [ + f"MATH{math + 1}:{cmd} {value:e}", + f"MATH{math + 1}:{cmd} {value:e}", + f"MATH{math + 1}:{cmd}?", + ], + [f"{value}"], + ) as inst: + inst.math[math].filter_risetime = value + inst.math[math].filter_risetime = value_unitful + unit_eq(inst.math[math].filter_risetime, value_unitful) + + +@given( + value=st.text( + alphabet=st.characters(blacklist_characters="\n", blacklist_categories=("Cs",)) + ) +) +def test_math_label(value): + """Get / set a label for the math channel.""" + math = 0 + cmd = "LAB:NAM" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f'MATH{math+1}:{cmd} "{value}"', f"MATH{math+1}:{cmd}?"], + [f'"{value}"'], + ) as inst: + inst.math[math].label = value + assert inst.math[math].label == value + + +@given( + value=st.floats( + min_value=-ik.tektronix.TekDPO70000.HOR_DIVS, + max_value=ik.tektronix.TekDPO70000.HOR_DIVS, + ) +) +def test_math_label_xpos(value): + """Get / set x position for label.""" + math = 0 + cmd = "LAB:XPOS" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"MATH{math + 1}:{cmd} {value:e}", f"MATH{math + 1}:{cmd}?"], + [f"{value}"], + ) as inst: + inst.math[math].label_xpos = value + assert inst.math[math].label_xpos == value + + +@given( + value=st.floats( + min_value=-ik.tektronix.TekDPO70000.VERT_DIVS, + max_value=ik.tektronix.TekDPO70000.VERT_DIVS, + ) +) +def test_math_label_ypos(value): + """Get / set y position for label.""" + math = 0 + cmd = "LAB:YPOS" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"MATH{math + 1}:{cmd} {value:e}", f"MATH{math + 1}:{cmd}?"], + [f"{value}"], + ) as inst: + inst.math[math].label_ypos = value + assert inst.math[math].label_ypos == value + + +@given(value=st.integers(min_value=0)) +def test_math_num_avg(value): + """Get / set number of averages.""" + math = 0 + cmd = "NUMAV" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"MATH{math + 1}:{cmd} {value:e}", f"MATH{math + 1}:{cmd}?"], + [f"{value}"], + ) as inst: + inst.math[math].num_avg = value + assert inst.math[math].num_avg == pytest.approx(value) + + +@given(value=st.floats(min_value=0)) +def test_math_spectral_center(value): + """Get / set spectral center.""" + math = 0 + cmd = "SPEC:CENTER" + value_unitful = u.Quantity(value, u.Hz) + with expected_protocol( + ik.tektronix.TekDPO70000, + [ + f"MATH{math + 1}:{cmd} {value:e}", + f"MATH{math + 1}:{cmd} {value:e}", + f"MATH{math + 1}:{cmd}?", + ], + [f"{value}"], + ) as inst: + inst.math[math].spectral_center = value + inst.math[math].spectral_center = value_unitful + unit_eq(inst.math[math].spectral_center, value_unitful) + + +@given(value=st.floats(allow_nan=False)) +def test_math_spectral_gatepos(value): + """Get / set gate position.""" + math = 0 + cmd = "SPEC:GATEPOS" + value_unitful = u.Quantity(value, u.s) + with expected_protocol( + ik.tektronix.TekDPO70000, + [ + f"MATH{math + 1}:{cmd} {value:e}", + f"MATH{math + 1}:{cmd} {value:e}", + f"MATH{math + 1}:{cmd}?", + ], + [f"{value}"], + ) as inst: + inst.math[math].spectral_gatepos = value + inst.math[math].spectral_gatepos = value_unitful + unit_eq(inst.math[math].spectral_gatepos, value_unitful) + + +@given(value=st.floats(allow_nan=False)) +def test_math_spectral_gatewidth(value): + """Get / set gate width.""" + math = 0 + cmd = "SPEC:GATEWIDTH" + value_unitful = u.Quantity(value, u.s) + with expected_protocol( + ik.tektronix.TekDPO70000, + [ + f"MATH{math + 1}:{cmd} {value:e}", + f"MATH{math + 1}:{cmd} {value:e}", + f"MATH{math + 1}:{cmd}?", + ], + [f"{value}"], + ) as inst: + inst.math[math].spectral_gatewidth = value + inst.math[math].spectral_gatewidth = value_unitful + unit_eq(inst.math[math].spectral_gatewidth, value_unitful) + + +@pytest.mark.parametrize("value", [True, False]) +def test_math_spectral_lock(value): + """Get / set spectral lock.""" + math = 0 + cmd = "SPEC:LOCK" + value_io = "ON" if value else "OFF" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"MATH{math + 1}:{cmd} {value_io}", f"MATH{math + 1}:{cmd}?"], + [f"{value_io}"], + ) as inst: + inst.math[math].spectral_lock = value + assert inst.math[math].spectral_lock == value + + +@pytest.mark.parametrize("value", ik.tektronix.TekDPO70000.Math.Mag) +def test_math_spectral_mag(value): + """Get / set spectral magnitude scaling.""" + math = 0 + cmd = "SPEC:MAG" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"MATH{math + 1}:{cmd} {value.value}", f"MATH{math + 1}:{cmd}?"], + [f"{value.value}"], + ) as inst: + inst.math[math].spectral_mag = value + assert inst.math[math].spectral_mag == value + + +@pytest.mark.parametrize("value", ik.tektronix.TekDPO70000.Math.Phase) +def test_math_spectral_phase(value): + """Get / set spectral phase unit.""" + math = 0 + cmd = "SPEC:PHASE" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"MATH{math + 1}:{cmd} {value.value}", f"MATH{math + 1}:{cmd}?"], + [f"{value.value}"], + ) as inst: + inst.math[math].spectral_phase = value + assert inst.math[math].spectral_phase == value + + +@given(value=st.floats(allow_nan=False)) +def test_math_spectral_reflevel(value): + """Get / set spectral reference level.""" + math = 0 + cmd = "SPEC:REFL" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"MATH{math + 1}:{cmd} {value:e}", f"MATH{math + 1}:{cmd}?"], + [f"{value}"], + ) as inst: + inst.math[math].spectral_reflevel = value + assert inst.math[math].spectral_reflevel == value + + +@given(value=st.floats(allow_nan=False)) +def test_math_spectral_reflevel_offset(value): + """Get / set spectral reference level offset.""" + math = 0 + cmd = "SPEC:REFLEVELO" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"MATH{math + 1}:{cmd} {value:e}", f"MATH{math + 1}:{cmd}?"], + [f"{value}"], + ) as inst: + inst.math[math].spectral_reflevel_offset = value + assert inst.math[math].spectral_reflevel_offset == value + + +@given(value=st.floats(min_value=0)) +def test_math_spectral_resolution_bandwidth(value): + """Get / set spectral resolution bandwidth.""" + math = 0 + cmd = "SPEC:RESB" + value_unitful = u.Quantity(value, u.Hz) + with expected_protocol( + ik.tektronix.TekDPO70000, + [ + f"MATH{math + 1}:{cmd} {value:e}", + f"MATH{math + 1}:{cmd} {value:e}", + f"MATH{math + 1}:{cmd}?", + ], + [f"{value}"], + ) as inst: + inst.math[math].spectral_resolution_bandwidth = value + inst.math[math].spectral_resolution_bandwidth = value_unitful + unit_eq(inst.math[math].spectral_resolution_bandwidth, value_unitful) + + +@given(value=st.floats(min_value=0)) +def test_math_spectral_span(value): + """Get / set frequency span of output data vector.""" + math = 0 + cmd = "SPEC:SPAN" + value_unitful = u.Quantity(value, u.Hz) + with expected_protocol( + ik.tektronix.TekDPO70000, + [ + f"MATH{math + 1}:{cmd} {value:e}", + f"MATH{math + 1}:{cmd} {value:e}", + f"MATH{math + 1}:{cmd}?", + ], + [f"{value}"], + ) as inst: + inst.math[math].spectral_span = value + inst.math[math].spectral_span = value_unitful + unit_eq(inst.math[math].spectral_span, value_unitful) + + +@given(value=st.floats(allow_nan=False)) +def test_math_spectral_suppress(value): + """Get / set spectral suppression value.""" + math = 0 + cmd = "SPEC:SUPP" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"MATH{math + 1}:{cmd} {value:e}", f"MATH{math + 1}:{cmd}?"], + [f"{value}"], + ) as inst: + inst.math[math].spectral_suppress = value + assert inst.math[math].spectral_suppress == value + + +@pytest.mark.parametrize("value", [True, False]) +def test_math_spectral_unwrap(value): + """Get / set phase wrapping.""" + math = 0 + cmd = "SPEC:UNWR" + value_io = "ON" if value else "OFF" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"MATH{math + 1}:{cmd} {value_io}", f"MATH{math + 1}:{cmd}?"], + [f"{value_io}"], + ) as inst: + inst.math[math].spectral_unwrap = value + assert inst.math[math].spectral_unwrap == value + + +@pytest.mark.parametrize("value", ik.tektronix.TekDPO70000.Math.SpectralWindow) +def test_math_spectral_window(value): + """Get / set spectral window.""" + math = 0 + cmd = "SPEC:WIN" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"MATH{math + 1}:{cmd} {value.value}", f"MATH{math + 1}:{cmd}?"], + [f"{value.value}"], + ) as inst: + inst.math[math].spectral_window = value + assert inst.math[math].spectral_window == value + + +@given(value=st.floats(min_value=0)) +def test_math_threshold(value): + """Get / set threshold of math channel.""" + math = 0 + cmd = "THRESH" + value_unitful = u.Quantity(value, u.V) + with expected_protocol( + ik.tektronix.TekDPO70000, + [ + f"MATH{math + 1}:{cmd} {value:e}", + f"MATH{math + 1}:{cmd} {value:e}", + f"MATH{math + 1}:{cmd}?", + ], + [f"{value}"], + ) as inst: + inst.math[math].threshhold = value + inst.math[math].threshhold = value_unitful + unit_eq(inst.math[math].threshhold, value_unitful) + + +@given( + value=st.text( + alphabet=st.characters(blacklist_characters="\n", blacklist_categories=("Cs",)) + ) +) +def test_math_units(value): + """Get / set a label for the units.""" + math = 0 + cmd = "UNITS" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f'MATH{math+1}:{cmd} "{value}"', f"MATH{math+1}:{cmd}?"], + [f'"{value}"'], + ) as inst: + inst.math[math].unit_string = value + assert inst.math[math].unit_string == value + + +@pytest.mark.parametrize("value", [True, False]) +def test_math_autoscale(value): + """Get / set if autoscale is enabled.""" + math = 0 + cmd = "VERT:AUTOSC" + value_io = "ON" if value else "OFF" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"MATH{math + 1}:{cmd} {value_io}", f"MATH{math + 1}:{cmd}?"], + [f"{value_io}"], + ) as inst: + inst.math[math].autoscale = value + assert inst.math[math].autoscale == value + + +@given( + value=st.floats( + min_value=-ik.tektronix.TekDPO70000.VERT_DIVS / 2, + max_value=ik.tektronix.TekDPO70000.VERT_DIVS / 2, + ) +) +def test_math_position(value): + """Get / set spectral vertical position from center.""" + math = 0 + cmd = "VERT:POS" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"MATH{math + 1}:{cmd} {value:e}", f"MATH{math + 1}:{cmd}?"], + [f"{value}"], + ) as inst: + inst.math[math].position = value + assert inst.math[math].position == value + + +@given(value=st.floats(min_value=0)) +def test_math_scale(value): + """Get / set scale in volts per division.""" + math = 0 + cmd = "VERT:SCALE" + value_unitful = u.Quantity(value, u.V) + with expected_protocol( + ik.tektronix.TekDPO70000, + [ + f"MATH{math + 1}:{cmd} {value:e}", + f"MATH{math + 1}:{cmd} {value:e}", + f"MATH{math + 1}:{cmd}?", + ], + [f"{value}"], + ) as inst: + inst.math[math].scale = value + inst.math[math].scale = value_unitful + unit_eq(inst.math[math].scale, value_unitful) + + +@given( + values=st.lists(st.floats(min_value=-2147483648, max_value=2147483647), min_size=1) +) +def test_math_scale_raw_data(values): + """Return scaled raw data according to current settings.""" + math = 0 + scale = 1.0 * u.V + position = -2.3 + expected_value = tuple( + scale + * ((ik.tektronix.TekDPO70000.VERT_DIVS / 2) * float(v) / (2 ** 15) - position) + for v in values + ) + if numpy: + values = numpy.array(values) + expected_value = scale * ( + (ik.tektronix.TekDPO70000.VERT_DIVS / 2) * values.astype(float) / (2 ** 15) + - position + ) + + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"MATH{math + 1}:VERT:SCALE?", f"MATH{math + 1}:VERT:POS?"], + [f"{scale}", f"{position}"], + ) as inst: + iterable_eq(inst.math[math]._scale_raw_data(values), expected_value) + + +# CHANNEL # + + +@pytest.mark.parametrize("channel", [it for it in range(4)]) +def test_channel_init(channel): + """Initialize a channel.""" + with expected_protocol(ik.tektronix.TekDPO70000, [], []) as inst: + assert inst.channel[channel]._parent is inst + assert inst.channel[channel]._idx == channel + 1 + + +@pytest.mark.parametrize("channel", [it for it in range(4)]) +def test_channel_sendcmd(channel): + """Send a command from a channel.""" + cmd = "TEST" + with expected_protocol( + ik.tektronix.TekDPO70000, [f"CH{channel+1}:{cmd}"], [] + ) as inst: + inst.channel[channel].sendcmd(cmd) + + +@pytest.mark.parametrize("channel", [it for it in range(4)]) +def test_channel_query(channel): + """Send a query from a channel.""" + cmd = "TEST" + answ = "ANSWER" + with expected_protocol( + ik.tektronix.TekDPO70000, [f"CH{channel+1}:{cmd}"], [answ] + ) as inst: + assert inst.channel[channel].query(cmd) == answ + + +@pytest.mark.parametrize("value", ik.tektronix.TekDPO70000.Channel.Coupling) +def test_channel_coupling(value): + """Get / set channel coupling.""" + channel = 0 + cmd = "COUP" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"CH{channel+1}:{cmd} {value.value}", f"CH{channel+1}:{cmd}?"], + [f"{value.value}"], + ) as inst: + inst.channel[channel].coupling = value + assert inst.channel[channel].coupling == value + + +@given(value=st.floats(min_value=0, max_value=30e9)) +def test_channel_bandwidth(value): + """Get / set bandwidth of a channel. + + Test unitful and unitless setting. + """ + channel = 0 + cmd = "BAN" + value_unitful = u.Quantity(value, u.Hz) + with expected_protocol( + ik.tektronix.TekDPO70000, + [ + f"CH{channel + 1}:{cmd} {value:e}", + f"CH{channel + 1}:{cmd} {value:e}", + f"CH{channel + 1}:{cmd}?", + ], + [f"{value}"], + ) as inst: + inst.channel[channel].bandwidth = value + inst.channel[channel].bandwidth = value_unitful + unit_eq(inst.channel[channel].bandwidth, value_unitful) + + +@given(value=st.floats(min_value=-25e-9, max_value=25e-9)) +def test_channel_deskew(value): + """Get / set deskew time. + + Test unitful and unitless setting. + """ + channel = 0 + cmd = "DESK" + value_unitful = u.Quantity(value, u.s) + with expected_protocol( + ik.tektronix.TekDPO70000, + [ + f"CH{channel + 1}:{cmd} {value:e}", + f"CH{channel + 1}:{cmd} {value:e}", + f"CH{channel + 1}:{cmd}?", + ], + [f"{value}"], + ) as inst: + inst.channel[channel].deskew = value + inst.channel[channel].deskew = value_unitful + unit_eq(inst.channel[channel].deskew, value_unitful) + + +@pytest.mark.parametrize("value", [50, 1000000]) +def test_channel_termination(value): + """Get / set termination of channel. + + Valid values are 50 Ohm or 1 MOhm. Try setting unitful and + unitless. + """ + channel = 0 + cmd = "TERM" + value_unitful = u.Quantity(value, u.ohm) + with expected_protocol( + ik.tektronix.TekDPO70000, + [ + f"CH{channel + 1}:{cmd} {value:e}", + f"CH{channel + 1}:{cmd} {value:e}", + f"CH{channel + 1}:{cmd}?", + ], + [f"{value}"], + ) as inst: + inst.channel[channel].termination = value + inst.channel[channel].termination = value_unitful + unit_eq(inst.channel[channel].termination, value_unitful) + + +@given( + value=st.text( + alphabet=st.characters(blacklist_characters="\n", blacklist_categories=("Cs",)) + ) +) +def test_channel_label(value): + """Get / set human readable label for channel.""" + channel = 0 + cmd = "LAB:NAM" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f'CH{channel+1}:{cmd} "{value}"', f"CH{channel+1}:{cmd}?"], + [f'"{value}"'], + ) as inst: + inst.channel[channel].label = value + assert inst.channel[channel].label == value + + +@given( + value=st.floats( + min_value=-ik.tektronix.TekDPO70000.HOR_DIVS, + max_value=ik.tektronix.TekDPO70000.HOR_DIVS, + ) +) +def test_channel_label_xpos(value): + """Get / set x position for label.""" + channel = 0 + cmd = "LAB:XPOS" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"CH{channel+1}:{cmd} {value:e}", f"CH{channel+1}:{cmd}?"], + [f"{value}"], + ) as inst: + inst.channel[channel].label_xpos = value + assert inst.channel[channel].label_xpos == value + + +@given( + value=st.floats( + min_value=-ik.tektronix.TekDPO70000.VERT_DIVS, + max_value=ik.tektronix.TekDPO70000.VERT_DIVS, + ) +) +def test_channel_label_ypos(value): + """Get / set y position for label.""" + channel = 0 + cmd = "LAB:YPOS" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"CH{channel+1}:{cmd} {value:e}", f"CH{channel+1}:{cmd}?"], + [f"{value}"], + ) as inst: + inst.channel[channel].label_ypos = value + assert inst.channel[channel].label_ypos == value + + +@given(value=st.floats(allow_nan=False)) +def test_channel_offset(value): + """Get / set offset, unitful in V and unitless.""" + channel = 0 + cmd = "OFFS" + value_unitful = u.Quantity(value, u.V) + with expected_protocol( + ik.tektronix.TekDPO70000, + [ + f"CH{channel + 1}:{cmd} {value:e}", + f"CH{channel + 1}:{cmd} {value:e}", + f"CH{channel + 1}:{cmd}?", + ], + [f"{value}"], + ) as inst: + inst.channel[channel].offset = value + inst.channel[channel].offset = value_unitful + unit_eq(inst.channel[channel].offset, value_unitful) + + +@given( + value=st.floats( + min_value=-ik.tektronix.TekDPO70000.VERT_DIVS, + max_value=ik.tektronix.TekDPO70000.VERT_DIVS, + ) +) +def test_channel_position(value): + """Get / set vertical position.""" + channel = 0 + cmd = "POS" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"CH{channel+1}:{cmd} {value:e}", f"CH{channel+1}:{cmd}?"], + [f"{value}"], + ) as inst: + inst.channel[channel].position = value + assert inst.channel[channel].position == value + + +@given(value=st.floats(min_value=0)) +def test_channel_scale(value): + """Get / set scale.""" + channel = 0 + cmd = "SCALE" + value_unitful = u.Quantity(value, u.V) + with expected_protocol( + ik.tektronix.TekDPO70000, + [ + f"CH{channel + 1}:{cmd} {value:e}", + f"CH{channel + 1}:{cmd} {value:e}", + f"CH{channel + 1}:{cmd}?", + ], + [f"{value}"], + ) as inst: + inst.channel[channel].scale = value + inst.channel[channel].scale = value_unitful + unit_eq(inst.channel[channel].scale, value_unitful) + + +@given( + values=st.lists(st.floats(min_value=-2147483648, max_value=2147483647), min_size=1) +) +def test_channel_scale_raw_data(values): + """Return scaled raw data according to current settings.""" + channel = 0 + scale = u.Quantity(1.0, u.V) + position = -1.0 + offset = u.Quantity(0.0, u.V) + expected_value = tuple( + scale + * ((ik.tektronix.TekDPO70000.VERT_DIVS / 2) * float(v) / (2 ** 15) - position) + for v in values + ) + if numpy: + values = numpy.array(values) + expected_value = ( + scale + * ( + (ik.tektronix.TekDPO70000.VERT_DIVS / 2) + * values.astype(float) + / (2 ** 15) + - position + ) + + offset + ) + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"CH{channel + 1}:SCALE?", f"CH{channel + 1}:POS?", f"CH{channel + 1}:OFFS?"], + [f"{scale}", f"{position}", f"{offset}"], + ) as inst: + actual_data = inst.channel[channel]._scale_raw_data(values) + iterable_eq(actual_data, expected_value) + + +# INSTRUMENT # + + +@pytest.mark.parametrize("value", ["AUTO", "OFF"]) +def test_acquire_enhanced_enob(value): + """Get / set enhanced effective number of bits.""" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"ACQ:ENHANCEDE {value}", "ACQ:ENHANCEDE?"], + [f"{value}"], + ) as inst: + inst.acquire_enhanced_enob = value + assert inst.acquire_enhanced_enob == value + + +@pytest.mark.parametrize("value", [True, False]) +def test_acquire_enhanced_state(value): + """Get / set state of enhanced effective number of bits.""" + value_io = "1" if value else "0" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"ACQ:ENHANCEDE:STATE {value_io}", "ACQ:ENHANCEDE:STATE?"], + [f"{value_io}"], + ) as inst: + inst.acquire_enhanced_state = value + assert inst.acquire_enhanced_state == value + + +@pytest.mark.parametrize("value", ["AUTO", "ON", "OFF"]) +def test_acquire_interp_8bit(value): + """Get / set interpolation method of instrument.""" + with expected_protocol( + ik.tektronix.TekDPO70000, [f"ACQ:INTERPE {value}", "ACQ:INTERPE?"], [f"{value}"] + ) as inst: + inst.acquire_interp_8bit = value + assert inst.acquire_interp_8bit == value + + +@pytest.mark.parametrize("value", [True, False]) +def test_acquire_magnivu(value): + """Get / set MagniVu feature.""" + value_io = "ON" if value else "OFF" + with expected_protocol( + ik.tektronix.TekDPO70000, [f"ACQ:MAG {value_io}", "ACQ:MAG?"], [f"{value_io}"] + ) as inst: + inst.acquire_magnivu = value + assert inst.acquire_magnivu == value + + +@pytest.mark.parametrize("value", ik.tektronix.TekDPO70000.AcquisitionMode) +def test_acquire_mode(value): + """Get / set acquisition mode.""" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"ACQ:MOD {value.value}", "ACQ:MOD?"], + [f"{value.value}"], + ) as inst: + inst.acquire_mode = value + assert inst.acquire_mode == value + + +@pytest.mark.parametrize("value", ik.tektronix.TekDPO70000.AcquisitionMode) +def test_acquire_mode_actual(value): + """Get actually used acquisition mode (query only).""" + with expected_protocol( + ik.tektronix.TekDPO70000, ["ACQ:MOD:ACT?"], [f"{value.value}"] + ) as inst: + assert inst.acquire_mode_actual == value + + +@given(value=st.integers(min_value=0, max_value=2 ** 30 - 1)) +def test_acquire_num_acquisitions(value): + """Get number of waveform acquisitions since start (query only).""" + with expected_protocol( + ik.tektronix.TekDPO70000, ["ACQ:NUMAC?"], [f"{value}"] + ) as inst: + assert inst.acquire_num_acquisitions == value + + +@given(value=st.integers(min_value=0)) +def test_acquire_num_avgs(value): + """Get / set number of waveform acquisitions to average.""" + with expected_protocol( + ik.tektronix.TekDPO70000, [f"ACQ:NUMAV {value}", "ACQ:NUMAV?"], [f"{value}"] + ) as inst: + inst.acquire_num_avgs = value + assert inst.acquire_num_avgs == value + + +@given(value=st.integers(min_value=0)) +def test_acquire_num_envelop(value): + """Get / set number of waveform acquisitions to envelope.""" + with expected_protocol( + ik.tektronix.TekDPO70000, [f"ACQ:NUME {value}", "ACQ:NUME?"], [f"{value}"] + ) as inst: + inst.acquire_num_envelop = value + assert inst.acquire_num_envelop == value + + +@given(value=st.integers(min_value=0)) +def test_acquire_num_frames(value): + """Get / set number of frames in FastFrame Single Sequence mode. + + Query only. + """ + with expected_protocol( + ik.tektronix.TekDPO70000, ["ACQ:NUMFRAMESACQ?"], [f"{value}"] + ) as inst: + assert inst.acquire_num_frames == value + + +@given(value=st.integers(min_value=5000, max_value=2147400000)) +def test_acquire_num_samples(value): + """Get / set number of acquired samples to make up waveform database.""" + with expected_protocol( + ik.tektronix.TekDPO70000, [f"ACQ:NUMSAM {value}", "ACQ:NUMSAM?"], [f"{value}"] + ) as inst: + inst.acquire_num_samples = value + assert inst.acquire_num_samples == value + + +@pytest.mark.parametrize("value", ik.tektronix.TekDPO70000.SamplingMode) +def test_acquire_sampling_mode(value): + """Get / set sampling mode.""" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"ACQ:SAMP {value.value}", "ACQ:SAMP?"], + [f"{value.value}"], + ) as inst: + inst.acquire_sampling_mode = value + assert inst.acquire_sampling_mode == value + + +@pytest.mark.parametrize("value", ik.tektronix.TekDPO70000.AcquisitionState) +def test_acquire_state(value): + """Get / set acquisition state.""" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"ACQ:STATE {value.value}", "ACQ:STATE?"], + [f"{value.value}"], + ) as inst: + inst.acquire_state = value + assert inst.acquire_state == value + + +@pytest.mark.parametrize("value", ik.tektronix.TekDPO70000.StopAfter) +def test_acquire_stop_after(value): + """Get / set whether acquisition is continuous.""" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"ACQ:STOPA {value.value}", "ACQ:STOPA?"], + [f"{value.value}"], + ) as inst: + inst.acquire_stop_after = value + assert inst.acquire_stop_after == value + + +@given(value=st.integers(min_value=0)) +def test_data_framestart(value): + """Get / set start frame for waveform transfer.""" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"DAT:FRAMESTAR {value}", "DAT:FRAMESTAR?"], + [f"{value}"], + ) as inst: + inst.data_framestart = value + assert inst.data_framestart == value + + +@given(value=st.integers(min_value=0)) +def test_data_framestop(value): + """Get / set stop frame for waveform transfer.""" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"DAT:FRAMESTOP {value}", "DAT:FRAMESTOP?"], + [f"{value}"], + ) as inst: + inst.data_framestop = value + assert inst.data_framestop == value + + +@given(value=st.integers(min_value=0)) +def test_data_start(value): + """Get / set start data point for waveform transfer.""" + with expected_protocol( + ik.tektronix.TekDPO70000, [f"DAT:STAR {value}", "DAT:STAR?"], [f"{value}"] + ) as inst: + inst.data_start = value + assert inst.data_start == value + + +@given(value=st.integers(min_value=0)) +def test_data_stop(value): + """Get / set stop data point for waveform transfer.""" + with expected_protocol( + ik.tektronix.TekDPO70000, [f"DAT:STOP {value}", "DAT:STOP?"], [f"{value}"] + ) as inst: + inst.data_stop = value + assert inst.data_stop == value + + +@pytest.mark.parametrize("value", [True, False]) +def test_data_sync_sources(value): + """Get / set if data sync sources are on or off.""" + value_io = "ON" if value else "OFF" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"DAT:SYNCSOU {value_io}", "DAT:SYNCSOU?"], + [f"{value_io}"], + ) as inst: + inst.data_sync_sources = value + assert inst.data_sync_sources == value + + +valid_channel_range = [it for it in range(4)] + + +@pytest.mark.parametrize("no", valid_channel_range) +def test_data_source_channel(no): + """Get / set channel as data source.""" + channel_name = f"CH{no + 1}" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"DAT:SOU {channel_name}", f"DAT:SOU?"], + [channel_name], + ) as inst: + channel = inst.channel[no] + inst.data_source = channel + assert inst.data_source == channel + + +valid_math_range = [it for it in range(4)] + + +@pytest.mark.parametrize("no", valid_math_range) +def test_data_source_math(no, mocker): + """Get / set math as data source.""" + math_name = f"MATH{no + 1}" + + # patch call to time.sleep with mock + mock_time = mocker.patch.object(time, "sleep", return_value=None) + + with expected_protocol( + ik.tektronix.TekDPO70000, [f"DAT:SOU {math_name}", f"DAT:SOU?"], [math_name] + ) as inst: + math = inst.math[no] + inst.data_source = math + assert inst.data_source == math + + # assert that time.sleep has been called + mock_time.assert_called() + + +def test_data_source_ref_not_implemented_error(): + """Get / set a reference channel raises a NotImplemented error.""" + ref_name = "REF1" # example, range not important + with expected_protocol(ik.tektronix.TekDPO70000, [f"DAT:SOU?"], [ref_name]) as inst: + # getter + with pytest.raises(NotImplementedError): + print(inst.data_source) + # setter + with pytest.raises(NotImplementedError): + inst.data_source = inst.ref[0] + + +def test_data_source_not_implemented_error(): + """Get a data source that is currently not implemented.""" + ds_name = "HHG29" # example, range not important + with expected_protocol(ik.tektronix.TekDPO70000, [f"DAT:SOU?"], [ds_name]) as inst: + with pytest.raises(NotImplementedError): + print(inst.data_source) + + +def test_data_source_invalid_type(): + """Raise TypeError when a wrong type is set for data source.""" + invalid_data_source = 42 + with expected_protocol(ik.tektronix.TekDPO70000, [], []) as inst: + with pytest.raises(TypeError) as exc_info: + inst.data_source = invalid_data_source + exc_msg = exc_info.value.args[0] + assert exc_msg == f"{type(invalid_data_source)} is not a valid data " f"source." + + +@given(value=st.floats(min_value=0, max_value=1000)) +def test_horiz_acq_duration(value): + """Get horizontal acquisition duration (query only).""" + value_unitful = u.Quantity(value, u.s) + with expected_protocol( + ik.tektronix.TekDPO70000, ["HOR:ACQDURATION?"], [f"{value}"] + ) as inst: + unit_eq(inst.horiz_acq_duration, value_unitful) + + +@given(value=st.integers(min_value=0)) +def test_horiz_acq_length(value): + """Get horizontal acquisition length (query only).""" + with expected_protocol( + ik.tektronix.TekDPO70000, ["HOR:ACQLENGTH?"], [f"{value}"] + ) as inst: + assert inst.horiz_acq_length == value + + +@pytest.mark.parametrize("value", [True, False]) +def test_horiz_delay_mode(value): + """Get / set state of horizontal delay mode.""" + value_io = "1" if value else "0" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"HOR:DEL:MOD {value_io}", "HOR:DEL:MOD?"], + [f"{value_io}"], + ) as inst: + inst.horiz_delay_mode = value + assert inst.horiz_delay_mode == value + + +@given(value=st.floats(min_value=0, max_value=100)) +def test_horiz_delay_pos(value): + """Get / set horizontal time base if delay mode is on. + + Test setting unitful and without units.""" + value_unitful = u.Quantity(value, u.percent) + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"HOR:DEL:POS {value:e}", f"HOR:DEL:POS {value:e}", "HOR:DEL:POS?"], + [f"{value}"], + ) as inst: + inst.horiz_delay_pos = value + inst.horiz_delay_pos = value_unitful + unit_eq(inst.horiz_delay_pos, value_unitful) + + +@given(value=st.floats(min_value=0)) +def test_horiz_delay_time(value): + """Get / set horizontal delay time.""" + value_unitful = u.Quantity(value, u.s) + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"HOR:DEL:TIM {value:e}", f"HOR:DEL:TIM {value:e}", "HOR:DEL:TIM?"], + [f"{value}"], + ) as inst: + inst.horiz_delay_time = value + inst.horiz_delay_time = value_unitful + unit_eq(inst.horiz_delay_time, value_unitful) + + +@given(value=st.floats(min_value=0)) +def test_horiz_interp_ratio(value): + """Get horizontal interpolation ratio (query only).""" + with expected_protocol( + ik.tektronix.TekDPO70000, ["HOR:MAI:INTERPR?"], [f"{value}"] + ) as inst: + assert inst.horiz_interp_ratio == value + + +@given(value=st.floats(min_value=0)) +def test_horiz_main_pos(value): + """Get / set horizontal main position. + + Test setting unitful and without units.""" + value_unitful = u.Quantity(value, u.percent) + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"HOR:MAI:POS {value:e}", f"HOR:MAI:POS {value:e}", "HOR:MAI:POS?"], + [f"{value}"], + ) as inst: + inst.horiz_main_pos = value + inst.horiz_main_pos = value_unitful + unit_eq(inst.horiz_main_pos, value_unitful) + + +def test_horiz_unit(): + """Get / set horizontal unit string.""" + unit_string = "LUM" # as example in manual + with expected_protocol( + ik.tektronix.TekDPO70000, + [f'HOR:MAI:UNI "{unit_string}"', "HOR:MAI:UNI?"], + [f'"{unit_string}"'], + ) as inst: + inst.horiz_unit = unit_string + assert inst.horiz_unit == unit_string + + +@pytest.mark.parametrize("value", ik.tektronix.TekDPO70000.HorizontalMode) +def test_horiz_mode(value): + """Get / set horizontal mode.""" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"HOR:MODE {value.value}", "HOR:MODE?"], + [f"{value.value}"], + ) as inst: + inst.horiz_mode = value + assert inst.horiz_mode == value + + +@given(value=st.integers(min_value=0)) +def test_horiz_record_length_lim(value): + """Get / set horizontal record length limit.""" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"HOR:MODE:AUTO:LIMIT {value}", "HOR:MODE:AUTO:LIMIT?"], + [f"{value}"], + ) as inst: + inst.horiz_record_length_lim = value + assert inst.horiz_record_length_lim == value + + +@given(value=st.integers(min_value=0)) +def test_horiz_record_length(value): + """Get / set horizontal record length.""" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"HOR:MODE:RECO {value}", "HOR:MODE:RECO?"], + [f"{value}"], + ) as inst: + inst.horiz_record_length = value + assert inst.horiz_record_length == value + + +@given(value=st.floats(min_value=0, max_value=30e9)) +def test_horiz_sample_rate(value): + """Get / set horizontal sampling rate. + + Set with and without units.""" + value_unitful = u.Quantity(value, u.Hz) + with expected_protocol( + ik.tektronix.TekDPO70000, + [ + f"HOR:MODE:SAMPLER {value:e}", + f"HOR:MODE:SAMPLER {value:e}", + f"HOR:MODE:SAMPLER?", + ], + [f"{value}"], + ) as inst: + inst.horiz_sample_rate = value_unitful + inst.horiz_sample_rate = value + unit_eq(inst.horiz_sample_rate, value_unitful) + + +@given(value=st.floats(min_value=0)) +def test_horiz_scale(value): + """Get / set horizontal scale in seconds per division. + + Set with and without units.""" + value_unitful = u.Quantity(value, u.s) + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"HOR:MODE:SCA {value:e}", f"HOR:MODE:SCA {value:e}", f"HOR:MODE:SCA?"], + [f"{value}"], + ) as inst: + inst.horiz_scale = value_unitful + inst.horiz_scale = value + unit_eq(inst.horiz_scale, value_unitful) + + +@given(value=st.floats(min_value=0)) +def test_horiz_pos(value): + """Get / set position of trigger point on the screen. + + Set with and without units. + """ + value_unitful = u.Quantity(value, u.percent) + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"HOR:POS {value:e}", f"HOR:POS {value:e}", f"HOR:POS?"], + [f"{value}"], + ) as inst: + inst.horiz_pos = value_unitful + inst.horiz_pos = value + unit_eq(inst.horiz_pos, value_unitful) + + +@pytest.mark.parametrize("value", ["AUTO", "OFF", "ON"]) +def test_horiz_roll(value): + """Get / set roll mode status.""" + with expected_protocol( + ik.tektronix.TekDPO70000, [f"HOR:ROLL {value}", f"HOR:ROLL?"], [f"{value}"] + ) as inst: + inst.horiz_roll = value + assert inst.horiz_roll == value + + +@pytest.mark.parametrize("value", ik.tektronix.TekDPO70000.TriggerState) +def test_trigger_state(value): + """Get / set the trigger state.""" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"TRIG:STATE {value.value}", "TRIG:STATE?"], + [f"{value.value}"], + ) as inst: + inst.trigger_state = value + assert inst.trigger_state == value + + +@pytest.mark.parametrize("value", ik.tektronix.TekDPO70000.WaveformEncoding) +def test_outgoing_waveform_encoding(value): + """Get / set the encoding used for outgoing waveforms.""" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"WFMO:ENC {value.value}", "WFMO:ENC?"], + [f"{value.value}"], + ) as inst: + inst.outgoing_waveform_encoding = value + assert inst.outgoing_waveform_encoding == value + + +@pytest.mark.parametrize("value", ik.tektronix.TekDPO70000.BinaryFormat) +def test_outgoing_byte_format(value): + """Get / set the binary format for outgoing waveforms.""" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"WFMO:BN_F {value.value}", "WFMO:BN_F?"], + [f"{value.value}"], + ) as inst: + inst.outgoing_binary_format = value + assert inst.outgoing_binary_format == value + + +@pytest.mark.parametrize("value", ik.tektronix.TekDPO70000.ByteOrder) +def test_outgoing_byte_order(value): + """Get / set the binary data endianness for outgoing waveforms.""" + with expected_protocol( + ik.tektronix.TekDPO70000, + [f"WFMO:BYT_O {value.value}", "WFMO:BYT_O?"], + [f"{value.value}"], + ) as inst: + inst.outgoing_byte_order = value + assert inst.outgoing_byte_order == value + + +@pytest.mark.parametrize("value", (1, 2, 4, 8)) +def test_outgoing_n_bytes(value): + """Get / set the number of bytes sampled in waveforms binary encoding.""" + with expected_protocol( + ik.tektronix.TekDPO70000, [f"WFMO:BYT_N {value}", "WFMO:BYT_N?"], [f"{value}"] + ) as inst: + inst.outgoing_n_bytes = value + assert inst.outgoing_n_bytes == value + + +# METHODS # + + +def test_select_fastest_encoding(): + """Sets encoding to fastest methods.""" + with expected_protocol(ik.tektronix.TekDPO70000, ["DAT:ENC FAS"], []) as inst: + inst.select_fastest_encoding() + + +def test_force_trigger(): + """Force a trivver event.""" + with expected_protocol(ik.tektronix.TekDPO70000, ["TRIG FORC"], []) as inst: + inst.force_trigger() + + +def test_run(): + """Enables the trigger for the oscilloscope.""" + with expected_protocol(ik.tektronix.TekDPO70000, [":RUN"], []) as inst: + inst.run() + + +def test_stop(): + """Disables the trigger for the oscilloscope.""" + with expected_protocol(ik.tektronix.TekDPO70000, [":STOP"], []) as inst: + inst.stop() diff --git a/instruments/tests/test_tektronix/test_tektronix_tds224.py b/instruments/tests/test_tektronix/test_tektronix_tds224.py index fa9830644..267285de2 100644 --- a/instruments/tests/test_tektronix/test_tektronix_tds224.py +++ b/instruments/tests/test_tektronix/test_tektronix_tds224.py @@ -1,78 +1,123 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing tests for the Tektronix TDS224 """ # IMPORTS #################################################################### -from __future__ import absolute_import -from builtins import bytes +from enum import Enum +import time -import numpy as np +from hypothesis import given, strategies as st +import pytest import instruments as ik -from instruments.tests import expected_protocol, make_name_test +from instruments.optional_dep_finder import numpy +from instruments.tests import ( + expected_protocol, + iterable_eq, + make_name_test, +) # TESTS ###################################################################### -# pylint: disable=protected-access +# pylint: disable=protected-access,redefined-outer-name + + +# FIXTURES # + + +@pytest.fixture(autouse=True) +def mock_time(mocker): + """Mock time to replace time.sleep.""" + return mocker.patch.object(time, "sleep", return_value=None) + test_tektds224_name = make_name_test(ik.tektronix.TekTDS224) +def test_ref_init(): + """Initialize a reference channel.""" + with expected_protocol(ik.tektronix.TekTDS224, [], []) as tek: + assert tek.ref[0]._tek is tek + + +def test_data_source_name(): + """Get name of data source.""" + with expected_protocol(ik.tektronix.TekTDS224, [], []) as tek: + assert tek.math.name == "MATH" + + def test_tektds224_data_width(): with expected_protocol( - ik.tektronix.TekTDS224, - [ - "DATA:WIDTH?", - "DATA:WIDTH 1" - ], [ - "2" - ] + ik.tektronix.TekTDS224, ["DATA:WIDTH?", "DATA:WIDTH 1"], ["2"] ) as tek: assert tek.data_width == 2 tek.data_width = 1 -def test_tektds224_data_source(): +@given(width=st.integers().filter(lambda x: x > 2 or x < 1)) +def test_tektds224_data_width_value_error(width): + """Raise value error if data_width is out of range.""" + with expected_protocol(ik.tektronix.TekTDS224, [], []) as tek: + with pytest.raises(ValueError) as err_info: + tek.data_width = width + err_msg = err_info.value.args[0] + assert err_msg == "Only one or two byte-width is supported." + + +def test_tektds224_data_source(mock_time): with expected_protocol( ik.tektronix.TekTDS224, - [ - "DAT:SOU?", - "DAT:SOU MATH" - ], [ - "CH1" - ] + ["DAT:SOU?", "DAT:SOU?", "DAT:SOU MATH"], + ["MATH", "CH1"], ) as tek: - assert tek.data_source == ik.tektronix.tektds224._TekTDS224Channel(tek, 0) + assert tek.data_source == tek.math + assert tek.data_source == ik.tektronix.tektds224.TekTDS224.Channel(tek, 0) tek.data_source = tek.math + # assert that time.sleep is called + mock_time.assert_called() + + +def test_tektds224_data_source_with_enum(): + """Set data source from an enum.""" + + class Channel(Enum): + """Fake class to init data_source with enum.""" + + channel = "MATH" + + with expected_protocol(ik.tektronix.TekTDS224, ["DAT:SOU MATH"], []) as tek: + tek.data_source = Channel.channel + def test_tektds224_channel(): - with expected_protocol( - ik.tektronix.TekTDS224, - [], - [] - ) as tek: - assert tek.channel[ - 0] == ik.tektronix.tektds224._TekTDS224Channel(tek, 0) + with expected_protocol(ik.tektronix.TekTDS224, [], []) as tek: + assert tek.channel[0] == ik.tektronix.tektds224.TekTDS224.Channel(tek, 0) def test_tektds224_channel_coupling(): with expected_protocol( - ik.tektronix.TekTDS224, - [ - "CH1:COUPL?", - "CH2:COUPL AC" - ], [ - "DC" - ] + ik.tektronix.TekTDS224, ["CH1:COUPL?", "CH2:COUPL AC"], ["DC"] ) as tek: assert tek.channel[0].coupling == tek.Coupling.dc tek.channel[1].coupling = tek.Coupling.ac +def test_tektds224_channel_coupling_type_error(): + """Raise TypeError if coupling setting is wrong type.""" + wrong_type = 42 + with expected_protocol(ik.tektronix.TekTDS224, [], []) as tek: + with pytest.raises(TypeError) as err_info: + tek.channel[1].coupling = wrong_type + err_msg = err_info.value.args[0] + assert ( + err_msg == f"Coupling setting must be a `TekTDS224.Coupling` " + f"value,got {type(wrong_type)} instead." + ) + + def test_tektds224_data_source_read_waveform(): with expected_protocol( ik.tektronix.TekTDS224, @@ -88,8 +133,9 @@ def test_tektds224_data_source_read_waveform(): "WFMP:XZE?", "WFMP:XIN?", "WFMP:CH2:NR_P?", - "DAT:SOU CH1" - ], [ + "DAT:SOU CH1", + ], + [ "CH1", "2", # pylint: disable=no-member @@ -98,10 +144,76 @@ def test_tektds224_data_source_read_waveform(): "0", "0", "1", - "5" - ] + "5", + ], ) as tek: - data = np.array([0, 1, 2, 3, 4]) + data = tuple(range(5)) + if numpy: + data = numpy.array([0, 1, 2, 3, 4]) (x, y) = tek.channel[1].read_waveform() - assert (x == data).all() - assert (y == data).all() + iterable_eq(x, data) + iterable_eq(y, data) + + +@given(values=st.lists(st.floats(allow_infinity=False, allow_nan=False), min_size=1)) +def test_tektds224_data_source_read_waveform_ascii(values): + """Read waveform as ASCII""" + # values + values_str = ",".join([str(value) for value in values]) + + # parameters + yoffs = 1 + ymult = 1 + yzero = 0 + xzero = 0 + xincr = 1 + ptcnt = len(values) + + with expected_protocol( + ik.tektronix.TekTDS224, + [ + "DAT:SOU?", + "DAT:SOU CH2", + "DAT:ENC ASCI", + "CURVE?", + "WFMP:CH2:YOF?", + "WFMP:CH2:YMU?", + "WFMP:CH2:YZE?", + "WFMP:XZE?", + "WFMP:XIN?", + "WFMP:CH2:NR_P?", + "DAT:SOU CH1", + ], + [ + "CH1", + values_str, + f"{yoffs}", + f"{ymult}", + f"{yzero}", + f"{xzero}", + f"{xincr}", + f"{ptcnt}", + ], + ) as tek: + if numpy: + x_expected = numpy.arange(float(ptcnt)) * float(xincr) + float(xzero) + y_expected = ((numpy.array(values) - float(yoffs)) * float(ymult)) + float( + yzero + ) + else: + x_expected = tuple( + float(val) * float(xincr) + float(xzero) for val in range(ptcnt) + ) + y_expected = tuple( + ((val - float(yoffs)) * float(ymult)) + float(yzero) for val in values + ) + x_read, y_read = tek.channel[1].read_waveform(bin_format=False) + iterable_eq(x_read, x_expected) + iterable_eq(y_read, y_expected) + + +def test_force_trigger(): + """Raise NotImplementedError when trying to force a trigger.""" + with expected_protocol(ik.tektronix.TekTDS224, [], []) as tek: + with pytest.raises(NotImplementedError): + tek.force_trigger() diff --git a/instruments/tests/test_tektronix/test_tktds5xx.py b/instruments/tests/test_tektronix/test_tktds5xx.py new file mode 100644 index 000000000..095d9604f --- /dev/null +++ b/instruments/tests/test_tektronix/test_tktds5xx.py @@ -0,0 +1,716 @@ +#!/usr/bin/env python +""" +Tests for the Tektronix TDS 5xx series oscilloscope. +""" + + +# IMPORTS ##################################################################### + + +from datetime import datetime +import struct +import time + +from hypothesis import ( + given, + strategies as st, +) +import pytest + +import instruments as ik +from instruments.optional_dep_finder import numpy +from instruments.tests import ( + expected_protocol, + iterable_eq, + make_name_test, +) + + +# TESTS ####################################################################### + + +# pylint: disable=protected-access + + +test_tektds5xx_name = make_name_test(ik.tektronix.TekTDS5xx) + + +# MEASUREMENT # + + +@pytest.mark.parametrize("msr", [it for it in range(3)]) +def test_measurement_init(msr): + """Initialize a new measurement.""" + meas_categories = [ + "enabled", + "type", + "units", + "src1", + "src2", + "edge1", + "edge2", + "dir", + ] + meas_return = '0;UNDEFINED;"V",CH1,CH2,RISE,RISE,FORWARDS' + data_expected = dict(zip(meas_categories, meas_return.split(";"))) + with expected_protocol( + ik.tektronix.TekTDS5xx, [f"MEASU:MEAS{msr+1}?"], [meas_return] + ) as inst: + measurement = inst.measurement[msr] + assert measurement._tek is inst + assert measurement._id == msr + 1 + assert measurement._data == data_expected + + +@pytest.mark.parametrize("msr", [it for it in range(3)]) +@given(value=st.floats(allow_nan=False)) +def test_measurement_read_enabled_true(msr, value): + """Read a new measurement value since enabled is true.""" + enabled = 1 + # initialization dictionary + meas_categories = [ + "enabled", + "type", + "units", + "src1", + "src2", + "edge1", + "edge2", + "dir", + ] + meas_return = f'{enabled};UNDEFINED;"V",CH1,CH2,RISE,RISE,FORWARDS' + data_expected = dict(zip(meas_categories, meas_return.split(";"))) + + # extended dictionary + data_expected["value"] = value + + with expected_protocol( + ik.tektronix.TekTDS5xx, + [f"MEASU:MEAS{msr+1}?", f"MEASU:MEAS{msr+1}:VAL?"], + [meas_return, f"{value}"], + ) as inst: + measurement = inst.measurement[msr] + assert measurement.read() == data_expected + + +def test_measurement_read_enabled_false(): + """Do not read a new measurement value since enabled is false.""" + msr = 0 + enabled = 0 + # initialization dictionary + meas_categories = [ + "enabled", + "type", + "units", + "src1", + "src2", + "edge1", + "edge2", + "dir", + ] + meas_return = f'{enabled};UNDEFINED;"V",CH1,CH2,RISE,RISE,FORWARDS' + data_expected = dict(zip(meas_categories, meas_return.split(";"))) + with expected_protocol( + ik.tektronix.TekTDS5xx, [f"MEASU:MEAS{msr+1}?"], [meas_return] + ) as inst: + measurement = inst.measurement[msr] + assert measurement.read() == data_expected + + +# DATA SOURCE # + + +@given(values=st.lists(st.integers(min_value=-32768, max_value=32767), min_size=1)) +def test_data_source_read_waveform_binary(values): + """Read waveform from data source as binary.""" + # constants - to not overkill it with hypothesis + channel_no = 0 + data_width = 2 + yoffs = 1.0 + ymult = 1.0 + yzero = 0.3 + xincr = 0.001 + # make values to compare with + ptcnt = len(values) + values_arr = values + if numpy: + values_arr = numpy.array(values) + values_packed = b"".join(struct.pack(">h", value) for value in values) + values_len = str(len(values_packed)).encode() + values_len_of_len = str(len(values_len)).encode() + + # calculations + if numpy: + x_calc = numpy.arange(float(ptcnt)) * xincr + y_calc = ((values_arr - yoffs) * ymult) + yzero + else: + x_calc = tuple(float(val) * float(xincr) for val in range(ptcnt)) + y_calc = tuple(((val - yoffs) * float(ymult)) + float(yzero) for val in values) + + with expected_protocol( + ik.tektronix.TekTDS5xx, + [ + "DAT:SOU?", + "DAT:ENC RIB", + "DATA:WIDTH?", + "CURVE?", + f"WFMP:CH{channel_no+1}:YOF?", + f"WFMP:CH{channel_no+1}:YMU?", + f"WFMP:CH{channel_no+1}:YZE?", + f"WFMP:CH{channel_no+1}:XIN?", + f"WFMP:CH{channel_no+1}:NR_P?", + ], + [ + f"CH{channel_no+1}", + f"{data_width}", + b"#" + values_len_of_len + values_len + values_packed, + f"{yoffs}", + f"{ymult}", + f"{yzero}", + f"{xincr}", + f"{ptcnt}", + ], + ) as inst: + channel = inst.channel[channel_no] + x_read, y_read = channel.read_waveform(bin_format=True) + iterable_eq(x_read, x_calc) + iterable_eq(y_read, y_calc) + + +@given(values=st.lists(st.floats(min_value=0), min_size=1)) +def test_data_source_read_waveform_ascii(values): + """Read waveform from data source as ASCII.""" + # constants - to not overkill it with hypothesis + channel_no = 0 + yoffs = 1.0 + ymult = 1.0 + yzero = 0.3 + xincr = 0.001 + # make values to compare with + values_str = ",".join([str(value) for value in values]) + values_arr = values + if numpy: + values_arr = numpy.array(values) + + # calculations + ptcnt = len(values) + if numpy: + x_calc = numpy.arange(float(ptcnt)) * xincr + y_calc = ((values_arr - yoffs) * ymult) + yzero + else: + x_calc = tuple(float(val) * float(xincr) for val in range(ptcnt)) + y_calc = tuple(((val - yoffs) * float(ymult)) + float(yzero) for val in values) + + with expected_protocol( + ik.tektronix.TekTDS5xx, + [ + "DAT:SOU?", + "DAT:ENC ASCI", + "CURVE?", + f"WFMP:CH{channel_no+1}:YOF?", + f"WFMP:CH{channel_no+1}:YMU?", + f"WFMP:CH{channel_no+1}:YZE?", + f"WFMP:CH{channel_no+1}:XIN?", + f"WFMP:CH{channel_no+1}:NR_P?", + ], + [ + f"CH{channel_no+1}", + values_str, + f"{yoffs}", + f"{ymult}", + f"{yzero}", + f"{xincr}", + f"{ptcnt}", + ], + ) as inst: + channel = inst.channel[channel_no] + x_read, y_read = channel.read_waveform(bin_format=False) + iterable_eq(x_read, x_calc) + iterable_eq(y_read, y_calc) + + +# CHANNEL # + + +@pytest.mark.parametrize("channel", [it for it in range(4)]) +def test_channel_init(channel): + """Initialize a new channel.""" + with expected_protocol(ik.tektronix.TekTDS5xx, [], []) as inst: + assert inst.channel[channel]._parent is inst + assert inst.channel[channel]._idx == channel + 1 + + +@pytest.mark.parametrize("coupl", ik.tektronix.TekTDS5xx.Coupling) +def test_channel_coupling(coupl): + """Get / set channel coupling.""" + channel = 0 + with expected_protocol( + ik.tektronix.TekTDS5xx, + [f"CH{channel+1}:COUPL {coupl.value}", f"CH{channel+1}:COUPL?"], + [f"{coupl.value}"], + ) as inst: + inst.channel[channel].coupling = coupl + assert inst.channel[channel].coupling == coupl + + +def test_channel_coupling_type_error(): + """Raise type error if channel coupling is set with wrong type.""" + wrong_type = 42 + channel = 0 + with expected_protocol(ik.tektronix.TekTDS5xx, [], []) as inst: + with pytest.raises(TypeError) as err_info: + inst.channel[channel].coupling = wrong_type + err_msg = err_info.value.args[0] + assert ( + err_msg == f"Coupling setting must be a `TekTDS5xx.Coupling` " + f"value, got {type(wrong_type)} instead." + ) + + +@pytest.mark.parametrize("bandw", ik.tektronix.TekTDS5xx.Bandwidth) +def test_channel_bandwidth(bandw): + """Get / set channel bandwidth.""" + channel = 0 + with expected_protocol( + ik.tektronix.TekTDS5xx, + [f"CH{channel+1}:BAND {bandw.value}", f"CH{channel+1}:BAND?"], + [f"{bandw.value}"], + ) as inst: + inst.channel[channel].bandwidth = bandw + assert inst.channel[channel].bandwidth == bandw + + +def test_channel_bandwidth_type_error(): + """Raise type error if channel bandwidth is set with wrong type.""" + wrong_type = 42 + channel = 0 + with expected_protocol(ik.tektronix.TekTDS5xx, [], []) as inst: + with pytest.raises(TypeError) as err_info: + inst.channel[channel].bandwidth = wrong_type + err_msg = err_info.value.args[0] + assert ( + err_msg == f"Bandwidth setting must be a " + f"`TekTDS5xx.Bandwidth` value, got " + f"{type(wrong_type)} instead." + ) + + +@pytest.mark.parametrize("imped", ik.tektronix.TekTDS5xx.Impedance) +def test_channel_impedance(imped): + """Get / set channel impedance.""" + channel = 0 + with expected_protocol( + ik.tektronix.TekTDS5xx, + [f"CH{channel+1}:IMP {imped.value}", f"CH{channel+1}:IMP?"], + [f"{imped.value}"], + ) as inst: + inst.channel[channel].impedance = imped + assert inst.channel[channel].impedance == imped + + +def test_channel_impedance_type_error(): + """Raise type error if channel impedance is set with wrong type.""" + wrong_type = 42 + channel = 0 + with expected_protocol(ik.tektronix.TekTDS5xx, [], []) as inst: + with pytest.raises(TypeError) as err_info: + inst.channel[channel].impedance = wrong_type + err_msg = err_info.value.args[0] + assert ( + err_msg == f"Impedance setting must be a " + f"`TekTDS5xx.Impedance` value, got " + f"{type(wrong_type)} instead." + ) + + +@given(value=st.floats(min_value=0, exclude_min=True)) +def test_channel_probe(value): + """Get connected probe value.""" + channel = 0 + with expected_protocol( + ik.tektronix.TekTDS5xx, [f"CH{channel+1}:PRO?"], [f"{value}"] + ) as inst: + value_expected = round(1 / value, 0) + assert inst.channel[channel].probe == value_expected + + +@given(value=st.floats(min_value=0)) +def test_channel_scale(value): + """Get / set scale setting.""" + channel = 0 + with expected_protocol( + ik.tektronix.TekTDS5xx, + [ + f"CH{channel + 1}:SCA {value:.3E}", + f"CH{channel + 1}:SCA?", + f"CH{channel + 1}:SCA?", + ], + [f"{value}", f"{value}"], + ) as inst: + inst.channel[channel].scale = value + print(f"\n>>>{value}") + assert inst.channel[channel].scale == value + + +def test_channel_scale_value_error(): + """Raise ValueError if scale was not set properly.""" + scale_set = 42 + scale_rec = 13 + channel = 0 + with expected_protocol( + ik.tektronix.TekTDS5xx, + [f"CH{channel + 1}:SCA {scale_set:.3E}", f"CH{channel + 1}:SCA?"], + [f"{scale_rec}"], + ) as inst: + with pytest.raises(ValueError) as err_info: + inst.channel[channel].scale = scale_set + err_msg = err_info.value.args[0] + assert ( + err_msg == f"Tried to set CH{channel+1} Scale to {scale_set} " + f"but got {float(scale_rec)} instead" + ) + + +# INSTRUMENT # + + +@given(states=st.lists(st.integers(min_value=0, max_value=1), min_size=11, max_size=11)) +def test_sources(states): + """Get list of all active sources.""" + active_sources = [] + with expected_protocol( + ik.tektronix.TekTDS5xx, ["SEL?"], [";".join([str(state) for state in states])] + ) as inst: + # create active_sources + for idx in range(4): + if states[idx]: + active_sources.append( + ik.tektronix.tektds5xx.TekTDS5xx.Channel(inst, idx) + ) + for idx in range(4, 7): + if states[idx]: + active_sources.append( + ik.tektronix.tektds5xx.TekTDS5xx.DataSource(inst, f"MATH{idx - 3}") + ) + for idx in range(7, 11): + if states[idx]: + active_sources.append( + ik.tektronix.tektds5xx.TekTDS5xx.DataSource(inst, f"REF{idx - 6}") + ) + # read active sources + active_read = inst.sources + + assert active_read == active_sources + + +@pytest.mark.parametrize("channel", [it for it in range(4)]) +def test_data_source_channel(channel): + """Get / set channel data source for waveform transfer.""" + with expected_protocol( + ik.tektronix.TekTDS5xx, + [f"DAT:SOU CH{channel+1}", f"DAT:SOU CH{channel+1}", "DAT:SOU?"], + [f"CH{channel+1}"], + ) as inst: + # set as Source enum + inst.data_source = ik.tektronix.TekTDS5xx.Source[f"CH{channel + 1}"] + # set as channel object + data_source = inst.channel[channel] + inst.data_source = data_source + assert inst.data_source == data_source + + +@pytest.mark.parametrize("channel", [it for it in range(3)]) +def test_data_source_math(channel): + """Get / set math data source for waveform transfer.""" + with expected_protocol( + ik.tektronix.TekTDS5xx, + [f"DAT:SOU MATH{channel+1}", f"DAT:SOU MATH{channel+1}", "DAT:SOU?"], + [f"MATH{channel+1}"], + ) as inst: + # set as Source enum + inst.data_source = ik.tektronix.TekTDS5xx.Source[f"Math{channel + 1}"] + # set as channel object + data_source = inst.math[channel] + inst.data_source = data_source + assert inst.data_source == data_source + + +@pytest.mark.parametrize("channel", [it for it in range(3)]) +def test_data_source_ref(channel): + """Get / set ref data source for waveform transfer.""" + with expected_protocol( + ik.tektronix.TekTDS5xx, + [f"DAT:SOU REF{channel+1}", f"DAT:SOU REF{channel+1}", "DAT:SOU?"], + [f"REF{channel+1}"], + ) as inst: + # set as Source enum + inst.data_source = ik.tektronix.TekTDS5xx.Source[f"Ref{channel + 1}"] + # set as channel object + data_source = inst.ref[channel] + inst.data_source = data_source + assert inst.data_source == data_source + + +def test_data_source_raise_type_error(): + """Raise TypeError when setting data source with wrong type.""" + wrong_type = 42 + with expected_protocol(ik.tektronix.TekTDS5xx, [], []) as inst: + with pytest.raises(TypeError) as err_info: + inst.data_source = wrong_type + err_msg = err_info.value.args[0] + assert ( + err_msg == f"Source setting must be a `TekTDS5xx.Source` " + f"value, got {type(wrong_type)} instead." + ) + + +@pytest.mark.parametrize("width", (1, 2)) +def test_data_width(width): + """Get / set data width.""" + with expected_protocol( + ik.tektronix.TekTDS5xx, [f"DATA:WIDTH {width}", "DATA:WIDTH?"], [f"{width}"] + ) as inst: + inst.data_width = width + assert inst.data_width == width + + +@given(width=st.integers().filter(lambda x: x < 1 or x > 2)) +def test_data_width_value_error(width): + """Raise ValueError when setting a wrong data width.""" + with expected_protocol(ik.tektronix.TekTDS5xx, [], []) as inst: + with pytest.raises(ValueError) as err_info: + inst.data_width = width + err_msg = err_info.value.args[0] + assert err_msg == "Only one or two byte-width is supported." + + +def test_force_trigger(): + """Raise NotImplementedError when forcing a trigger.""" + with expected_protocol(ik.tektronix.TekTDS5xx, [], []) as inst: + with pytest.raises(NotImplementedError): + inst.force_trigger() + + +@given(value=st.floats(min_value=0)) +def test_horizontal_scale(value): + """Get / set horizontal scale.""" + with expected_protocol( + ik.tektronix.TekTDS5xx, + [f"HOR:MAI:SCA {value:.3E}", "HOR:MAI:SCA?", "HOR:MAI:SCA?"], + [f"{value}", f"{value}"], + ) as inst: + inst.horizontal_scale = value + assert inst.horizontal_scale == value + + +def test_horizontal_scale_value_error(): + """Raise ValueError if setting horizontal scale does not work.""" + set_value = 42 + get_value = 13 + with expected_protocol( + ik.tektronix.TekTDS5xx, + [f"HOR:MAI:SCA {set_value:.3E}", "HOR:MAI:SCA?"], + [ + f"{get_value}", + ], + ) as inst: + with pytest.raises(ValueError) as err_info: + inst.horizontal_scale = set_value + err_msg = err_info.value.args[0] + assert ( + err_msg == f"Tried to set Horizontal Scale to {set_value} " + f"but got {float(get_value)} instead" + ) + + +@given(value=st.floats(min_value=0)) +def test_trigger_level(value): + """Get / set trigger level.""" + with expected_protocol( + ik.tektronix.TekTDS5xx, + [f"TRIG:MAI:LEV {value:.3E}", "TRIG:MAI:LEV?", "TRIG:MAI:LEV?"], + [f"{value}", f"{value}"], + ) as inst: + inst.trigger_level = value + assert inst.trigger_level == value + + +def test_trigger_level_value_error(): + """Raise ValueError if setting trigger level does not work.""" + set_value = 42 + get_value = 13 + with expected_protocol( + ik.tektronix.TekTDS5xx, + [f"TRIG:MAI:LEV {set_value:.3E}", "TRIG:MAI:LEV?"], + [f"{get_value}"], + ) as inst: + with pytest.raises(ValueError) as err_info: + inst.trigger_level = set_value + err_msg = err_info.value.args[0] + assert ( + err_msg == f"Tried to set trigger level to {set_value} " + f"but got {float(get_value)} instead" + ) + + +@pytest.mark.parametrize("coupl", ik.tektronix.TekTDS5xx.Coupling) +def test_trigger_coupling(coupl): + """Get / set trigger coupling.""" + with expected_protocol( + ik.tektronix.TekTDS5xx, + [f"TRIG:MAI:EDGE:COUP {coupl.value}", "TRIG:MAI:EDGE:COUP?"], + [f"{coupl.value}"], + ) as inst: + inst.trigger_coupling = coupl + assert inst.trigger_coupling == coupl + + +def test_trigger_coupling_type_error(): + """Raise type error when coupling is not a `Coupling` enum.""" + wrong_type = 42 + with expected_protocol(ik.tektronix.TekTDS5xx, [], []) as inst: + with pytest.raises(TypeError) as err_info: + inst.trigger_coupling = wrong_type + err_msg = err_info.value.args[0] + assert ( + err_msg == f"Coupling setting must be a `TekTDS5xx.Coupling` " + f"value, got {type(wrong_type)} instead." + ) + + +@pytest.mark.parametrize("edge", ik.tektronix.TekTDS5xx.Edge) +def test_trigger_slope(edge): + """Get / set trigger slope.""" + with expected_protocol( + ik.tektronix.TekTDS5xx, + [f"TRIG:MAI:EDGE:SLO {edge.value}", "TRIG:MAI:EDGE:SLO?"], + [f"{edge.value}"], + ) as inst: + inst.trigger_slope = edge + assert inst.trigger_slope == edge + + +def test_trigger_slope_type_error(): + """Raise type error when edge is not an `Edge` enum.""" + wrong_type = 42 + with expected_protocol(ik.tektronix.TekTDS5xx, [], []) as inst: + with pytest.raises(TypeError) as err_info: + inst.trigger_slope = wrong_type + err_msg = err_info.value.args[0] + assert ( + err_msg == f"Edge setting must be a `TekTDS5xx.Edge` " + f"value, got {type(wrong_type)} instead." + ) + + +@pytest.mark.parametrize("source", ik.tektronix.TekTDS5xx.Trigger) +def test_trigger_source(source): + """Get / set trigger source.""" + with expected_protocol( + ik.tektronix.TekTDS5xx, + [f"TRIG:MAI:EDGE:SOU {source.value}", "TRIG:MAI:EDGE:SOU?"], + [f"{source.value}"], + ) as inst: + inst.trigger_source = source + assert inst.trigger_source == source + + +def test_trigger_source_type_error(): + """Raise type error when source is not an `source` enum.""" + wrong_type = 42 + with expected_protocol(ik.tektronix.TekTDS5xx, [], []) as inst: + with pytest.raises(TypeError) as err_info: + inst.trigger_source = wrong_type + err_msg = err_info.value.args[0] + assert ( + err_msg == f"Trigger source setting must be a " + f"`TekTDS5xx.Trigger` value, got " + f"{type(wrong_type)} instead." + ) + + +@given(dt=st.datetimes(min_value=datetime(1000, 1, 1))) +def test_clock(dt): + """Get / set oscilloscope clock.""" + # create a date and time + dt_fmt_receive = '"%Y-%m-%d";"%H:%M:%S"' + dt_fmt_send = 'DATE "%Y-%m-%d";:TIME "%H:%M:%S"' + with expected_protocol( + ik.tektronix.TekTDS5xx, + [dt.strftime(dt_fmt_send), "DATE?;:TIME?"], + [dt.strftime(dt_fmt_receive)], + ) as inst: + inst.clock = dt + assert inst.clock == dt.replace(microsecond=0) + + +def test_clock_value_error(): + """Raise ValueError when not set with datetime object.""" + wrong_type = 42 + with expected_protocol(ik.tektronix.TekTDS5xx, [], []) as inst: + with pytest.raises(ValueError) as err_info: + inst.clock = wrong_type + err_msg = err_info.value.args[0] + assert ( + err_msg == f"Expected datetime.datetime but got " + f"{type(wrong_type)} instead" + ) + + +@pytest.mark.parametrize("newval", (True, False)) +def test_display_clock(newval): + """Get / set if clock is displayed on screen.""" + with expected_protocol( + ik.tektronix.TekTDS5xx, + [f"DISPLAY:CLOCK {int(newval)}", "DISPLAY:CLOCK?"], + [f"{int(newval)}"], + ) as inst: + inst.display_clock = newval + assert inst.display_clock == newval + + +def test_display_clock_value_error(): + """Raise ValueError when display_clock is called w/o a bool.""" + wrong_type = 42 + with expected_protocol(ik.tektronix.TekTDS5xx, [], []) as inst: + with pytest.raises(ValueError) as err_info: + inst.display_clock = wrong_type + err_msg = err_info.value.args[0] + assert err_msg == f"Expected bool but got {type(wrong_type)} instead" + + +@given(data=st.binary(min_size=1, max_size=2147483647)) +def test_get_hardcopy(mocker, data): + """Transfer data in binary from the instrument. + + Data is at least 1 byte long, then we need to add 8 for the + color table. + Fake the header of the data such that in byte 18:30 are 4 factorial + packed as ' stop + message_id=ThorLabsCommands.PZMOT_MOVE_JOG, + param1=0x01, + param2=0x00, + dest=0x50, + source=0x01, + data=None, + ).pack(), + ], + [init_kim101[1]], + sep="", + ) as apt: + apt.channel[0].move_jog_stop() + + +# CONTROLLER # + + +def test_apt_pia_enabled_multi(init_kim101): + """Multi-channel enabling APT Piezo Inertia Actuator KIM101. + + Tested with KIM101 driver connected to PIM1 mirror mount. + """ + with expected_protocol( + ik.thorlabs.APTPiezoInertiaActuator, + [ + init_kim101[0], + ThorLabsPacket( # all off + message_id=ThorLabsCommands.PZMOT_REQ_PARAMS, + param1=0x2B, + param2=0x00, + dest=0x50, + source=0x01, + data=None, + ).pack(), + ThorLabsPacket( # read channel 0 & 1 + message_id=ThorLabsCommands.PZMOT_REQ_PARAMS, + param1=0x2B, + param2=0x00, + dest=0x50, + source=0x01, + data=None, + ).pack(), + ThorLabsPacket( # read channel 2 & 3 + message_id=ThorLabsCommands.PZMOT_REQ_PARAMS, + param1=0x2B, + param2=0x00, + dest=0x50, + source=0x01, + data=None, + ).pack(), + ThorLabsPacket( # send off + message_id=ThorLabsCommands.PZMOT_SET_PARAMS, + param1=None, + param2=None, + dest=0x50, + source=0x01, + data=struct.pack(" 0) + for key, bit_mask in apt_mc_channel_status_bit_mask.items() + } + + with expected_protocol( + ik.thorlabs.APTMotorController, + [ + init_kdc101[0], + ThorLabsPacket( # read position + message_id=ThorLabsCommands.MOT_REQ_STATUSUPDATE, + param1=0x01, + param2=0x00, + dest=0x50, + source=0x01, + data=None, + ).pack(), + ], + [ + init_kdc101[1], + ThorLabsPacket( + message_id=ThorLabsCommands.MOT_GET_POSCOUNTER, + param1=None, + param2=None, + dest=0x50, + source=0x01, + data=struct.pack("" - ], - sep="\r" + ik.thorlabs.LCC25, ["*idn?"], ["*idn?", "bloopbloop", "> "], sep="\r" ) as lcc: name = lcc.name - assert name == "bloopbloop", "got {} expected bloopbloop".format(name) + assert name == "bloopbloop", f"got {name} expected bloopbloop" def test_lcc25_frequency(): with expected_protocol( ik.thorlabs.LCC25, - [ - "freq?", - "freq=10.0" - ], - [ - "freq?", - "20", - ">freq=10.0", - ">" - ], - sep="\r" + ["freq?", "freq=10.0"], + ["freq?", "20", "> freq=10.0", "> "], + sep="\r", ) as lcc: - unit_eq(lcc.frequency, pq.Quantity(20, "Hz")) + unit_eq(lcc.frequency, u.Quantity(20, "Hz")) lcc.frequency = 10.0 -@raises(ValueError) def test_lcc25_frequency_lowlimit(): - with expected_protocol( - ik.thorlabs.LCC25, - [ - "freq=0.0" - ], - [ - "freq=0.0", - ">" - ], - sep="\r" + with pytest.raises(ValueError), expected_protocol( + ik.thorlabs.LCC25, ["freq=0.0"], ["freq=0.0", "> "], sep="\r" ) as lcc: lcc.frequency = 0.0 -@raises(ValueError) def test_lcc25_frequency_highlimit(): - with expected_protocol( - ik.thorlabs.LCC25, - [ - "freq=160.0" - ], - [ - "freq=160.0", - ">" - ], - sep="\r" + with pytest.raises(ValueError), expected_protocol( + ik.thorlabs.LCC25, ["freq=160.0"], ["freq=160.0", "> "], sep="\r" ) as lcc: lcc.frequency = 160.0 @@ -88,135 +51,75 @@ def test_lcc25_frequency_highlimit(): def test_lcc25_mode(): with expected_protocol( ik.thorlabs.LCC25, - [ - "mode?", - "mode=1" - ], - [ - "mode?", - "2", - ">mode=1", - ">" - ], - sep="\r" + ["mode?", "mode=1"], + ["mode?", "2", "> mode=1", "> "], + sep="\r", ) as lcc: assert lcc.mode == ik.thorlabs.LCC25.Mode.voltage2 lcc.mode = ik.thorlabs.LCC25.Mode.voltage1 -@raises(ValueError) def test_lcc25_mode_invalid(): - with expected_protocol( - ik.thorlabs.LCC25, - [], - [] - ) as lcc: + with pytest.raises(ValueError), expected_protocol(ik.thorlabs.LCC25, [], []) as lcc: lcc.mode = "blo" def test_lcc25_enable(): with expected_protocol( ik.thorlabs.LCC25, - [ - "enable?", - "enable=1" - ], - [ - "enable?", - "0", - ">enable=1", - ">" - ], - sep="\r" + ["enable?", "enable=1"], + ["enable?", "0", "> enable=1", "> "], + sep="\r", ) as lcc: assert lcc.enable is False lcc.enable = True -@raises(TypeError) def test_lcc25_enable_invalid_type(): - with expected_protocol( - ik.thorlabs.LCC25, - [], - [] - ) as lcc: + with pytest.raises(TypeError), expected_protocol(ik.thorlabs.LCC25, [], []) as lcc: lcc.enable = "blo" def test_lcc25_extern(): with expected_protocol( ik.thorlabs.LCC25, - [ - "extern?", - "extern=1" - ], - [ - "extern?", - "0", - ">extern=1", - ">" - ], - sep="\r" + ["extern?", "extern=1"], + ["extern?", "0", "> extern=1", "> "], + sep="\r", ) as lcc: assert lcc.extern is False lcc.extern = True -@raises(TypeError) def test_tc200_extern_invalid_type(): - with expected_protocol( - ik.thorlabs.LCC25, - [], - [] - ) as tc: + with pytest.raises(TypeError), expected_protocol(ik.thorlabs.LCC25, [], []) as tc: tc.extern = "blo" def test_lcc25_remote(): with expected_protocol( ik.thorlabs.LCC25, - [ - "remote?", - "remote=1" - ], - [ - "remote?", - "0", - ">remote=1", - ">" - ], - sep="\r" + ["remote?", "remote=1"], + ["remote?", "0", "> remote=1", "> "], + sep="\r", ) as lcc: assert lcc.remote is False lcc.remote = True -@raises(TypeError) def test_tc200_remote_invalid_type(): - with expected_protocol( - ik.thorlabs.LCC25, - [], - [] - ) as tc: + with pytest.raises(TypeError), expected_protocol(ik.thorlabs.LCC25, [], []) as tc: tc.remote = "blo" def test_lcc25_voltage1(): with expected_protocol( ik.thorlabs.LCC25, - [ - "volt1?", - "volt1=10.0" - ], - [ - "volt1?", - "20", - ">volt1=10.0", - ">" - ], - sep="\r" + ["volt1?", "volt1=10.0"], + ["volt1?", "20", "> volt1=10.0", "> "], + sep="\r", ) as lcc: - unit_eq(lcc.voltage1, pq.Quantity(20, "V")) + unit_eq(lcc.voltage1, u.Quantity(20, "V")) lcc.voltage1 = 10.0 @@ -233,87 +136,49 @@ def test_lcc25_voltage2(): "volt2?", "volt2=10.0", ], - [ - "volt2?", - "20", - ">volt2=10.0", - ">" - ], - sep="\r" + ["volt2?", "20", "> volt2=10.0", "> "], + sep="\r", ) as lcc: - unit_eq(lcc.voltage2, pq.Quantity(20, "V")) + unit_eq(lcc.voltage2, u.Quantity(20, "V")) lcc.voltage2 = 10.0 def test_lcc25_minvoltage(): with expected_protocol( ik.thorlabs.LCC25, - [ - "min?", - "min=10.0" - ], - [ - "min?", - "20", - ">min=10.0", - ">" - ], - sep="\r" + ["min?", "min=10.0"], + ["min?", "20", "> min=10.0", "> "], + sep="\r", ) as lcc: - unit_eq(lcc.min_voltage, pq.Quantity(20, "V")) + unit_eq(lcc.min_voltage, u.Quantity(20, "V")) lcc.min_voltage = 10.0 def test_lcc25_maxvoltage(): with expected_protocol( ik.thorlabs.LCC25, - [ - "max?", - "max=10.0" - ], - [ - "max?", - "20", - ">max=10.0", - ">" - ], - sep="\r" + ["max?", "max=10.0"], + ["max?", "20", "> max=10.0", "> "], + sep="\r", ) as lcc: - unit_eq(lcc.max_voltage, pq.Quantity(20, "V")) + unit_eq(lcc.max_voltage, u.Quantity(20, "V")) lcc.max_voltage = 10.0 def test_lcc25_dwell(): with expected_protocol( ik.thorlabs.LCC25, - [ - "dwell?", - "dwell=10" - ], - [ - "dwell?", - "20", - ">dwell=10", - ">" - ], - sep="\r" + ["dwell?", "dwell=10"], + ["dwell?", "20", "> dwell=10", "> "], + sep="\r", ) as lcc: - unit_eq(lcc.dwell, pq.Quantity(20, "ms")) + unit_eq(lcc.dwell, u.Quantity(20, "ms")) lcc.dwell = 10 -@raises(ValueError) def test_lcc25_dwell_positive(): - with expected_protocol( - ik.thorlabs.LCC25, - [ - "dwell=-10" - ], - [ - "dwell=-10", - ">" - ], - sep="\r" + with pytest.raises(ValueError), expected_protocol( + ik.thorlabs.LCC25, ["dwell=-10"], ["dwell=-10", "> "], sep="\r" ) as lcc: lcc.dwell = -10 @@ -321,155 +186,75 @@ def test_lcc25_dwell_positive(): def test_lcc25_increment(): with expected_protocol( ik.thorlabs.LCC25, - [ - "increment?", - "increment=10.0" - ], - [ - "increment?", - "20", - ">increment=10.0", - ">" - ], - sep="\r" + ["increment?", "increment=10.0"], + ["increment?", "20", "> increment=10.0", "> "], + sep="\r", ) as lcc: - unit_eq(lcc.increment, pq.Quantity(20, "V")) + unit_eq(lcc.increment, u.Quantity(20, "V")) lcc.increment = 10.0 -@raises(ValueError) def test_lcc25_increment_positive(): - with expected_protocol( - ik.thorlabs.LCC25, - [ - "increment=-10" - ], - [ - "increment=-10", - ">" - ], - sep="\r" + with pytest.raises(ValueError), expected_protocol( + ik.thorlabs.LCC25, ["increment=-10"], ["increment=-10", "> "], sep="\r" ) as lcc: lcc.increment = -10 def test_lcc25_default(): with expected_protocol( - ik.thorlabs.LCC25, - [ - "default" - ], - [ - "default", - "1", - ">" - ], - sep="\r" + ik.thorlabs.LCC25, ["default"], ["default", "1", "> "], sep="\r" ) as lcc: lcc.default() def test_lcc25_save(): with expected_protocol( - ik.thorlabs.LCC25, - [ - "save" - ], - [ - "save", - "1", - ">" - ], - sep="\r" + ik.thorlabs.LCC25, ["save"], ["save", "1", "> "], sep="\r" ) as lcc: lcc.save() def test_lcc25_set_settings(): with expected_protocol( - ik.thorlabs.LCC25, - [ - "set=2" - ], - [ - "set=2", - "1", - ">" - ], - sep="\r" + ik.thorlabs.LCC25, ["set=2"], ["set=2", "1", "> "], sep="\r" ) as lcc: lcc.set_settings(2) -@raises(ValueError) def test_lcc25_set_settings_invalid(): - with expected_protocol( - ik.thorlabs.LCC25, - [], - [], - sep="\r" + with pytest.raises(ValueError), expected_protocol( + ik.thorlabs.LCC25, [], [], sep="\r" ) as lcc: lcc.set_settings(5) def test_lcc25_get_settings(): with expected_protocol( - ik.thorlabs.LCC25, - [ - "get=2" - ], - [ - "get=2", - "1", - ">" - ], - sep="\r" + ik.thorlabs.LCC25, ["get=2"], ["get=2", "1", "> "], sep="\r" ) as lcc: lcc.get_settings(2) -@raises(ValueError) def test_lcc25_get_settings_invalid(): - with expected_protocol( - ik.thorlabs.LCC25, - [], - [], - sep="\r" + with pytest.raises(ValueError), expected_protocol( + ik.thorlabs.LCC25, [], [], sep="\r" ) as lcc: lcc.get_settings(5) def test_lcc25_test_mode(): with expected_protocol( - ik.thorlabs.LCC25, - [ - "test" - ], - [ - "test", - "1", - ">" - ], - sep="\r" + ik.thorlabs.LCC25, ["test"], ["test", "1", "> "], sep="\r" ) as lcc: lcc.test_mode() -@raises(TypeError) def test_lcc25_remote_invalid_type(): - with expected_protocol( - ik.thorlabs.LCC25, - [], - [] - ) as lcc: + with pytest.raises(TypeError), expected_protocol(ik.thorlabs.LCC25, [], []) as lcc: lcc.remote = "blo" -@raises(TypeError) def test_lcc25_extern_invalid_type(): - with expected_protocol( - ik.thorlabs.LCC25, - [], - [] - ) as lcc: + with pytest.raises(TypeError), expected_protocol(ik.thorlabs.LCC25, [], []) as lcc: lcc.extern = "blo" diff --git a/instruments/tests/test_thorlabs/test_thorlabs_pm100usb.py b/instruments/tests/test_thorlabs/test_thorlabs_pm100usb.py new file mode 100644 index 000000000..fdbe055e0 --- /dev/null +++ b/instruments/tests/test_thorlabs/test_thorlabs_pm100usb.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python +""" +Module containing tests for the Thorlabs PM100USB +""" + +# IMPORTS #################################################################### + + +from hypothesis import ( + given, + strategies as st, +) +import pytest + +import instruments as ik +from instruments.tests import expected_protocol + + +# TESTS ###################################################################### + + +# pylint: disable=protected-access,redefined-outer-name + + +# FIXTURES # + + +@pytest.fixture +def init_sensor(): + """Initialize a sensor - return initialized sensor class.""" + + class Sensor: + """Initialize a sensor class""" + + NAME = "SENSOR" + SERIAL_NUMBER = "123456" + CALIBRATION_MESSAGE = "OK" + SENSOR_TYPE = "TEMPERATURE" + SENSOR_SUBTYPE = "KDP" + FLAGS = "256" + + def sendmsg(self): + return "SYST:SENSOR:IDN?" + + def message(self): + return ",".join( + [ + self.NAME, + self.SERIAL_NUMBER, + self.CALIBRATION_MESSAGE, + self.SENSOR_TYPE, + self.SENSOR_SUBTYPE, + self.FLAGS, + ] + ) + + return Sensor() + + +# SENSOR CLASS # + + +def test_sensor_init(init_sensor): + """Initialize a sensor object from the parent class.""" + with expected_protocol( + ik.thorlabs.PM100USB, [init_sensor.sendmsg()], [init_sensor.message()] + ) as inst: + assert inst.sensor._parent is inst + + +def test_sensor_name(init_sensor): + """Get name of the sensor.""" + with expected_protocol( + ik.thorlabs.PM100USB, [init_sensor.sendmsg()], [init_sensor.message()] + ) as inst: + assert inst.sensor.name == init_sensor.NAME + + +def test_sensor_serial_number(init_sensor): + """Get serial number of the sensor.""" + with expected_protocol( + ik.thorlabs.PM100USB, [init_sensor.sendmsg()], [init_sensor.message()] + ) as inst: + assert inst.sensor.serial_number == init_sensor.SERIAL_NUMBER + + +def test_sensor_calibration_message(init_sensor): + """Get calibration message of the sensor.""" + with expected_protocol( + ik.thorlabs.PM100USB, [init_sensor.sendmsg()], [init_sensor.message()] + ) as inst: + assert inst.sensor.calibration_message == init_sensor.CALIBRATION_MESSAGE + + +def test_sensor_type(init_sensor): + """Get type of the sensor.""" + with expected_protocol( + ik.thorlabs.PM100USB, [init_sensor.sendmsg()], [init_sensor.message()] + ) as inst: + assert inst.sensor.type == (init_sensor.SENSOR_TYPE, init_sensor.SENSOR_SUBTYPE) + + +def test_sensor_flags(init_sensor): + """Get flags of the sensor.""" + flag_read = init_sensor.FLAGS + flags = ik.thorlabs.PM100USB._SensorFlags( + **{e.name: bool(e & int(flag_read)) for e in ik.thorlabs.PM100USB.SensorFlags} + ) + with expected_protocol( + ik.thorlabs.PM100USB, [init_sensor.sendmsg()], [init_sensor.message()] + ) as inst: + assert inst.sensor.flags == flags + + +# INSTRUMENT # + + +def test_cache_units(): + """Get, set cache units bool.""" + msr_conf = ik.thorlabs.PM100USB.MeasurementConfiguration.current + with expected_protocol( + ik.thorlabs.PM100USB, + ["CONF?"], + [f"{msr_conf.value}"], # measurement configuration temperature + ) as inst: + inst.cache_units = True + assert inst._cache_units == inst._READ_UNITS[msr_conf] + inst.cache_units = False + assert not inst.cache_units + + +@pytest.mark.parametrize("msr_conf", ik.thorlabs.PM100USB.MeasurementConfiguration) +def test_measurement_configuration(msr_conf): + """Get / set measurement configuration.""" + with expected_protocol( + ik.thorlabs.PM100USB, + [f"CONF {msr_conf.value}", "CONF?"], + [f"{msr_conf.value}"], # measurement configuration temperature + ) as inst: + inst.measurement_configuration = msr_conf + assert inst.measurement_configuration == msr_conf + + +@given(value=st.integers(min_value=1)) +def test_averaging_count(value): + """Get / set averaging count.""" + with expected_protocol( + ik.thorlabs.PM100USB, + [f"SENS:AVER:COUN {value}", "SENS:AVER:COUN?"], + [f"{value}"], # measurement configuration temperature + ) as inst: + inst.averaging_count = value + assert inst.averaging_count == value + + +@given(value=st.integers(max_value=0)) +def test_averaging_count_value_error(value): + """Raise a ValueError if the averaging count is wrong.""" + with expected_protocol(ik.thorlabs.PM100USB, [], []) as inst: + with pytest.raises(ValueError) as err_info: + inst.averaging_count = value + err_msg = err_info.value.args[0] + assert err_msg == "Must count at least one time." + + +@given(value=st.floats(min_value=0)) +def test_read(value): + """Read instrument and grab the units.""" + msr_conf = ik.thorlabs.PM100USB.MeasurementConfiguration.current + with expected_protocol( + ik.thorlabs.PM100USB, + ["CONF?", "READ?"], + [f"{msr_conf.value}", f"{value}"], # measurement configuration temperature + ) as inst: + units = inst._READ_UNITS[msr_conf] # cache units is False at init + assert inst.read() == value * units + + +def test_read_cached_units(): + """Read instrument and grab the units.""" + msr_conf = ik.thorlabs.PM100USB.MeasurementConfiguration.current + value = 42 + with expected_protocol( + ik.thorlabs.PM100USB, + ["CONF?", "READ?"], + [f"{msr_conf.value}", f"{value}"], # measurement configuration temperature + ) as inst: + units = inst._READ_UNITS[msr_conf] # cache units is False at init + inst.cache_units = True + assert inst.read() == value * units diff --git a/instruments/tests/test_thorlabs/test_thorlabs_sc10.py b/instruments/tests/test_thorlabs/test_thorlabs_sc10.py index 942c2bbef..2564ac584 100644 --- a/instruments/tests/test_thorlabs/test_thorlabs_sc10.py +++ b/instruments/tests/test_thorlabs/test_thorlabs_sc10.py @@ -1,15 +1,13 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing tests for the Thorlabs SC10 """ # IMPORTS #################################################################### -from __future__ import absolute_import -from nose.tools import raises -import quantities as pq +import pytest +from instruments.units import ureg as u import instruments as ik from instruments.tests import expected_protocol, unit_eq @@ -19,76 +17,37 @@ def test_sc10_name(): with expected_protocol( - ik.thorlabs.SC10, - [ - "id?" - ], - [ - "id?", - "bloopbloop", - ">" - ], - sep="\r" + ik.thorlabs.SC10, ["id?"], ["id?", "bloopbloop", "> "], sep="\r" ) as sc: assert sc.name == "bloopbloop" def test_sc10_enable(): with expected_protocol( - ik.thorlabs.SC10, - [ - "ens?", - "ens=1" - ], - [ - "ens?", - "0", - ">ens=1", - ">" - ], - sep="\r" + ik.thorlabs.SC10, ["ens?", "ens=1"], ["ens?", "0", "> ens=1", "> "], sep="\r" ) as sc: assert sc.enable is False sc.enable = True -@raises(TypeError) def test_sc10_enable_invalid(): - with expected_protocol( - ik.thorlabs.SC10, - [], - [], - sep="\r" + with pytest.raises(TypeError), expected_protocol( + ik.thorlabs.SC10, [], [], sep="\r" ) as sc: sc.enable = 10 def test_sc10_repeat(): with expected_protocol( - ik.thorlabs.SC10, - [ - "rep?", - "rep=10" - ], - [ - "rep?", - "20", - ">rep=10", - ">" - ], - sep="\r" + ik.thorlabs.SC10, ["rep?", "rep=10"], ["rep?", "20", "> rep=10", "> "], sep="\r" ) as sc: assert sc.repeat == 20 sc.repeat = 10 -@raises(ValueError) def test_sc10_repeat_invalid(): - with expected_protocol( - ik.thorlabs.SC10, - [], - [], - sep="\r" + with pytest.raises(ValueError), expected_protocol( + ik.thorlabs.SC10, [], [], sep="\r" ) as sc: sc.repeat = -1 @@ -96,29 +55,17 @@ def test_sc10_repeat_invalid(): def test_sc10_mode(): with expected_protocol( ik.thorlabs.SC10, - [ - "mode?", - "mode=2" - ], - [ - "mode?", - "1", - ">mode=2", - ">" - ], - sep="\r" + ["mode?", "mode=2"], + ["mode?", "1", "> mode=2", "> "], + sep="\r", ) as sc: assert sc.mode == ik.thorlabs.SC10.Mode.manual sc.mode = ik.thorlabs.SC10.Mode.auto -@raises(ValueError) def test_sc10_mode_invalid(): - with expected_protocol( - ik.thorlabs.SC10, - [], - [], - sep="\r" + with pytest.raises(ValueError), expected_protocol( + ik.thorlabs.SC10, [], [], sep="\r" ) as sc: sc.mode = "blo" @@ -126,17 +73,9 @@ def test_sc10_mode_invalid(): def test_sc10_trigger(): with expected_protocol( ik.thorlabs.SC10, - [ - "trig?", - "trig=1" - ], - [ - "trig?", - "0", - ">trig=1", - ">" - ], - sep="\r" + ["trig?", "trig=1"], + ["trig?", "0", "> trig=1", "> "], + sep="\r", ) as sc: assert sc.trigger == 0 sc.trigger = 1 @@ -144,18 +83,7 @@ def test_sc10_trigger(): def test_sc10_out_trigger(): with expected_protocol( - ik.thorlabs.SC10, - [ - "xto?", - "xto=1" - ], - [ - "xto?", - "0", - ">xto=1", - ">" - ], - sep="\r" + ik.thorlabs.SC10, ["xto?", "xto=1"], ["xto?", "0", "> xto=1", "> "], sep="\r" ) as sc: assert sc.out_trigger == 0 sc.out_trigger = 1 @@ -164,162 +92,80 @@ def test_sc10_out_trigger(): def test_sc10_open_time(): with expected_protocol( ik.thorlabs.SC10, - [ - "open?", - "open=10" - ], - [ - "open?", - "20", - ">open=10", - ">" - ], - sep="\r" + ["open?", "open=10"], + ["open?", "20", "> open=10", "> "], + sep="\r", ) as sc: - unit_eq(sc.open_time, pq.Quantity(20, "ms")) + unit_eq(sc.open_time, u.Quantity(20, "ms")) sc.open_time = 10 def test_sc10_shut_time(): with expected_protocol( ik.thorlabs.SC10, - [ - "shut?", - "shut=10" - ], - [ - "shut?", - "20", - ">shut=10", - ">" - ], - sep="\r" + ["shut?", "shut=10"], + ["shut?", "20", "> shut=10", "> "], + sep="\r", ) as sc: - unit_eq(sc.shut_time, pq.Quantity(20, "ms")) + unit_eq(sc.shut_time, u.Quantity(20, "ms")) sc.shut_time = 10.0 def test_sc10_baud_rate(): with expected_protocol( ik.thorlabs.SC10, - [ - "baud?", - "baud=1" - ], - [ - "baud?", - "0", - ">baud=1", - ">" - ], - sep="\r" + ["baud?", "baud=1"], + ["baud?", "0", "> baud=1", "> "], + sep="\r", ) as sc: assert sc.baud_rate == 9600 sc.baud_rate = 115200 -@raises(ValueError) def test_sc10_baud_rate_error(): - with expected_protocol( - ik.thorlabs.SC10, - [], - [], - sep="\r" + with pytest.raises(ValueError), expected_protocol( + ik.thorlabs.SC10, [], [], sep="\r" ) as sc: sc.baud_rate = 115201 def test_sc10_closed(): with expected_protocol( - ik.thorlabs.SC10, - [ - "closed?" - ], - [ - "closed?", - "1", - ">" - ], - sep="\r" + ik.thorlabs.SC10, ["closed?"], ["closed?", "1", "> "], sep="\r" ) as sc: assert sc.closed def test_sc10_interlock(): with expected_protocol( - ik.thorlabs.SC10, - [ - "interlock?" - ], - [ - "interlock?", - "1", - ">" - ], - sep="\r" + ik.thorlabs.SC10, ["interlock?"], ["interlock?", "1", "> "], sep="\r" ) as sc: assert sc.interlock def test_sc10_default(): with expected_protocol( - ik.thorlabs.SC10, - [ - "default" - ], - [ - "default", - "1", - ">" - ], - sep="\r" + ik.thorlabs.SC10, ["default"], ["default", "1", "> "], sep="\r" ) as sc: assert sc.default() def test_sc10_save(): with expected_protocol( - ik.thorlabs.SC10, - [ - "savp" - ], - [ - "savp", - "1", - ">" - ], - sep="\r" + ik.thorlabs.SC10, ["savp"], ["savp", "1", "> "], sep="\r" ) as sc: assert sc.save() def test_sc10_save_mode(): with expected_protocol( - ik.thorlabs.SC10, - [ - "save" - ], - [ - "save", - "1", - ">" - ], - sep="\r" + ik.thorlabs.SC10, ["save"], ["save", "1", "> "], sep="\r" ) as sc: assert sc.save_mode() def test_sc10_restore(): with expected_protocol( - ik.thorlabs.SC10, - [ - "resp" - ], - [ - "resp", - "1", - ">" - ], - sep="\r" + ik.thorlabs.SC10, ["resp"], ["resp", "1", "> "], sep="\r" ) as sc: assert sc.restore() diff --git a/instruments/tests/test_thorlabs/test_thorlabs_tc200.py b/instruments/tests/test_thorlabs/test_thorlabs_tc200.py index 350d8ed5e..a9084db48 100644 --- a/instruments/tests/test_thorlabs/test_thorlabs_tc200.py +++ b/instruments/tests/test_thorlabs/test_thorlabs_tc200.py @@ -1,16 +1,14 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing tests for the Thorlabs TC200 """ # IMPORTS #################################################################### -from __future__ import absolute_import from enum import IntEnum -from nose.tools import raises -import quantities as pq +import pytest +from instruments.units import ureg as u import instruments as ik from instruments.tests import expected_protocol @@ -20,16 +18,7 @@ def test_tc200_name(): with expected_protocol( - ik.thorlabs.TC200, - [ - "*idn?" - ], - [ - "*idn?", - "bloopbloop", - "> " - ], - sep="\r" + ik.thorlabs.TC200, ["*idn?"], ["*idn?", "bloopbloop", "> "], sep="\r" ) as tc: assert tc.name() == "bloopbloop" @@ -37,18 +26,9 @@ def test_tc200_name(): def test_tc200_mode(): with expected_protocol( ik.thorlabs.TC200, - [ - "stat?", - "stat?", - "mode=cycle" - ], - [ - "stat?", - "0 > stat?", - "2 > mode=cycle", - "> " - ], - sep="\r" + ["stat?", "stat?", "mode=cycle"], + ["stat?", "0 > stat?", "2 > mode=cycle", "> "], + sep="\r", ) as tc: assert tc.mode == tc.Mode.normal assert tc.mode == tc.Mode.cycle @@ -58,76 +38,47 @@ def test_tc200_mode(): def test_tc200_mode_2(): with expected_protocol( ik.thorlabs.TC200, - [ - "mode=normal" - ], - [ - "mode=normal", - "Command error CMD_ARG_RANGE_ERR\n", - "> " - ], - sep="\r" + ["mode=normal"], + ["mode=normal", "Command error CMD_ARG_RANGE_ERR\n", "> "], + sep="\r", ) as tc: tc.mode = ik.thorlabs.TC200.Mode.normal -@raises(TypeError) def test_tc200_mode_error(): - with expected_protocol( - ik.thorlabs.TC200, - [], - [], - sep="\r" + with pytest.raises(TypeError), expected_protocol( + ik.thorlabs.TC200, [], [], sep="\r" ) as tc: tc.mode = "blo" -@raises(TypeError) def test_tc200_mode_error2(): - with expected_protocol( - ik.thorlabs.TC200, - [], - [], - sep="\r" + with pytest.raises(TypeError), expected_protocol( + ik.thorlabs.TC200, [], [], sep="\r" ) as tc: + class TestEnum(IntEnum): blo = 1 beep = 2 + tc.mode = TestEnum.blo def test_tc200_enable(): with expected_protocol( ik.thorlabs.TC200, - [ - "stat?", - "stat?", - "ens", - "stat?", - "ens" - ], - [ - "stat?", - "54 > stat?", - "54 > ens", - "> stat?", - "55 > ens", - "> " - ], - sep="\r" + ["stat?", "stat?", "ens", "stat?", "ens"], + ["stat?", "54 > stat?", "54 > ens", "> stat?", "55 > ens", "> "], + sep="\r", ) as tc: assert tc.enable == 0 tc.enable = True tc.enable = False -@raises(TypeError) def test_tc200_enable_type(): - with expected_protocol( - ik.thorlabs.TC200, - [], - [], - sep="\r" + with pytest.raises(TypeError), expected_protocol( + ik.thorlabs.TC200, [], [], sep="\r" ) as tc: tc.enable = "blo" @@ -143,227 +94,112 @@ def test_tc200_temperature(): "30 C", "> ", ], - sep="\r" + sep="\r", ) as tc: - assert tc.temperature == 30.0 * pq.degC + assert tc.temperature == u.Quantity(30.0, u.degC) def test_tc200_temperature_set(): with expected_protocol( ik.thorlabs.TC200, - [ - "tset?", - "tmax?", - "tset=40.0" - ], - [ - "tset?", - "30 C", - "> tmax?", - "250", - "> tset=40.0", - "> " - ], - sep="\r" + ["tset?", "tmax?", "tset=40"], + ["tset?", "30 C", "> tmax?", "250", "> tset=40", "> "], + sep="\r", ) as tc: - assert tc.temperature_set == 30.0 * pq.degC - tc.temperature_set = 40 * pq.degC + assert tc.temperature_set == u.Quantity(30.0, u.degC) + tc.temperature_set = u.Quantity(40, u.degC) -@raises(ValueError) def test_tc200_temperature_range(): - with expected_protocol( - ik.thorlabs.TC200, - [ - "tmax?" - ], - [ - "tmax?", - "40", - "> " - ], - sep="\r" + with pytest.raises(ValueError), expected_protocol( + ik.thorlabs.TC200, ["tmax?"], ["tmax?", "40", "> "], sep="\r" ) as tc: - tc.temperature_set = 50 * pq.degC + tc.temperature_set = u.Quantity(50, u.degC) def test_tc200_pid(): with expected_protocol( ik.thorlabs.TC200, - [ - "pid?", - "pgain=2" - ], - [ - "pid?", - "2 0 220", - "> pgain=2", - "> " - ], - sep="\r" + ["pid?", "pgain=2"], + ["pid?", "2 0 220", "> pgain=2", "> "], + sep="\r", ) as tc: assert tc.p == 2 tc.p = 2 with expected_protocol( ik.thorlabs.TC200, - [ - "pid?", - "igain=0" - ], - [ - "pid?", - "2 0 220", - "> igain=0", - "> " - ], - sep="\r" + ["pid?", "igain=0"], + ["pid?", "2 0 220", "> igain=0", "> "], + sep="\r", ) as tc: assert tc.i == 0 tc.i = 0 with expected_protocol( ik.thorlabs.TC200, - [ - "pid?", - "dgain=220" - ], - [ - "pid?", - "2 0 220", - "> dgain=220", - "> " - ], - sep="\r" + ["pid?", "dgain=220"], + ["pid?", "2 0 220", "> dgain=220", "> "], + sep="\r", ) as tc: assert tc.d == 220 tc.d = 220 with expected_protocol( ik.thorlabs.TC200, - [ - "pid?", - "pgain=2", - "igain=0", - "dgain=220" - ], - [ - "pid?", - "2 0 220", - "> pgain=2", - "> igain=0", - "> dgain=220", - "> " - ], - sep="\r" + ["pid?", "pgain=2", "igain=0", "dgain=220"], + ["pid?", "2 0 220", "> pgain=2", "> igain=0", "> dgain=220", "> "], + sep="\r", ) as tc: assert tc.pid == [2, 0, 220] tc.pid = (2, 0, 220) -@raises(TypeError) def test_tc200_pid_invalid_type(): - with expected_protocol( - ik.thorlabs.TC200, - [], - [], - sep="\r" + with pytest.raises(TypeError), expected_protocol( + ik.thorlabs.TC200, [], [], sep="\r" ) as tc: tc.pid = "foo" -@raises(ValueError) def test_tc200_pmin(): - with expected_protocol( - ik.thorlabs.TC200, - [ - "pgain=-1" - ], - [ - "pgain=-1", - "> " - ], - sep="\r" + with pytest.raises(ValueError), expected_protocol( + ik.thorlabs.TC200, ["pgain=-1"], ["pgain=-1", "> "], sep="\r" ) as tc: tc.p = -1 -@raises(ValueError) def test_tc200_pmax(): - with expected_protocol( - ik.thorlabs.TC200, - [ - "pgain=260" - ], - [ - "pgain=260", - "> " - ], - sep="\r" + with pytest.raises(ValueError), expected_protocol( + ik.thorlabs.TC200, ["pgain=260"], ["pgain=260", "> "], sep="\r" ) as tc: tc.p = 260 -@raises(ValueError) def test_tc200_imin(): - with expected_protocol( - ik.thorlabs.TC200, - [ - "igain=-1" - ], - [ - "igain=-1", - "> " - ], - sep="\r" + with pytest.raises(ValueError), expected_protocol( + ik.thorlabs.TC200, ["igain=-1"], ["igain=-1", "> "], sep="\r" ) as tc: tc.i = -1 -@raises(ValueError) def test_tc200_imax(): - with expected_protocol( - ik.thorlabs.TC200, - [ - "igain=260" - ], - [ - "igain=260", - "> " - ], - sep="\r" + with pytest.raises(ValueError), expected_protocol( + ik.thorlabs.TC200, ["igain=260"], ["igain=260", "> "], sep="\r" ) as tc: tc.i = 260 -@raises(ValueError) def test_tc200_dmin(): - with expected_protocol( - ik.thorlabs.TC200, - [ - "dgain=-1" - ], - [ - "dgain=-1", - "> " - ], - sep="\r" + with pytest.raises(ValueError), expected_protocol( + ik.thorlabs.TC200, ["dgain=-1"], ["dgain=-1", "> "], sep="\r" ) as tc: tc.d = -1 -@raises(ValueError) def test_tc200_dmax(): - with expected_protocol( - ik.thorlabs.TC200, - [ - "dgain=260" - ], - [ - "dgain=260", - "> " - ], - sep="\r" + with pytest.raises(ValueError), expected_protocol( + ik.thorlabs.TC200, ["dgain=260"], ["dgain=260", "> "], sep="\r" ) as tc: tc.d = 260 @@ -371,14 +207,7 @@ def test_tc200_dmax(): def test_tc200_degrees(): with expected_protocol( ik.thorlabs.TC200, - [ - "stat?", - "stat?", - "stat?", - "unit=c", - "unit=f", - "unit=k" - ], + ["stat?", "stat?", "stat?", "unit=c", "unit=f", "unit=k"], [ "stat?", "44 > stat?", @@ -386,27 +215,22 @@ def test_tc200_degrees(): "0 > unit=c", "> unit=f", "> unit=k", - "> " + "> ", ], - sep="\r" + sep="\r", ) as tc: - assert str(tc.degrees).split(" ")[1] == "K" - assert str(tc.degrees).split(" ")[1] == "degC" - assert tc.degrees == pq.degF + assert tc.degrees == u.degK + assert tc.degrees == u.degC + assert tc.degrees == u.degF - tc.degrees = pq.degC - tc.degrees = pq.degF - tc.degrees = pq.degK + tc.degrees = u.degC + tc.degrees = u.degF + tc.degrees = u.degK -@raises(TypeError) def test_tc200_degrees_invalid(): - - with expected_protocol( - ik.thorlabs.TC200, - [], - [], - sep="\r" + with pytest.raises(TypeError), expected_protocol( + ik.thorlabs.TC200, [], [], sep="\r" ) as tc: tc.degrees = "blo" @@ -414,92 +238,50 @@ def test_tc200_degrees_invalid(): def test_tc200_sensor(): with expected_protocol( ik.thorlabs.TC200, - [ - "sns?", - "sns=ptc100" - ], - [ - "sns?", - "Sensor = NTC10K, Beta = 5600", - "> sns=ptc100", - "> " - ], - sep="\r" + ["sns?", "sns=ptc100"], + ["sns?", "Sensor = NTC10K, Beta = 5600", "> sns=ptc100", "> "], + sep="\r", ) as tc: assert tc.sensor == tc.Sensor.ntc10k tc.sensor = tc.Sensor.ptc100 -@raises(ValueError) def test_tc200_sensor_error(): - with expected_protocol( - ik.thorlabs.TC200, - [], - [] - ) as tc: + with pytest.raises(ValueError), expected_protocol(ik.thorlabs.TC200, [], []) as tc: tc.sensor = "blo" -@raises(ValueError) def test_tc200_sensor_error2(): - with expected_protocol( - ik.thorlabs.TC200, - [], - [] - ) as tc: + with pytest.raises(ValueError), expected_protocol(ik.thorlabs.TC200, [], []) as tc: + class TestEnum(IntEnum): blo = 1 beep = 2 + tc.sensor = TestEnum.blo def test_tc200_beta(): with expected_protocol( ik.thorlabs.TC200, - [ - "beta?", - "beta=2000" - ], - [ - "beta?", - "5600", - "> beta=2000", - "> " - ], - sep="\r" + ["beta?", "beta=2000"], + ["beta?", "5600", "> beta=2000", "> "], + sep="\r", ) as tc: assert tc.beta == 5600 tc.beta = 2000 -@raises(ValueError) def test_tc200_beta_min(): - with expected_protocol( - ik.thorlabs.TC200, - [ - "beta=200" - ], - [ - "beta=200", - "> " - ], - sep="\r" + with pytest.raises(ValueError), expected_protocol( + ik.thorlabs.TC200, ["beta=200"], ["beta=200", "> "], sep="\r" ) as tc: tc.beta = 200 -@raises(ValueError) def test_tc200_beta_max(): - with expected_protocol( - ik.thorlabs.TC200, - [ - "beta=20000" - ], - [ - "beta=20000", - "> " - ], - sep="\r" + with pytest.raises(ValueError), expected_protocol( + ik.thorlabs.TC200, ["beta=20000"], ["beta=20000", "> "], sep="\r" ) as tc: tc.beta = 20000 @@ -507,50 +289,24 @@ def test_tc200_beta_max(): def test_tc200_max_power(): with expected_protocol( ik.thorlabs.TC200, - [ - "pmax?", - "pmax=12.0" - ], - [ - "pmax?", - "15.0", - "> pmax=12.0", - "> " - ], - sep="\r" + ["pmax?", "pmax=12.0"], + ["pmax?", "15.0", "> pmax=12.0", "> "], + sep="\r", ) as tc: - assert tc.max_power == 15.0 * pq.W - tc.max_power = 12 * pq.W + assert tc.max_power == 15.0 * u.W + tc.max_power = 12 * u.W -@raises(ValueError) def test_tc200_power_min(): - with expected_protocol( - ik.thorlabs.TC200, - [ - "PMAX=-2" - ], - [ - "PMAX=-2", - "> " - ], - sep="\r" + with pytest.raises(ValueError), expected_protocol( + ik.thorlabs.TC200, ["PMAX=-2"], ["PMAX=-2", "> "], sep="\r" ) as tc: tc.max_power = -1 -@raises(ValueError) def test_tc200_power_max(): - with expected_protocol( - ik.thorlabs.TC200, - [ - "PMAX=20000" - ], - [ - "PMAX=20000", - "> " - ], - sep="\r" + with pytest.raises(ValueError), expected_protocol( + ik.thorlabs.TC200, ["PMAX=20000"], ["PMAX=20000", "> "], sep="\r" ) as tc: tc.max_power = 20000 @@ -558,49 +314,23 @@ def test_tc200_power_max(): def test_tc200_max_temperature(): with expected_protocol( ik.thorlabs.TC200, - [ - "tmax?", - "tmax=180.0" - ], - [ - "tmax?", - "200.0", - "> tmax=180.0", - "> " - ], - sep="\r" + ["tmax?", "tmax=180.0"], + ["tmax?", "200.0", "> tmax=180.0", "> "], + sep="\r", ) as tc: - assert tc.max_temperature == 200.0 * pq.degC - tc.max_temperature = 180 * pq.degC + assert tc.max_temperature == u.Quantity(200.0, u.degC) + tc.max_temperature = u.Quantity(180, u.degC) -@raises(ValueError) def test_tc200_temp_min(): - with expected_protocol( - ik.thorlabs.TC200, - [ - "TMAX=-2" - ], - [ - "TMAX=-2", - ">" - ], - sep="\r" + with pytest.raises(ValueError), expected_protocol( + ik.thorlabs.TC200, ["TMAX=-2"], ["TMAX=-2", ">"], sep="\r" ) as tc: tc.max_temperature = -1 -@raises(ValueError) def test_tc200_temp_max(): - with expected_protocol( - ik.thorlabs.TC200, - [ - "TMAX=20000" - ], - [ - "TMAX=20000", - ">" - ], - sep="\r" + with pytest.raises(ValueError), expected_protocol( + ik.thorlabs.TC200, ["TMAX=20000"], ["TMAX=20000", ">"], sep="\r" ) as tc: tc.max_temperature = 20000 diff --git a/instruments/tests/test_thorlabs/test_utils.py b/instruments/tests/test_thorlabs/test_utils.py index 2752b9095..b876b3899 100644 --- a/instruments/tests/test_thorlabs/test_utils.py +++ b/instruments/tests/test_thorlabs/test_utils.py @@ -1,12 +1,10 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing tests for the Thorlabs util functions """ # IMPORTS #################################################################### -from __future__ import absolute_import import instruments as ik diff --git a/instruments/tests/test_toptica/test_toptica_topmode.py b/instruments/tests/test_toptica/test_toptica_topmode.py index 1833b4a66..57eb9911f 100644 --- a/instruments/tests/test_toptica/test_toptica_topmode.py +++ b/instruments/tests/test_toptica/test_toptica_topmode.py @@ -1,15 +1,13 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing tests for the Toptica Topmode """ # IMPORTS ##################################################################### -from __future__ import absolute_import from datetime import datetime -from nose.tools import raises -import quantities as pq +import pytest +from instruments.units import ureg as u import instruments as ik @@ -21,18 +19,15 @@ def test_laser_serial_number(): with expected_protocol( ik.toptica.TopMode, - [ - "(param-ref 'laser1:serial-number)", - "(param-ref 'laser2:serial-number)" - ], + ["(param-ref 'laser1:serial-number)", "(param-ref 'laser2:serial-number)"], [ "(param-ref 'laser1:serial-number)", "bloop1", "> (param-ref 'laser2:serial-number)", "bloop2", - "> " + "> ", ], - sep="\r\n" + sep="\r\n", ) as tm: assert tm.laser[0].serial_number == "bloop1" assert tm.laser[1].serial_number == "bloop2" @@ -41,18 +36,15 @@ def test_laser_serial_number(): def test_model(): with expected_protocol( ik.toptica.TopMode, - [ - "(param-ref 'laser1:model)", - "(param-ref 'laser2:model)" - ], + ["(param-ref 'laser1:model)", "(param-ref 'laser2:model)"], [ "(param-ref 'laser1:model)", "bloop1", "> (param-ref 'laser2:model)", "bloop2", - "> " + "> ", ], - sep="\r\n" + sep="\r\n", ) as tm: assert tm.laser[0].model == "bloop1" assert tm.laser[1].model == "bloop2" @@ -61,21 +53,18 @@ def test_model(): def test_wavelength(): with expected_protocol( ik.toptica.TopMode, - [ - "(param-ref 'laser1:wavelength)", - "(param-ref 'laser2:wavelength)" - ], + ["(param-ref 'laser1:wavelength)", "(param-ref 'laser2:wavelength)"], [ "(param-ref 'laser1:wavelength)", "640", "> (param-ref 'laser2:wavelength)", "405.3", - "> " + "> ", ], - sep="\r\n" + sep="\r\n", ) as tm: - assert tm.laser[0].wavelength == 640 * pq.nm - assert tm.laser[1].wavelength == 405.3 * pq.nm + assert tm.laser[0].wavelength == 640 * u.nm + assert tm.laser[1].wavelength == 405.3 * u.nm def test_laser_enable(): @@ -84,7 +73,7 @@ def test_laser_enable(): [ "(param-ref 'laser1:emission)", "(param-ref 'laser1:serial-number)", - "(param-set! 'laser1:enable-emission #t)" + "(param-set! 'laser1:enable-emission #t)", ], [ "(param-ref 'laser1:emission)", @@ -93,66 +82,58 @@ def test_laser_enable(): "bloop1", "> (param-set! 'laser1:enable-emission #t)", "0", - "> " + "> ", ], - sep="\r\n" + sep="\r\n", ) as tm: assert tm.laser[0].enable is False tm.laser[0].enable = True -@raises(RuntimeError) def test_laser_enable_no_laser(): - with expected_protocol( + with pytest.raises(RuntimeError), expected_protocol( ik.toptica.TopMode, [ "(param-ref 'laser1:serial-number)", - "(param-set! 'laser1:enable-emission #t)" + "(param-set! 'laser1:enable-emission #t)", ], [ "(param-ref 'laser1:serial-number)", "unknown", "> (param-set! 'laser1:enable-emission #t)", "0", - "> " + "> ", ], - sep="\r\n" + sep="\r\n", ) as tm: tm.laser[0].enable = True -@raises(TypeError) def test_laser_enable_error(): - with expected_protocol( + with pytest.raises(TypeError), expected_protocol( ik.toptica.TopMode, [ "(param-ref 'laser1:serial-number)", - "(param-set! 'laser1:enable-emission #t)" + "(param-set! 'laser1:enable-emission #t)", ], [ "(param-ref 'laser1:serial-number)", "bloop1", "> (param-set! 'laser1:enable-emission #t)", "0", - "> " + "> ", ], - sep="\n" + sep="\n", ) as tm: - tm.laser[0].enable = 'True' + tm.laser[0].enable = "True" def test_laser_tec_status(): with expected_protocol( ik.toptica.TopMode, - [ - "(param-ref 'laser1:tec:ready)" - ], - [ - "(param-ref 'laser1:tec:ready)", - "#f", - "> " - ], - sep="\r\n" + ["(param-ref 'laser1:tec:ready)"], + ["(param-ref 'laser1:tec:ready)", "#f", "> "], + sep="\r\n", ) as tm: assert tm.laser[0].tec_status is False @@ -160,15 +141,9 @@ def test_laser_tec_status(): def test_laser_intensity(): with expected_protocol( ik.toptica.TopMode, - [ - "(param-ref 'laser1:intensity)" - ], - [ - "(param-ref 'laser1:intensity)", - "0.666", - "> " - ], - sep="\r\n" + ["(param-ref 'laser1:intensity)"], + ["(param-ref 'laser1:intensity)", "0.666", "> "], + sep="\r\n", ) as tm: assert tm.laser[0].intensity == 0.666 @@ -176,15 +151,9 @@ def test_laser_intensity(): def test_laser_mode_hop(): with expected_protocol( ik.toptica.TopMode, - [ - "(param-ref 'laser1:charm:reg:mh-occurred)" - ], - [ - "(param-ref 'laser1:charm:reg:mh-occurred)", - "#f", - "> " - ], - sep="\r\n" + ["(param-ref 'laser1:charm:reg:mh-occurred)"], + ["(param-ref 'laser1:charm:reg:mh-occurred)", "#f", "> "], + sep="\r\n", ) as tm: assert tm.laser[0].mode_hop is False @@ -194,57 +163,56 @@ def test_laser_lock_start(): ik.toptica.TopMode, [ "(param-ref 'laser1:charm:correction-status)", - "(param-ref 'laser1:charm:reg:started)" + "(param-ref 'laser1:charm:reg:started)", ], [ "(param-ref 'laser1:charm:correction-status)", "2", "> (param-ref 'laser1:charm:reg:started)", - "\"2012-12-01 01:02:01\"", - "> " + '"2012-12-01 01:02:01"', + "> ", ], - sep="\r\n" + sep="\r\n", ) as tm: _date = datetime(2012, 12, 1, 1, 2, 1) assert tm.laser[0].lock_start == _date -@raises(RuntimeError) + def test_laser_lock_start_runtime_error(): - with expected_protocol( + with pytest.raises(RuntimeError), expected_protocol( ik.toptica.TopMode, [ "(param-ref 'laser1:charm:correction-status)", - "(param-ref 'laser1:charm:reg:started)" + "(param-ref 'laser1:charm:reg:started)", ], [ "(param-ref 'laser1:charm:correction-status)", "0", "> (param-ref 'laser1:charm:reg:started)", - "\"\"", - "> " + '""', + "> ", ], - sep="\r\n" + sep="\r\n", ) as tm: _date = datetime(2012, 12, 1, 1, 2, 1) assert tm.laser[0].lock_start == _date -@raises(RuntimeError) def test_laser_first_mode_hop_time_runtime_error(): - with expected_protocol( + with pytest.raises(RuntimeError), expected_protocol( ik.toptica.TopMode, [ "(param-ref 'laser1:charm:reg:mh-occurred)", - "(param-ref 'laser1:charm:reg:first-mh)" + "(param-ref 'laser1:charm:reg:first-mh)", ], [ "(param-ref 'laser1:charm:reg:mh-occurred)", "#f", "> (param-ref 'laser1:charm:reg:first-mh)", - "\"\"", - "> " + '""', + "> ", ], - sep="\r\n" + sep="\r\n", ) as tm: assert tm.laser[0].first_mode_hop_time is None @@ -254,37 +222,36 @@ def test_laser_first_mode_hop_time(): ik.toptica.TopMode, [ "(param-ref 'laser1:charm:reg:mh-occurred)", - "(param-ref 'laser1:charm:reg:first-mh)" + "(param-ref 'laser1:charm:reg:first-mh)", ], [ "(param-ref 'laser1:charm:reg:mh-occurred)", "#t", "> (param-ref 'laser1:charm:reg:first-mh)", - "\"2012-12-01 01:02:01\"", - "> " + '"2012-12-01 01:02:01"', + "> ", ], - sep="\r\n" + sep="\r\n", ) as tm: _date = datetime(2012, 12, 1, 1, 2, 1) assert tm.laser[0].first_mode_hop_time == _date -@raises(RuntimeError) def test_laser_latest_mode_hop_time_none(): - with expected_protocol( + with pytest.raises(RuntimeError), expected_protocol( ik.toptica.TopMode, [ "(param-ref 'laser1:charm:reg:mh-occurred)", - "(param-ref 'laser1:charm:reg:latest-mh)" + "(param-ref 'laser1:charm:reg:latest-mh)", ], [ "(param-ref 'laser1:charm:reg:mh-occurred)", "#f", "> (param-ref 'laser1:charm:reg:latest-mh)", - "\"\"", - "> " + '""', + "> ", ], - sep="\r\n" + sep="\r\n", ) as tm: assert tm.laser[0].latest_mode_hop_time is None @@ -294,16 +261,16 @@ def test_laser_latest_mode_hop_time(): ik.toptica.TopMode, [ "(param-ref 'laser1:charm:reg:mh-occurred)", - "(param-ref 'laser1:charm:reg:latest-mh)" + "(param-ref 'laser1:charm:reg:latest-mh)", ], [ "(param-ref 'laser1:charm:reg:mh-occurred)", "#t", "> (param-ref 'laser1:charm:reg:latest-mh)", - "\"2012-12-01 01:02:01\"", - "> " + '"2012-12-01 01:02:01"', + "> ", ], - sep="\r\n" + sep="\r\n", ) as tm: _date = datetime(2012, 12, 1, 1, 2, 1) assert tm.laser[0].latest_mode_hop_time == _date @@ -312,18 +279,14 @@ def test_laser_latest_mode_hop_time(): def test_laser_correction_status(): with expected_protocol( ik.toptica.TopMode, - [ - "(param-ref 'laser1:charm:correction-status)" - ], - [ - "(param-ref 'laser1:charm:correction-status)", - "0", - "> " - ], - sep="\r\n" + ["(param-ref 'laser1:charm:correction-status)"], + ["(param-ref 'laser1:charm:correction-status)", "0", "> "], + sep="\r\n", ) as tm: - assert tm.laser[ - 0].correction_status == ik.toptica.TopMode.CharmStatus.un_initialized + assert ( + tm.laser[0].correction_status + == ik.toptica.TopMode.CharmStatus.un_initialized + ) def test_laser_correction(): @@ -336,7 +299,7 @@ def test_laser_correction(): "(exec 'laser1:charm:start-correction)", "(param-ref 'laser1:charm:correction-status)", # 3rd "(param-ref 'laser1:charm:correction-status)", # 4th - "(exec 'laser1:charm:start-correction)" + "(exec 'laser1:charm:start-correction)", ], [ "(param-ref 'laser1:charm:correction-status)", # 1st @@ -355,7 +318,7 @@ def test_laser_correction(): "()", "> ", ], - sep="\r\n" + sep="\r\n", ) as tm: tm.laser[0].correction() tm.laser[0].correction() @@ -366,15 +329,9 @@ def test_laser_correction(): def test_reboot_system(): with expected_protocol( ik.toptica.TopMode, - [ - "(exec 'reboot-system)" - ], - [ - "(exec 'reboot-system)", - "reboot process started.", - "> " - ], - sep="\r\n" + ["(exec 'reboot-system)"], + ["(exec 'reboot-system)", "reboot process started.", "> "], + sep="\r\n", ) as tm: tm.reboot() @@ -382,31 +339,19 @@ def test_reboot_system(): def test_laser_ontime(): with expected_protocol( ik.toptica.TopMode, - [ - "(param-ref 'laser1:ontime)" - ], - [ - "(param-ref 'laser1:ontime)", - "10000", - "> " - ], - sep="\r\n" + ["(param-ref 'laser1:ontime)"], + ["(param-ref 'laser1:ontime)", "10000", "> "], + sep="\r\n", ) as tm: - assert tm.laser[0].on_time == 10000 * pq.s + assert tm.laser[0].on_time == 10000 * u.s def test_laser_charm_status(): with expected_protocol( ik.toptica.TopMode, - [ - "(param-ref 'laser1:health)" - ], - [ - "(param-ref 'laser1:health)", - "230", - "> " - ], - sep="\r\n" + ["(param-ref 'laser1:health)"], + ["(param-ref 'laser1:health)", "230", "> "], + sep="\r\n", ) as tm: assert tm.laser[0].charm_status == 1 @@ -414,15 +359,9 @@ def test_laser_charm_status(): def test_laser_temperature_control_status(): with expected_protocol( ik.toptica.TopMode, - [ - "(param-ref 'laser1:health)" - ], - [ - "(param-ref 'laser1:health)", - "230", - "> " - ], - sep="\r\n" + ["(param-ref 'laser1:health)"], + ["(param-ref 'laser1:health)", "230", "> "], + sep="\r\n", ) as tm: assert tm.laser[0].temperature_control_status == 1 @@ -430,15 +369,9 @@ def test_laser_temperature_control_status(): def test_laser_current_control_status(): with expected_protocol( ik.toptica.TopMode, - [ - "(param-ref 'laser1:health)" - ], - [ - "(param-ref 'laser1:health)", - "230", - "> " - ], - sep="\r\n" + ["(param-ref 'laser1:health)"], + ["(param-ref 'laser1:health)", "230", "> "], + sep="\r\n", ) as tm: assert tm.laser[0].current_control_status == 1 @@ -446,15 +379,9 @@ def test_laser_current_control_status(): def test_laser_production_date(): with expected_protocol( ik.toptica.TopMode, - [ - "(param-ref 'laser1:production-date)" - ], - [ - "(param-ref 'laser1:production-date)", - "2016-01-16", - "> " - ], - sep="\r\n" + ["(param-ref 'laser1:production-date)"], + ["(param-ref 'laser1:production-date)", "2016-01-16", "> "], + sep="\r\n", ) as tm: assert tm.laser[0].production_date == "2016-01-16" @@ -462,66 +389,45 @@ def test_laser_production_date(): def test_set_str(): with expected_protocol( ik.toptica.TopMode, - [ - "(param-set! 'blo \"blee\")" - ], - [ - "(param-set! 'blo \"blee\")", - "0", - "> " - ], - sep="\r\n" + ['(param-set! \'blo "blee")'], + ['(param-set! \'blo "blee")', "0", "> "], + sep="\r\n", ) as tm: - tm.set('blo', 'blee') + tm.set("blo", "blee") def test_set_list(): with expected_protocol( ik.toptica.TopMode, - [ - "(param-set! 'blo '(blee blo))" - ], - [ - "(param-set! 'blo '(blee blo))", - "0", - "> " - ], - sep="\r\n" + ["(param-set! 'blo '(blee blo))"], + ["(param-set! 'blo '(blee blo))", "0", "> "], + sep="\r\n", ) as tm: - tm.set('blo', ['blee', 'blo']) + tm.set("blo", ["blee", "blo"]) def test_display(): with expected_protocol( ik.toptica.TopMode, - [ - "(param-disp 'blo)" - ], - [ - "(param-disp 'blo)", - "bloop", - "> " - ], - sep="\r\n" + ["(param-disp 'blo)"], + ["(param-disp 'blo)", "bloop", "> "], + sep="\r\n", ) as tm: - assert tm.display('blo') == "bloop" + assert tm.display("blo") == "bloop" def test_enable(): with expected_protocol( ik.toptica.TopMode, - [ - "(param-ref 'emission)", - "(param-set! 'enable-emission #f)" - ], + ["(param-ref 'emission)", "(param-set! 'enable-emission #f)"], [ "(param-ref 'emission)", "#f", "> (param-set! 'enable-emission #f)", "0", - "> " + "> ", ], - sep="\r\n" + sep="\r\n", ) as tm: assert tm.enable is False tm.enable = False @@ -530,15 +436,9 @@ def test_enable(): def test_firmware(): with expected_protocol( ik.toptica.TopMode, - [ - "(param-ref 'fw-ver)" - ], - [ - "(param-ref 'fw-ver)", - "1.02.01", - "> " - ], - sep="\r\n" + ["(param-ref 'fw-ver)"], + ["(param-ref 'fw-ver)", "1.02.01", "> "], + sep="\r\n", ) as tm: assert tm.firmware == (1, 2, 1) @@ -546,47 +446,30 @@ def test_firmware(): def test_serial_number(): with expected_protocol( ik.toptica.TopMode, - [ - "(param-ref 'serial-number)" - ], - [ - "(param-ref 'serial-number)", - "010101", - "> " - ], - sep="\r\n" + ["(param-ref 'serial-number)"], + ["(param-ref 'serial-number)", "010101", "> "], + sep="\r\n", ) as tm: - assert tm.serial_number == '010101' + assert tm.serial_number == "010101" -@raises(TypeError) def test_enable_error(): - with expected_protocol( - ik.toptica.TopMode, - [ - "(param-set! 'enable-emission #f)" - ], - [ - "(param-set! 'enable-emission #f)", - ">" - ], - sep="\r\n" - ) as tm: - tm.enable = "False" + with pytest.raises(TypeError): + with expected_protocol( + ik.toptica.TopMode, + ["(param-set! 'enable-emission #f)"], + ["(param-set! 'enable-emission #f)", ">"], + sep="\r\n", + ) as tm: + tm.enable = "False" def test_front_key(): with expected_protocol( ik.toptica.TopMode, - [ - "(param-ref 'front-key-locked)" - ], - [ - "(param-ref 'front-key-locked)", - "#f", - "> " - ], - sep="\r\n" + ["(param-ref 'front-key-locked)"], + ["(param-ref 'front-key-locked)", "#f", "> "], + sep="\r\n", ) as tm: assert tm.locked is False @@ -594,15 +477,9 @@ def test_front_key(): def test_interlock(): with expected_protocol( ik.toptica.TopMode, - [ - "(param-ref 'interlock-open)" - ], - [ - "(param-ref 'interlock-open)", - "#f", - "> " - ], - sep="\r\n" + ["(param-ref 'interlock-open)"], + ["(param-ref 'interlock-open)", "#f", "> "], + sep="\r\n", ) as tm: assert tm.interlock is False @@ -610,15 +487,9 @@ def test_interlock(): def test_fpga_status(): with expected_protocol( ik.toptica.TopMode, - [ - "(param-ref 'system-health)" - ], - [ - "(param-ref 'system-health)", - "0", - "> " - ], - sep="\r\n" + ["(param-ref 'system-health)"], + ["(param-ref 'system-health)", "0", "> "], + sep="\r\n", ) as tm: assert tm.fpga_status is True @@ -626,15 +497,9 @@ def test_fpga_status(): def test_fpga_status_false(): with expected_protocol( ik.toptica.TopMode, - [ - "(param-ref 'system-health)" - ], - [ - "(param-ref 'system-health)", - "#f", - "> " - ], - sep="\r\n" + ["(param-ref 'system-health)"], + ["(param-ref 'system-health)", "#f", "> "], + sep="\r\n", ) as tm: assert tm.fpga_status is False @@ -642,15 +507,9 @@ def test_fpga_status_false(): def test_temperature_status(): with expected_protocol( ik.toptica.TopMode, - [ - "(param-ref 'system-health)" - ], - [ - "(param-ref 'system-health)", - "2", - "> " - ], - sep="\r\n" + ["(param-ref 'system-health)"], + ["(param-ref 'system-health)", "2", "> "], + sep="\r\n", ) as tm: assert tm.temperature_status is False @@ -658,14 +517,8 @@ def test_temperature_status(): def test_current_status(): with expected_protocol( ik.toptica.TopMode, - [ - "(param-ref 'system-health)" - ], - [ - "(param-ref 'system-health)", - "4", - "> " - ], - sep="\r\n" + ["(param-ref 'system-health)"], + ["(param-ref 'system-health)", "4", "> "], + sep="\r\n", ) as tm: assert tm.current_status is False diff --git a/instruments/tests/test_toptica/test_toptica_utils.py b/instruments/tests/test_toptica/test_toptica_utils.py index 2e6414531..23b71e6f6 100644 --- a/instruments/tests/test_toptica/test_toptica_utils.py +++ b/instruments/tests/test_toptica/test_toptica_utils.py @@ -1,15 +1,13 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing tests for Topical util functions """ # IMPORTS ##################################################################### -from __future__ import absolute_import import datetime -from nose.tools import raises +import pytest from instruments.toptica import toptica_utils @@ -22,9 +20,9 @@ def test_convert_boolean(): assert toptica_utils.convert_toptica_boolean("Error: -3") is None -@raises(ValueError) def test_convert_boolean_value(): - toptica_utils.convert_toptica_boolean("blo") + with pytest.raises(ValueError): + toptica_utils.convert_toptica_boolean("blo") def test_convert_toptica_datetime(): diff --git a/instruments/tests/test_util_fns.py b/instruments/tests/test_util_fns.py index 914372404..9b2433ca6 100644 --- a/instruments/tests/test_util_fns.py +++ b/instruments/tests/test_util_fns.py @@ -1,32 +1,89 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing tests for util_fns.py """ # IMPORTS #################################################################### -from __future__ import absolute_import - -from builtins import range - from enum import Enum -import quantities as pq -from nose.tools import raises, eq_ +import pint +import pytest + +from instruments.units import ureg as u from instruments.util_fns import ( + assume_units, + bool_property, + enum_property, + int_property, ProxyList, - assume_units, convert_temperature + setattr_expression, + string_property, + unitful_property, + unitless_property, ) + +# FIXTURES ################################################################### + + +@pytest.fixture +def mock_inst(mocker): + """Intialize a mock instrument to test property factories. + + Include a call to each property factory to be tested. The command + given to the property factory must be a valid argument returned by + query. This argument can be asserted later. Also set up are mocker + spies to assert `query` and `sendcmd` have actually been called. + + :return: Fake instrument class. + """ + + class Inst: + """Mock instrument class.""" + + def __init__(self): + """Set up the mocker spies and send command placeholder.""" + # spies + self.spy_query = mocker.spy(self, "query") + self.spy_sendcmd = mocker.spy(self, "sendcmd") + + # variable to set with send command + self._sendcmd = None + + def query(self, cmd): + """Return the command minus the ? which is sent along.""" + return f"{cmd[:-1]}" + + def sendcmd(self, cmd): + """Sets the command to `self._sendcmd`.""" + self._sendcmd = cmd + + class SomeEnum(Enum): + test = "enum" + + bool_property = bool_property("ON") # return True + + enum_property = enum_property("enum", SomeEnum) + + unitless_property = unitless_property("42") + + int_property = int_property("42") + + unitful_property = unitful_property("42", u.K) + + string_property = string_property("'STRING'") + + return Inst() + + # TEST CASES ################################################################# -# pylint: disable=protected-access,missing-docstring +# pylint: disable=protected-access,missing-docstring,redefined-outer-name def test_ProxyList_basics(): - class ProxyChild(object): - + class ProxyChild: def __init__(self, parent, name): self._parent = parent self._name = name @@ -41,8 +98,7 @@ def __init__(self, parent, name): def test_ProxyList_valid_range_is_enum(): - class ProxyChild(object): - + class ProxyChild: def __init__(self, parent, name): self._parent = parent self._name = name @@ -54,14 +110,13 @@ class MockEnum(Enum): parent = object() proxy_list = ProxyList(parent, ProxyChild, MockEnum) - assert proxy_list['aa']._name == MockEnum.a.value - assert proxy_list['b']._name == MockEnum.b.value + assert proxy_list["aa"]._name == MockEnum.a.value + assert proxy_list["b"]._name == MockEnum.b.value assert proxy_list[MockEnum.a]._name == MockEnum.a.value def test_ProxyList_length(): - class ProxyChild(object): - + class ProxyChild: def __init__(self, parent, name): self._parent = parent self._name = name @@ -70,12 +125,11 @@ def __init__(self, parent, name): proxy_list = ProxyList(parent, ProxyChild, range(10)) - eq_(len(proxy_list), 10) + assert len(proxy_list) == 10 def test_ProxyList_iterator(): - class ProxyChild(object): - + class ProxyChild: def __init__(self, parent, name): self._parent = parent self._name = name @@ -86,86 +140,174 @@ def __init__(self, parent, name): i = 0 for item in proxy_list: - eq_(item._name, i) + assert item._name == i i = i + 1 -@raises(IndexError) def test_ProxyList_invalid_idx_enum(): - class ProxyChild(object): + with pytest.raises(IndexError): - def __init__(self, parent, name): - self._parent = parent - self._name = name + class ProxyChild: + def __init__(self, parent, name): + self._parent = parent + self._name = name - class MockEnum(Enum): - a = "aa" - b = "bb" + class MockEnum(Enum): + a = "aa" + b = "bb" - parent = object() + parent = object() - proxy_list = ProxyList(parent, ProxyChild, MockEnum) + proxy_list = ProxyList(parent, ProxyChild, MockEnum) - _ = proxy_list['c'] # Should raise IndexError + _ = proxy_list["c"] # Should raise IndexError -@raises(IndexError) def test_ProxyList_invalid_idx(): - class ProxyChild(object): + with pytest.raises(IndexError): - def __init__(self, parent, name): - self._parent = parent - self._name = name + class ProxyChild: + def __init__(self, parent, name): + self._parent = parent + self._name = name - parent = object() + parent = object() - proxy_list = ProxyList(parent, ProxyChild, range(5)) + proxy_list = ProxyList(parent, ProxyChild, range(5)) - _ = proxy_list[10] # Should raise IndexError + _ = proxy_list[10] # Should raise IndexError def test_assume_units_correct(): - m = pq.Quantity(1, 'm') + m = u.Quantity(1, "m") # Check that unitful quantities are kept unitful. - eq_(assume_units(m, 'mm').rescale('mm').magnitude, 1000) + assert assume_units(m, "mm").to("mm").magnitude == 1000 # Check that raw scalars are made unitful. - eq_(assume_units(1, 'm').rescale('mm').magnitude, 1000) - - -def test_temperature_conversion(): - blo = 70.0 * pq.degF - out = convert_temperature(blo, pq.degC) - eq_(out.magnitude, 21.11111111111111) - out = convert_temperature(blo, pq.degK) - eq_(out.magnitude, 294.2055555555555) - out = convert_temperature(blo, pq.degF) - eq_(out.magnitude, 70.0) - - blo = 20.0 * pq.degC - out = convert_temperature(blo, pq.degF) - eq_(out.magnitude, 68) - out = convert_temperature(blo, pq.degC) - eq_(out.magnitude, 20.0) - out = convert_temperature(blo, pq.degK) - eq_(out.magnitude, 293.15) - - blo = 270 * pq.degK - out = convert_temperature(blo, pq.degC) - eq_(out.magnitude, -3.1499999999999773) - out = convert_temperature(blo, pq.degF) - eq_(out.magnitude, 141.94736842105263) - out = convert_temperature(blo, pq.K) - eq_(out.magnitude, 270) - - -@raises(ValueError) -def test_temperater_conversion_failure(): - blo = 70.0 * pq.degF - convert_temperature(blo, pq.V) - - -@raises(ValueError) + assert assume_units(1, "m").to("mm").magnitude == 1000 + + def test_assume_units_failures(): - assume_units(1, 'm').rescale('s') + with pytest.raises(pint.errors.DimensionalityError): + assume_units(1, "m").to("s") + + +def test_setattr_expression_simple(): + class A: + x = "x" + y = "y" + z = "z" + + a = A() + setattr_expression(a, "x", "foo") + assert a.x == "foo" + + +def test_setattr_expression_index(): + class A: + x = ["x", "y", "z"] + + a = A() + setattr_expression(a, "x[1]", "foo") + assert a.x[1] == "foo" + + +def test_setattr_expression_nested(): + class B: + x = "x" + + class A: + b = None + + def __init__(self): + self.b = B() + + a = A() + setattr_expression(a, "b.x", "foo") + assert a.b.x == "foo" + + +def test_setattr_expression_both(): + class B: + x = "x" + + class A: + b = None + + def __init__(self): + self.b = [B()] + + a = A() + setattr_expression(a, "b[0].x", "foo") + assert a.b[0].x == "foo" + + +def test_bool_property_sendcmd_query(mock_inst): + """Assert that bool_property calls sendcmd, query of parent class.""" + # fixture query should return "On" -> True + assert mock_inst.bool_property + mock_inst.spy_query.assert_called() + # setter + mock_inst.bool_property = True + assert mock_inst._sendcmd == "ON ON" + mock_inst.spy_sendcmd.assert_called() + + +def test_enum_property_sendcmd_query(mock_inst): + """Assert that enum_property calls sendcmd, query of parent class.""" + # test getter + assert mock_inst.enum_property == mock_inst.SomeEnum.test + mock_inst.spy_query.assert_called() + # setter + mock_inst.enum_property = mock_inst.SomeEnum.test + assert mock_inst._sendcmd == "enum enum" + mock_inst.spy_sendcmd.assert_called() + + +def test_unitless_property_sendcmd_query(mock_inst): + """Assert that unitless_property calls sendcmd, query of parent class.""" + # getter + assert mock_inst.unitless_property == 42 + mock_inst.spy_query.assert_called() + # setter + value = 13 + mock_inst.unitless_property = value + assert mock_inst._sendcmd == f"42 {value:e}" + mock_inst.spy_sendcmd.assert_called() + + +def test_int_property_sendcmd_query(mock_inst): + """Assert that int_property calls sendcmd, query of parent class.""" + # getter + assert mock_inst.int_property == 42 + mock_inst.spy_query.assert_called() + # setter + value = 13 + mock_inst.int_property = value + assert mock_inst._sendcmd == f"42 {value}" + mock_inst.spy_sendcmd.assert_called() + + +def test_unitful_property_sendcmd_query(mock_inst): + """Assert that unitful_property calls sendcmd, query of parent class.""" + # getter + assert mock_inst.unitful_property == u.Quantity(42, u.K) + mock_inst.spy_query.assert_called() + # setter + value = 13 + mock_inst.unitful_property = u.Quantity(value, u.K) + assert mock_inst._sendcmd == f"42 {value:e}" + mock_inst.spy_sendcmd.assert_called() + + +def test_string_property_sendcmd_query(mock_inst): + """Assert that string_property calls sendcmd, query of parent class.""" + # getter + assert mock_inst.string_property == "STRING" + mock_inst.spy_query.assert_called() + # setter + value = "forty-two" + mock_inst.string_property = value + assert mock_inst._sendcmd == f"'STRING' \"{value}\"" + mock_inst.spy_sendcmd.assert_called() diff --git a/instruments/tests/test_yokogawa/__init__.py b/instruments/tests/test_yokogawa/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/instruments/tests/test_yokogawa/test_yokogawa7651.py b/instruments/tests/test_yokogawa/test_yokogawa7651.py new file mode 100644 index 000000000..d7b46ea3e --- /dev/null +++ b/instruments/tests/test_yokogawa/test_yokogawa7651.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python +""" +Unit tests for the Yokogawa 7651 power supply +""" + +# IMPORTS ##################################################################### + + +import pytest + +import instruments as ik +from instruments.units import ureg as u +from instruments.tests import expected_protocol + + +# TESTS ####################################################################### + +# pylint: disable=protected-access + +# TEST CHANNEL # + + +def test_channel_init(): + """Initialize of channel class.""" + with expected_protocol(ik.yokogawa.Yokogawa7651, [], []) as yok: + assert yok.channel[0]._parent is yok + assert yok.channel[0]._name == 0 + + +def test_channel_mode(): + """Get / Set mode of the channel.""" + with expected_protocol( + ik.yokogawa.Yokogawa7651, ["F5;", "E;", "F1;", "E;"], [] # trigger # trigger + ) as yok: + # query + with pytest.raises(NotImplementedError) as exc_info: + print(f"Mode is: {yok.channel[0].mode}") + exc_msg = exc_info.value.args[0] + assert ( + exc_msg == "This instrument does not support querying the " + "operation mode." + ) + + # set first current, then voltage mode + yok.channel[0].mode = yok.Mode.current + yok.channel[0].mode = yok.Mode.voltage + + +def test_channel_invalid_mode_set(): + """Set mode to invalid value.""" + with expected_protocol(ik.yokogawa.Yokogawa7651, [], []) as yok: + wrong_mode = 42 + with pytest.raises(TypeError) as exc_info: + yok.channel[0].mode = wrong_mode + exc_msg = exc_info.value.args[0] + assert ( + exc_msg == "Mode setting must be a `Yokogawa7651.Mode` " + "value, got {} instead.".format(type(wrong_mode)) + ) + + +def test_channel_voltage(): + """Get / Set voltage of channel.""" + + # values to set for test + value_unitless = 5.0 + value_unitful = u.Quantity(500, u.mV) + + with expected_protocol( + ik.yokogawa.Yokogawa7651, + [ + "F1;\nE;", # set voltage mode + f"SA{value_unitless};", + "E;", # trigger + "F1;\nE;", # set voltage mode + f"SA{value_unitful.to(u.volt).magnitude};", + "E;", # trigger + ], + [], + ) as yok: + # query + with pytest.raises(NotImplementedError) as exc_info: + print(f"Voltage is: {yok.channel[0].voltage}") + exc_msg = exc_info.value.args[0] + assert ( + exc_msg == "This instrument does not support querying the " + "output voltage setting." + ) + + # set first current, then voltage mode + yok.channel[0].voltage = value_unitless + yok.channel[0].voltage = value_unitful + + +def test_channel_current(): + """Get / Set current of channel.""" + + # values to set for test + value_unitless = 0.8 + value_unitful = u.Quantity(50, u.mA) + + with expected_protocol( + ik.yokogawa.Yokogawa7651, + [ + "F5;\nE;", # set voltage mode + f"SA{value_unitless};", + "E;", # trigger + "F5;\nE;", # set voltage mode + f"SA{value_unitful.to(u.A).magnitude};", + "E;", # trigger + ], + [], + ) as yok: + # query + with pytest.raises(NotImplementedError) as exc_info: + print(f"Current is: {yok.channel[0].current}") + exc_msg = exc_info.value.args[0] + assert ( + exc_msg == "This instrument does not support querying the " + "output current setting." + ) + + # set first current, then current mode + yok.channel[0].current = value_unitless + yok.channel[0].current = value_unitful + + +def test_channel_output(): + """Get / Set output of channel.""" + with expected_protocol( + ik.yokogawa.Yokogawa7651, + ["O1;", "E;", "O0;", "E;"], # turn output on # turn output off + [], + ) as yok: + # query + with pytest.raises(NotImplementedError) as exc_info: + print(f"Output is: {yok.channel[0].output}") + exc_msg = exc_info.value.args[0] + assert ( + exc_msg == "This instrument does not support querying the " "output status." + ) + + # set first current, then current mode + yok.channel[0].output = True + yok.channel[0].output = False + + +# CLASS PROPERTIES # + + +def test_voltage(): + """Get / Set voltage of instrument.""" + + # values to set for test + value_unitless = 5.0 + value_unitful = u.Quantity(500, u.mV) + + with expected_protocol( + ik.yokogawa.Yokogawa7651, + [ + "F1;\nE;", # set voltage mode + f"SA{value_unitless};", + "E;", # trigger + "F1;\nE;", # set voltage mode + f"SA{value_unitful.to(u.volt).magnitude};", + "E;", # trigger + ], + [], + ) as yok: + # query + with pytest.raises(NotImplementedError) as exc_info: + print(f"Voltage is: {yok.voltage}") + exc_msg = exc_info.value.args[0] + assert ( + exc_msg == "This instrument does not support querying the " + "output voltage setting." + ) + + # set first current, then voltage mode + yok.voltage = value_unitless + yok.voltage = value_unitful + + +def test_current(): + """Get / Set current of instrument.""" + + # values to set for test + value_unitless = 0.8 + value_unitful = u.Quantity(50, u.mA) + + with expected_protocol( + ik.yokogawa.Yokogawa7651, + [ + "F5;\nE;", # set current mode + f"SA{value_unitless};", + "E;", # trigger + "F5;\nE;", # set current mode + f"SA{value_unitful.to(u.A).magnitude};", + "E;", # trigger + ], + [], + ) as yok: + # query + with pytest.raises(NotImplementedError) as exc_info: + print(f"current is: {yok.current}") + exc_msg = exc_info.value.args[0] + assert ( + exc_msg == "This instrument does not support querying the " + "output current setting." + ) + + # set first current, then current mode + yok.current = value_unitless + yok.current = value_unitful diff --git a/instruments/tests/test_yokogawa/test_yokogawa_6370.py b/instruments/tests/test_yokogawa/test_yokogawa_6370.py new file mode 100644 index 000000000..87117cc83 --- /dev/null +++ b/instruments/tests/test_yokogawa/test_yokogawa_6370.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python +""" +Unit tests for the Yokogawa 6370 +""" + +# IMPORTS ##################################################################### + + +import struct + +from hypothesis import ( + given, + strategies as st, +) + +import instruments as ik +from instruments.optional_dep_finder import numpy +from instruments.tests import ( + expected_protocol, + iterable_eq, +) +from instruments.units import ureg as u + + +# TESTS ####################################################################### + + +def test_channel_is_channel_class(): + inst = ik.yokogawa.Yokogawa6370.open_test() + assert isinstance(inst.channel["A"], inst.Channel) is True + + +def test_init(): + with expected_protocol(ik.yokogawa.Yokogawa6370, [":FORMat:DATA REAL,64"], []) as _: + pass + + +@given( + values=st.lists(st.floats(allow_infinity=False, allow_nan=False), min_size=1), + channel=st.sampled_from(ik.yokogawa.Yokogawa6370.Traces), +) +def test_channel_data(values, channel): + values_packed = b"".join(struct.pack(" timeout: + break + if not resp: if expect is None: return None else: - raise IOError("Expected packet {}, got nothing instead.".format( - expect - )) + raise OSError(f"Expected packet {expect}, got nothing instead.") pkt = _packets.ThorLabsPacket.unpack(resp) if expect is not None and pkt._message_id != expect: # TODO: make specialized subclass that can record the offending # packet. - raise IOError("APT returned message ID {}, expected {}".format( - pkt._message_id, expect - )) + raise OSError( + "APT returned message ID {}, expected {}".format( + pkt._message_id, expect + ) + ) + return pkt diff --git a/instruments/thorlabs/_cmds.py b/instruments/thorlabs/_cmds.py index a92ad885c..c60fd7b69 100644 --- a/instruments/thorlabs/_cmds.py +++ b/instruments/thorlabs/_cmds.py @@ -1,5 +1,4 @@ #!/usr/bin/python -# -*- coding: utf-8 -*- """ Contains command mneonics for the ThorLabs APT protocol @@ -8,8 +7,6 @@ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division from enum import IntEnum # CLASSES ##################################################################### @@ -19,6 +16,7 @@ class ThorLabsCommands(IntEnum): """ Enum containing command mneonics for the ThorLabs APT protocol """ + # General System Commands MOD_IDENTIFY = 0x0223 MOD_SET_CHANENABLESTATE = 0x0210 @@ -264,3 +262,12 @@ class ThorLabsCommands(IntEnum): LA_REQ_STATUSUPDATE = 0x0820 LA_GET_STATUSUPDATE = 0x0821 LA_ACK_STATUSUPDATE = 0x0822 + + # Additional messages for TIM101 and KIM101 + PZMOT_SET_PARAMS = 0x08C0 + PZMOT_REQ_PARAMS = 0x08C1 + PZMOT_GET_PARAMS = 0x08C2 + PZMOT_MOVE_ABSOLUTE = 0x08D4 + PZMOT_MOVE_COMPLETED = 0x08D6 + PZMOT_MOVE_JOG = 0x08D9 + PZMOT_GET_STATUSUPDATE = 0x08E1 diff --git a/instruments/thorlabs/_packets.py b/instruments/thorlabs/_packets.py index 83c6decb9..7159466cd 100644 --- a/instruments/thorlabs/_packets.py +++ b/instruments/thorlabs/_packets.py @@ -1,25 +1,34 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module for working with ThorLabs packets. """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division import struct # STRUCTS ##################################################################### -message_header_nopacket = struct.Struct(' self.max_temperature: + newval = convert_temperature(newval, u.degC) + if newval < u.Quantity(20.0, u.degC) or newval > self.max_temperature: raise ValueError("Temperature set is out of range.") - out_query = "tset={}".format(newval) + out_query = f"tset={newval.magnitude}" self.sendcmd(out_query) @property @@ -220,7 +225,7 @@ def p(self): def p(self, newval): if newval not in range(1, 251): raise ValueError("P-value not in [1, 250]") - self.sendcmd("pgain={}".format(newval)) + self.sendcmd(f"pgain={newval}") @property def i(self): @@ -236,7 +241,7 @@ def i(self): def i(self, newval): if newval not in range(0, 251): raise ValueError("I-value not in [0, 250]") - self.sendcmd("igain={}".format(newval)) + self.sendcmd(f"igain={newval}") @property def d(self): @@ -252,7 +257,7 @@ def d(self): def d(self, newval): if newval not in range(0, 251): raise ValueError("D-value not in [0, 250]") - self.sendcmd("dgain={}".format(newval)) + self.sendcmd(f"dgain={newval}") @property def pid(self): @@ -285,23 +290,23 @@ def degrees(self): Gets/sets the units of the temperature measurement. :return: The temperature units (degC/F/K) the TC200 is measuring in - :type: `~quantities.unitquantity.UnitTemperature` + :type: `~pint.Unit` """ response = self.status if (response >> 4) % 2 and (response >> 5) % 2: - return pq.degC + return u.degC elif (response >> 5) % 2: - return pq.degK - else: - return pq.degF + return u.degK + + return u.degF @degrees.setter def degrees(self, newval): - if newval is pq.degC: + if newval == u.degC: self.sendcmd("unit=c") - elif newval is pq.degF: + elif newval == u.degF: self.sendcmd("unit=f") - elif newval is pq.degK: + elif newval == u.degK: self.sendcmd("unit=k") else: raise TypeError("Invalid temperature type") @@ -309,8 +314,7 @@ def degrees(self, newval): sensor = enum_property( "sns", Sensor, - input_decoration=lambda x: x.split( - ",")[0].split("=")[1].strip().lower(), + input_decoration=lambda x: x.split(",")[0].split("=")[1].strip().lower(), set_fmt="{}={}", doc=""" Gets/sets the current thermistor type. Used for converting resistances @@ -318,7 +322,7 @@ def degrees(self, newval): :return: The thermistor type :type: `TC200.Sensor` - """ + """, ) beta = int_property( @@ -332,20 +336,20 @@ def degrees(self, newval): :return: the gain (in nnn) :type: `int` - """ + """, ) max_power = unitful_property( "pmax", - units=pq.W, + units=u.W, format_code="{:.1f}", set_fmt="{}={}", - valid_range=(0.1*pq.W, 18.0*pq.W), + valid_range=(0.1 * u.W, 18.0 * u.W), doc=""" Gets/sets the maximum power :return: The maximum power :units: Watts (linear units) - :type: `~quantities.quantity.Quantity` - """ + :type: `~pint.Quantity` + """, ) diff --git a/instruments/thorlabs/thorlabs_utils.py b/instruments/thorlabs/thorlabs_utils.py index d5514d0a6..8f17d6cf0 100644 --- a/instruments/thorlabs/thorlabs_utils.py +++ b/instruments/thorlabs/thorlabs_utils.py @@ -1,26 +1,4 @@ #!/usr/bin/python -# -*- coding: utf-8 -*- -# -# thorlabs_utils.py: Utility functions for Thorlabs-brand instruments. -# -# © 2016 Steven Casagrande (scasagrande@galvant.ca). -# -# This file is a part of the InstrumentKit project. -# Licensed under the AGPL version 3. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# """ Contains common utility functions for Thorlabs-brand instruments """ @@ -35,7 +13,4 @@ def check_cmd(response): :return: 1 if not found, 0 otherwise :rtype: int """ - if response != "CMD_NOT_DEFINED" and response != "CMD_ARG_INVALID": - return 1 - else: - return 0 + return 1 if response != "CMD_NOT_DEFINED" and response != "CMD_ARG_INVALID" else 0 diff --git a/instruments/thorlabs/thorlabsapt.py b/instruments/thorlabs/thorlabsapt.py index d79ab01ad..3414b0983 100644 --- a/instruments/thorlabs/thorlabsapt.py +++ b/instruments/thorlabs/thorlabsapt.py @@ -1,22 +1,20 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides the support for the Thorlabs APT Controller. """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division import re import struct import logging - -from builtins import range -import quantities as pq +import codecs +import warnings from instruments.thorlabs import _abstract, _packets, _cmds +from instruments.units import ureg as u +from instruments.util_fns import assume_units # LOGGING ##################################################################### @@ -25,6 +23,8 @@ # CLASSES ##################################################################### +# pylint: disable=too-many-lines + class ThorLabsAPT(_abstract.ThorLabsInstrument): @@ -34,7 +34,7 @@ class ThorLabsAPT(_abstract.ThorLabsInstrument): thorlabs source folder. """ - class APTChannel(object): + class APTChannel: """ Represents a channel within the hardware device. One device can have @@ -53,17 +53,30 @@ def enabled(self): Gets/sets the enabled status for the specified APT channel :type: `bool` + + :raises TypeError: If controller is not supported """ + if self._apt.model_number[0:3] == "KIM": + raise TypeError( + "For KIM controllers, use the " + "`enabled_single` function to enable " + "one axis. For KIM101 controllers, " + "multiple axes can be enabled using " + "the `enabled_multi` function from the " + "controller level." + ) + pkt = _packets.ThorLabsPacket( message_id=_cmds.ThorLabsCommands.MOD_REQ_CHANENABLESTATE, param1=self._idx_chan, param2=0x00, dest=self._apt.destination, source=0x01, - data=None + data=None, ) resp = self._apt.querypacket( - pkt, expect=_cmds.ThorLabsCommands.MOD_GET_CHANENABLESTATE) + pkt, expect=_cmds.ThorLabsCommands.MOD_GET_CHANENABLESTATE + ) return not bool(resp.parameters[1] - 1) @enabled.setter @@ -74,14 +87,14 @@ def enabled(self, newval): param2=0x01 if newval else 0x02, dest=self._apt.destination, source=0x01, - data=None + data=None, ) self._apt.sendpacket(pkt) _channel_type = APTChannel def __init__(self, filelike): - super(ThorLabsAPT, self).__init__(filelike) + super().__init__(filelike) self._dest = 0x50 # Generic USB device; make this configurable later. # Provide defaults in case an exception occurs below. @@ -104,44 +117,49 @@ def __init__(self, filelike): param2=0x00, dest=self._dest, source=0x01, - data=None + data=None, ) hw_info = self.querypacket( - req_packet, expect=_cmds.ThorLabsCommands.HW_GET_INFO) + req_packet, + expect=_cmds.ThorLabsCommands.HW_GET_INFO, + expect_data_len=84, + ) - self._serial_number = str(hw_info.data[0:4]).encode('hex') - self._model_number = str( - hw_info.data[4:12]).replace('\x00', '').strip() + self._serial_number = codecs.encode(hw_info.data[0:4], "hex").decode( + "ascii" + ) + self._model_number = ( + hw_info.data[4:12].decode("ascii").replace("\x00", "").strip() + ) - hw_type_int = struct.unpack(' 0: - self._channel = tuple(self._channel_type(self, chan_idx) - for chan_idx in range(self._n_channels)) + self._channel = tuple( + self._channel_type(self, chan_idx) + for chan_idx in range(self._n_channels) + ) @property def serial_number(self): @@ -170,13 +188,15 @@ def name(self): :type: `str` """ - return "ThorLabs APT Instrument model {model}, serial {serial} " \ - "(HW version {hw_ver}, FW version {fw_ver})".format( - hw_ver=self._hw_version, - serial=self.serial_number, - fw_ver=self._fw_version, - model=self.model_number - ) + return ( + "ThorLabs APT Instrument model {model}, serial {serial} " + "(HW version {hw_ver}, FW version {fw_ver})".format( + hw_ver=self._hw_version, + serial=self.serial_number, + fw_ver=self._fw_version, + model=self.model_number, + ) + ) @property def channel(self): @@ -206,9 +226,10 @@ def n_channels(self, nch): # If we add more channels, append them to the list, # If we remove channels, remove them from the end of the list. if nch > self._n_channels: - self._channel = self._channel + \ - list(self._channel_type(self, chan_idx) - for chan_idx in range(self._n_channels, nch)) + self._channel = list(self._channel) + list( + self._channel_type(self, chan_idx) + for chan_idx in range(self._n_channels, nch) + ) elif nch < self._n_channels: self._channel = self._channel[:nch] self._n_channels = nch @@ -224,7 +245,7 @@ def identify(self): param2=0x00, dest=self._dest, source=0x01, - data=None + data=None, ) self.sendpacket(pkt) @@ -252,6 +273,7 @@ class PiezoDeviceChannel(ThorLabsAPT.APTChannel): This class represents piezo stage channels. """ + # PIEZO COMMANDS # @property @@ -259,7 +281,7 @@ def max_travel(self): """ Gets the maximum travel for the specified piezo channel. - :type: `~quantities.Quantity` + :type: `~pint.Quantity` :units: Nanometers """ pkt = _packets.ThorLabsPacket( @@ -268,9 +290,9 @@ def max_travel(self): param2=0x00, dest=self._apt.destination, source=0x01, - data=None + data=None, ) - resp = self._apt.querypacket(pkt) + resp = self._apt.querypacket(pkt, expect_data_len=4) # Not all APT piezo devices support querying the maximum travel # distance. Those that do not simply ignore the PZ_REQ_MAXTRAVEL @@ -279,8 +301,8 @@ def max_travel(self): return NotImplemented # chan, int_maxtrav - _, int_maxtrav = struct.unpack('>> import instruments as ik + >>> import instruments.units as u + >>> # call the controller + >>> kim = ik.thorlabs.APTPiezoInertiaActuator.open_serial("/dev/ttyUSB0", baud=115200) + >>> # set first channel to enabled + >>> ch = kim.channel[0] + >>> ch.enabled_single = True + >>> # define and set drive parameters + >>> max_volts = u.Quantity(110, u.V) + >>> step_rate = u.Quantity(1000, 1/u.s) + >>> acceleration = u.Quantity(10000, 1/u.s**2) + >>> ch.drive_op_parameters = [max_volts, step_rate, acceleration] + >>> # aboslute move to 1000 steps + >>> ch.move_abs(1000) + """ + + class PiezoChannel(APTPiezoDevice.PiezoDeviceChannel): + """ + Class representing a single piezo channel within a piezo stage + on the Thorlabs APT controller. + """ + + # PROPERTIES # + + @property + def drive_op_parameters(self): + """Get / Set various drive parameters for move motion. + + Defines the speed and acceleration of moves initiated in + the following ways: + - by clicking in the position display + - via the top panel controls when ‘Go To Position’ mode is + selected (in the Set_TIM_JogParameters (09) or + Set_KCubeMMIParams (15) sub‐messages). + - via software using the MoveVelocity, MoveAbsoluteStepsEx + or MoveRelativeStepsEx methods. + + :setter: The setter must be be given as a list of 3 + entries. The three entries are: + - Maximum Voltage: + The maximum piezo drive voltage, in the range 85V + to 125V. Unitful, if no unit given, V are assumed. + - Step Rate: + The piezo motor moves by ramping up the drive + voltage to the value set in the MaxVoltage parameter + and then dropping quickly to zero, then repeating. + One cycle is termed a step. This parameter specifies + the velocity to move when a command is initiated. + The step rate is specified in steps/sec, in the range 1 + to 2,000. Unitful, if no unit given, 1 / sec assumed. + - Step Acceleration: + This parameter specifies the acceleration up to the + step rate, in the range 1 to 100,000 cycles/sec/sec. + Unitful, if no unit given, 1/sec**2 assumed. + + :return: List with the drive parameters, unitful. + + :raises TypeError: The setter was not a list or tuple. + :raises ValueError: The setter was not given a tuple with + three values. + :raises ValueError: One of the parameters was out of range. + + Example: + >>> import instruments as ik + >>> import instruments.units as u + >>> # call the controller + >>> kim = ik.thorlabs.APTPiezoInertiaActuator.open_serial("/dev/ttyUSB0", baud=115200) + >>> # grab channel 0 + >>> ch = kim.channel[0] + >>> # change the step rate to 2000 /s + >>> drive_params = ch.drive_op_parameters + >>> drive_params[1] = 2000 + >>> ch.drive_op_parameters = drive_params + """ + pkt = _packets.ThorLabsPacket( + message_id=_cmds.ThorLabsCommands.PZMOT_REQ_PARAMS, + param1=0x07, + param2=self._idx_chan, + dest=self._apt.destination, + source=0x01, + data=None, + ) + + resp = self._apt.querypacket( + pkt, expect=_cmds.ThorLabsCommands.PZMOT_GET_PARAMS, expect_data_len=14 + ) + + # unpack + ret_val = struct.unpack(" 125: + raise ValueError( + "The voltage ({} V) is out of range. It must " + "be between 85 V and 125 V.".format(volt) + ) + if rate < 1 or rate > 2000: + raise ValueError( + "The step rate ({} /s) is out of range. It " + "must be between 1 /s and 2,000 /s.".format(rate) + ) + + if accl < 1 or accl > 100000: + raise ValueError( + "The acceleration ({} /s/s) is out of range. " + "It must be between 1 /s/s and 100,000 /s/s.".format(accl) + ) + + pkt = _packets.ThorLabsPacket( + message_id=_cmds.ThorLabsCommands.PZMOT_SET_PARAMS, + param1=None, + param2=None, + dest=self._apt.destination, + source=0x01, + data=struct.pack(">> import instruments as ik + >>> # call the controller + >>> kim = ik.thorlabs.APTPiezoInertiaActuator.open_serial("/dev/ttyUSB0", baud=115200) + >>> # grab channel 0 + >>> ch = kim.channel[0] + >>> # enable channel 0 + >>> ch.enabled_single = True + """ + if self._apt.model_number[0:3] != "KIM": + raise ( + "This command is only valid with KIM001 and " + "KIM101 controllers. Your controller is a {}.".format( + self._apt.model_number + ) + ) + + pkt = _packets.ThorLabsPacket( + message_id=_cmds.ThorLabsCommands.PZMOT_REQ_PARAMS, + param1=0x2B, + param2=self._idx_chan, + dest=self._apt.destination, + source=0x01, + data=None, + ) + + resp = self._apt.querypacket( + pkt, expect=_cmds.ThorLabsCommands.PZMOT_GET_PARAMS, expect_data_len=4 + ) + + ret_val = struct.unpack(">> import instruments as ik + >>> import instruments.units as u + >>> # call the controller + >>> kim = ik.thorlabs.APTPiezoInertiaActuator.open_serial("/dev/ttyUSB0", baud=115200) + >>> # grab channel 0 + >>> ch = kim.channel[0] + >>> # set jog parameters + >>> mode = 2 # only move by set step size + >>> step = 100 # step size + >>> rate = u.Quantity(1000, 1/u.s) # step rate + >>> # if no quantity given, SI units assumed + >>> accl = 10000 + >>> ch.jog_parameters = [mode, step, step, rate, accl] + >>> ch.jog_parameters + [2, 100, 100, array(1000) * 1/s, array(10000) * 1/s**2] + """ + if self._apt.model_number[0:3] != "KIM": + raise TypeError( + "This command is only valid with " + "KIM001 and KIM101 controllers. Your " + "controller is a {}.".format(self._apt.model_number) + ) + + pkt = _packets.ThorLabsPacket( + message_id=_cmds.ThorLabsCommands.PZMOT_REQ_PARAMS, + param1=0x2D, + param2=self._idx_chan, + dest=self._apt.destination, + source=0x01, + data=None, + ) + + resp = self._apt.querypacket( + pkt, expect=_cmds.ThorLabsCommands.PZMOT_GET_PARAMS, expect_data_len=22 + ) + + # unpack response + ret_val = struct.unpack(" 2000: + raise ValueError( + "The steps forward ({}) are out of range. It " + "must be between 1 and 2,000.".format(steps_fwd) + ) + if steps_bkw < 1 or steps_bkw > 2000: + raise ValueError( + "The steps backward ({}) are out of range. " + "It must be between 1 and 2,000.".format(steps_bkw) + ) + if rate < 1 or rate > 2000: + raise ValueError( + "The step rate ({} /s) is out of range. It " + "must be between 1 /s and 2,000 /s.".format(rate) + ) + if accl < 1 or accl > 100000: + raise ValueError( + "The acceleration ({} /s/s) is out of range. " + "It must be between 1 /s/s and 100,000 /s/s.".format(accl) + ) + + pkt = _packets.ThorLabsPacket( + message_id=_cmds.ThorLabsCommands.PZMOT_SET_PARAMS, + param1=None, + param2=None, + dest=self._apt.destination, + source=0x01, + data=struct.pack( + ">> import instruments as ik + >>> # call the controller + >>> kim = ik.thorlabs.APTPiezoInertiaActuator.open_serial("/dev/ttyUSB0", baud=115200) + >>> # grab channel 0 + >>> ch = kim.channel[0] + >>> # set position count to zero + >>> ch.position_count = 0 + >>> ch.position_count + 0 + """ + pkt = _packets.ThorLabsPacket( + message_id=_cmds.ThorLabsCommands.PZMOT_REQ_PARAMS, + param1=0x05, + param2=self._idx_chan, + dest=self._apt.destination, + source=0x01, + data=None, + ) + + resp = self._apt.querypacket( + pkt, expect=_cmds.ThorLabsCommands.PZMOT_GET_PARAMS, expect_data_len=12 + ) + + ret_val = int(struct.unpack(">> import instruments as ik + >>> # call the controller + >>> kim = ik.thorlabs.APTPiezoInertiaActuator.open_serial("/dev/ttyUSB0", baud=115200) + >>> # grab channel 0 + >>> ch = kim.channel[0] + >>> # move to 314 steps + >>> ch.move_abs(314) + """ + pkt = _packets.ThorLabsPacket( + message_id=_cmds.ThorLabsCommands.PZMOT_MOVE_ABSOLUTE, + param1=None, + param2=None, + dest=self._apt.destination, + source=0x01, + data=struct.pack(">> import instruments as ik + >>> # call the controller + >>> kim = ik.thorlabs.APTPiezoInertiaActuator.open_serial("/dev/ttyUSB0", baud=115200) + >>> # grab channel 0 + >>> ch = kim.channel[0] + >>> # set jog parameters + >>> params = ch.jog_parameters + >>> params[0] = 2 # move by number of steps + >>> params[1] = 100 # step size forward + >>> params[2] = 200 # step size reverse + >>> ch.jog_parameters = params # set parameters + >>> # jog forward (default) + >>> ch.move_jog() + >>> # jog reverse + >>> ch.move_jog('rev') + """ + if direction == "rev": + param2 = 0x02 + else: + param2 = 0x01 + + pkt = _packets.ThorLabsPacket( + message_id=_cmds.ThorLabsCommands.PZMOT_MOVE_JOG, + param1=self._idx_chan, + param2=param2, + dest=self._apt.destination, + source=0x01, + data=None, + ) + self._apt.sendpacket(pkt) + + def move_jog_stop(self): + """Stops the current motor movement. + + Stop a jog command. The regular motor move stop command does + not work for jogging. This command somehow does... + + .. note:: This information is quite empirical. It would + only be really needed if jogging parameters are set to + continuous. The safer method is to set the step range. + """ + pkt = _packets.ThorLabsPacket( + message_id=_cmds.ThorLabsCommands.PZMOT_MOVE_JOG, + param1=self._idx_chan, + param2=0x00, + dest=self._apt.destination, + source=0x01, + data=None, + ) + + self._apt.sendpacket(pkt) + + _channel_type = PiezoChannel + + # PROPERTIES # + + @property + def enabled_multi(self): + """Enable / Query mulitple channel mode. + + For KIM101 controller, where multiple axes can be selected + simultaneously (i. e., for a mirror mount). + + :setter mode: Channel pair to be activated. + 0: All channels deactivated + 1: First channel pair activated (channel 0 & 1) + 2: Second channel pair activated (channel 2 & 3) + :type mode: int + + :return: The selected mode: + 0 - multi-channel selection disabled + 1 - Channel 0 & 1 enabled + 2 - Channel 2 & 3 enabled + :rtype: int + + :raises ValueError: No valid channel pair selected + :raises TypeError: Invalid controller for this command. + + Example: + >>> import instruments as ik + >>> kim = ik.thorlabs.APTPiezoInertiaActuator.open_serial("/dev/ttyUSB0", baud=115200) + >>> # activate the first two channels + >>> kim.enabled_multi = 1 + >>> # read back + >>> kim.enabled_multi + 1 + """ + if self.model_number != "KIM101": + raise TypeError( + "This command is only valid with " + "a KIM101 controller. Your " + "controller is a {}.".format(self.model_number) + ) + + pkt = _packets.ThorLabsPacket( + message_id=_cmds.ThorLabsCommands.PZMOT_REQ_PARAMS, + param1=0x2B, + param2=0x00, + dest=self.destination, + source=0x01, + data=None, + ) + + resp = self.querypacket( + pkt, expect=_cmds.ThorLabsCommands.PZMOT_GET_PARAMS, expect_data_len=4 + ) + + ret_val = int(struct.unpack(">> import instruments as ik + >>> import instruments.units as u + + >>> # load the controller, a KDC101 cube + >>> kdc = ik.thorlabs.APTMotorController.open_serial("/dev/ttyUSB0", baud=115200) + >>> # assign a channel to `ch` + >>> ch = kdc.channel[0] + >>> # select the stage that is connected to the controller + >>> ch.motor_model = 'PRM1-Z8' # a rotation stage + + >>> # home the stage + >>> ch.go_home() + >>> # move to 52 degrees absolute position + >>> ch.move(u.Quantity(52, u.deg)) + >>> # move 10 degrees back from current position + >>> ch.move(u.Quantity(-10, u.deg), absolute=False) """ class MotorChannel(ThorLabsAPT.APTChannel): @@ -443,6 +1117,8 @@ class MotorChannel(ThorLabsAPT.APTChannel): # INSTANCE VARIABLES # + _motor_model = None + #: Sets the scale between the encoder counts and physical units #: for the position, velocity and acceleration parameters of this #: channel. By default, set to dimensionless, indicating that the proper @@ -461,42 +1137,80 @@ class MotorChannel(ThorLabsAPT.APTChannel): #: from dimensionful input. #: #: For more details, see the APT protocol documentation. - scale_factors = (pq.Quantity(1, 'dimensionless'), ) * 3 + scale_factors = (u.Quantity(1, "dimensionless"),) * 3 + + _motion_timeout = u.Quantity(10, "second") __SCALE_FACTORS_BY_MODEL = { - re.compile('TST001|BSC00.|BSC10.|MST601'): { + # TODO: add other tables here. + re.compile("TST001|BSC00.|BSC10.|MST601"): { # Note that for these drivers, the scale factors are identical # for position, velcoity and acceleration. This is not true for # all drivers! - 'DRV001': (pq.Quantity(51200, 'ct/mm'),) * 3, - 'DRV013': (pq.Quantity(25600, 'ct/mm'),) * 3, - 'DRV014': (pq.Quantity(25600, 'ct/mm'),) * 3, - 'DRV113': (pq.Quantity(20480, 'ct/mm'),) * 3, - 'DRV114': (pq.Quantity(20480, 'ct/mm'),) * 3, - 'FW103': (pq.Quantity(25600 / 360, 'ct/deg'),) * 3, - 'NR360': (pq.Quantity(25600 / 5.4546, 'ct/deg'),) * 3 + "DRV001": (u.Quantity(51200, "count/mm"),) * 3, + "DRV013": (u.Quantity(25600, "count/mm"),) * 3, + "DRV014": (u.Quantity(25600, "count/mm"),) * 3, + "DRV113": (u.Quantity(20480, "count/mm"),) * 3, + "DRV114": (u.Quantity(20480, "count/mm"),) * 3, + "FW103": (u.Quantity(25600 / 360, "count/deg"),) * 3, + "NR360": (u.Quantity(25600 / 5.4546, "count/deg"),) * 3, + }, + re.compile("TDC001|KDC101"): { + "MTS25-Z8": ( + 1 / u.Quantity(34304, "mm/count"), + NotImplemented, + NotImplemented, + ), + "MTS50-Z8": ( + 1 / u.Quantity(34304, "mm/count"), + NotImplemented, + NotImplemented, + ), + # TODO: Z8xx and Z6xx models. Need to add regex support to motor models, too. + "PRM1-Z8": ( + u.Quantity(1919.64, "count/deg"), + NotImplemented, + NotImplemented, + ), }, - # TODO: add other tables here. } __STATUS_BIT_MASK = { - 'CW_HARD_LIM': 0x00000001, - 'CCW_HARD_LIM': 0x00000002, - 'CW_SOFT_LIM': 0x00000004, - 'CCW_SOFT_LIM': 0x00000008, - 'CW_MOVE_IN_MOTION': 0x00000010, - 'CCW_MOVE_IN_MOTION': 0x00000020, - 'CW_JOG_IN_MOTION': 0x00000040, - 'CCW_JOG_IN_MOTION': 0x00000080, - 'MOTOR_CONNECTED': 0x00000100, - 'HOMING_IN_MOTION': 0x00000200, - 'HOMING_COMPLETE': 0x00000400, - 'INTERLOCK_STATE': 0x00001000 + "CW_HARD_LIM": 0x00000001, + "CCW_HARD_LIM": 0x00000002, + "CW_SOFT_LIM": 0x00000004, + "CCW_SOFT_LIM": 0x00000008, + "CW_MOVE_IN_MOTION": 0x00000010, + "CCW_MOVE_IN_MOTION": 0x00000020, + "CW_JOG_IN_MOTION": 0x00000040, + "CCW_JOG_IN_MOTION": 0x00000080, + "MOTOR_CONNECTED": 0x00000100, + "HOMING_IN_MOTION": 0x00000200, + "HOMING_COMPLETE": 0x00000400, + "INTERLOCK_STATE": 0x00001000, } + # IK-SPECIFIC PROPERTIES # + # These properties don't correspond to any particular functionality + # of the underlying device, but control how we interact with it. + + @property + def motion_timeout(self): + """ + Gets/sets the motor channel motion timeout. + + :units: Seconds + :type: `~pint.Quantity` + """ + return self._motion_timeout + + @motion_timeout.setter + def motion_timeout(self, newval): + self._motion_timeout = assume_units(newval, u.second) + # UNIT CONVERSION METHODS # - def set_scale(self, motor_model): + def _set_scale(self, motor_model): """ Sets the scale factors for this motor channel, based on the model of the attached motor and the specifications of the driver of which @@ -514,8 +1228,42 @@ def set_scale(self, motor_model): break # If we've made it down here, emit a warning that we didn't find the # model. - logger.warning("Scale factors for controller %s and motor %s are " - "unknown", self._apt.model_number, motor_model) + logger.warning( + "Scale factors for controller %s and motor %s are " "unknown", + self._apt.model_number, + motor_model, + ) + + # We copy the docstring below, so it's OK for this method + # to not have a docstring of its own. + # pylint: disable=missing-docstring + def set_scale(self, motor_model): + warnings.warn( + "The set_scale method has been deprecated in favor " + "of the motor_model property.", + DeprecationWarning, + ) + return self._set_scale(motor_model) + + set_scale.__doc__ = _set_scale.__doc__ + + @property + def motor_model(self): + """ + Gets or sets the model name of the attached motor. + Note that the scale factors for this motor channel are based on the model + of the attached motor and the specifications of the driver of which + this is a channel, such that setting a new motor model will update + the scale factors accordingly. + + :type: `str` or `None` + """ + return self._motor_model + + @motor_model.setter + def motor_model(self, newval): + self._set_scale(newval) + self._motor_model = newval # MOTOR COMMANDS # @@ -534,19 +1282,22 @@ def status_bits(self): param2=0x00, dest=self._apt.destination, source=0x01, - data=None + data=None, ) # The documentation claims there are 14 data bytes, but it seems # there are sometimes some extra random ones... - resp_data = self._apt.querypacket(pkt).data[:14] + resp_data = self._apt.querypacket( + pkt, + expect=_cmds.ThorLabsCommands.MOT_GET_POSCOUNTER, + expect_data_len=14, + ).data[:14] # ch_ident, position, enc_count, status_bits - _, _, _, status_bits = struct.unpack( - ' 0)) + status_dict = { + key: (status_bits & bit_mask > 0) for key, bit_mask in self.__STATUS_BIT_MASK.items() - ) + } return status_dict @@ -555,7 +1306,7 @@ def position(self): """ Gets the current position of the specified motor channel - :type: `~quantities.Quantity` + :type: `~pint.Quantity` """ pkt = _packets.ThorLabsPacket( message_id=_cmds.ThorLabsCommands.MOT_REQ_POSCOUNTER, @@ -563,20 +1314,21 @@ def position(self): param2=0x00, dest=self._apt.destination, source=0x01, - data=None + data=None, ) response = self._apt.querypacket( - pkt, expect=_cmds.ThorLabsCommands.MOT_GET_POSCOUNTER) + pkt, expect=_cmds.ThorLabsCommands.MOT_GET_POSCOUNTER, expect_data_len=6 + ) # chan, pos - _, pos = struct.unpack('>> import instruments as ik + >>> import instruments.units as u + + >>> # load the controller, a KDC101 cube + >>> kdc = ik.thorlabs.APTMotorController.open_serial("/dev/ttyUSB0", baud=115200) + >>> # assign a channel to `ch` + >>> ch = kdc.channel[0] + >>> # select the stage that is connected to the controller + >>> ch.motor_model = 'PRM1-Z8' # a rotation stage + + >>> # move to 32 degrees absolute position + >>> ch.move(u.Quantity(32, u.deg)) + + >>> # move 10 degrees forward from current position + >>> ch.move(u.Quantity(10, u.deg), absolute=False) """ # Handle units as follows: # 1. Treat raw numbers as encoder counts. # 2. If units are provided (as a Quantity), check if they're encoder # counts. If they aren't, apply scale factor. - if not isinstance(pos, pq.Quantity): + if not isinstance(pos, u.Quantity): pos_ec = int(pos) else: - if pos.units == pq.counts: + if pos.units == u.counts: pos_ec = int(pos.magnitude) else: - scaled_pos = (pos * self.scale_factors[0]) + scaled_pos = pos * self.scale_factors[0] # Force a unit error. try: - pos_ec = int(scaled_pos.rescale(pq.counts).magnitude) + pos_ec = int(scaled_pos.to(u.counts).magnitude) except: - raise ValueError("Provided units are not compatible " - "with current motor scale factor.") + raise ValueError( + "Provided units are not compatible " + "with current motor scale factor." + ) # Now that we have our position as an integer number of encoder # counts, we're good to move. pkt = _packets.ThorLabsPacket( - message_id=_cmds.ThorLabsCommands.MOT_MOVE_ABSOLUTE if absolute + message_id=_cmds.ThorLabsCommands.MOT_MOVE_ABSOLUTE + if absolute else _cmds.ThorLabsCommands.MOT_MOVE_RELATIVE, param1=None, param2=None, dest=self._apt.destination, source=0x01, - data=struct.pack('>> import instruments as ik - >>> import quantities as pq + >>> import instruments.units as u >>> tm = ik.toptica.TopMode.open_serial('/dev/ttyUSB0', 115200) >>> print(tm.laser[0].wavelength) @@ -383,8 +382,9 @@ def enable(self): @enable.setter def enable(self, newval): if not isinstance(newval, bool): - raise TypeError("Emission status must be a boolean, " - "got: {}".format(type(newval))) + raise TypeError( + "Emission status must be a boolean, " "got: {}".format(type(newval)) + ) self.set("enable-emission", newval) @property diff --git a/instruments/toptica/toptica_utils.py b/instruments/toptica/toptica_utils.py index ead664049..1f87f1cc7 100644 --- a/instruments/toptica/toptica_utils.py +++ b/instruments/toptica/toptica_utils.py @@ -1,11 +1,9 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Contains common utility functions for Toptica-brand instruments """ -from __future__ import absolute_import from datetime import datetime @@ -17,11 +15,11 @@ def convert_toptica_boolean(response): :return: the converted boolean :rtype: bool """ - if response.find('Error: -3') > -1: + if response.find("Error: -3") > -1: return None - elif response.find('f') > -1: + elif response.find("f") > -1: return False - elif response.find('t') > -1: + elif response.find("t") > -1: return True else: raise ValueError("cannot convert: " + str(response) + " to boolean") @@ -37,5 +35,5 @@ def convert_toptica_datetime(response): """ if response.find('""') >= 0: return None - else: - return datetime.strptime(response, "%Y-%m-%d %H:%M:%S") + + return datetime.strptime(response, "%Y-%m-%d %H:%M:%S") diff --git a/instruments/units.py b/instruments/units.py index 632608e8f..dd8b1b868 100644 --- a/instruments/units.py +++ b/instruments/units.py @@ -1,52 +1,14 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing custom units used by various instruments. """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division - -from quantities import Hz, milli, UnitQuantity -from quantities.unitquantity import IrreducibleUnit +import pint # UNITS ####################################################################### -# IRREDUCIBLE UNITS # - - -class UnitLogPower(IrreducibleUnit): - """ - Base unit class for log-power units. The primary example of this - is `dBm`. - """ - _primary_order = 80 # Something large smaller than 99. - -# SPECIFIC UNITS # - -# Define basic unit of log-power, the dBm. - -#: Decibel-milliwatts, a basic unit of logarithmic power. -dBm = UnitLogPower('decibel-milliwatt', symbol='dBm') - -# The Phase Matrix signal generators communicate in units of millihertz (mHz) -# and centibel-milliwatts (cBm). We define those units here to make conversions -# easier later on. - -# TODO: move custom units out to another module. - -mHz = UnitQuantity( - 'millihertz', - milli * Hz, - symbol='mHz', - doc=""" - `~quantities.UnitQuantity` representing millihertz, the native unit of the - Phase Matrix FSW-0020. - """ -) - -#: Centibel-milliwatts, the native log-power unit supported by the -#: Phase Matrix FSW-0020. -cBm = UnitLogPower('centibel-milliwatt', dBm / 10, symbol='cBm') +ureg = pint.get_application_registry() +ureg.define("percent = []") +ureg.define("centibelmilliwatt = 1e-3 watt; logbase: 10; logfactor: 100 = cBm") diff --git a/instruments/util_fns.py b/instruments/util_fns.py index 316343fc9..18ea3316f 100644 --- a/instruments/util_fns.py +++ b/instruments/util_fns.py @@ -1,28 +1,28 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing various utility functions """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division import re from enum import Enum, IntEnum -import quantities as pq +from instruments.units import ureg as u -# FUNCTIONS ################################################################### +# CONSTANTS ################################################################### -# pylint: disable=too-many-arguments +_IDX_REGEX = re.compile(r"([a-zA-Z_][a-zA-Z0-9_]*)\[(-?[0-9]*)\]") + +# FUNCTIONS ################################################################### +# pylint: disable=too-many-arguments def assume_units(value, units): """ If units are not provided for ``value`` (that is, if it is a raw - `float`), then returns a `~quantities.Quantity` with magnitude + `float`), then returns a `~pint.Quantity` with magnitude given by ``value`` and units given by ``units``. :param value: A value that may or may not be unitful. @@ -33,62 +33,66 @@ def assume_units(value, units): ``units``, depending on if ``value`` is unitful. :rtype: `Quantity` """ - if not isinstance(value, pq.Quantity): - value = pq.Quantity(value, units) + if not isinstance(value, u.Quantity): + value = u.Quantity(value, units) return value +def setattr_expression(target, name_expr, value): + """ + Recursively calls getattr/setattr for attribute + names that are miniature expressions with subscripting. + For instance, of the form ``a[0].b``. + """ + # Allow "." in attribute names so that we can set attributes + # recursively. + if "." in name_expr: + # Recursion: We have to strip off a level of getattr. + head, name_expr = name_expr.split(".", 1) + match = _IDX_REGEX.match(head) + if match: + head_name, head_idx = match.groups() + target = getattr(target, head_name)[int(head_idx)] + else: + target = getattr(target, head) + + setattr_expression(target, name_expr, value) + else: + # Base case: We're in the last part of a dot-expression. + match = _IDX_REGEX.match(name_expr) + if match: + name, idx = match.groups() + getattr(target, name)[int(idx)] = value + else: + setattr(target, name_expr, value) + + def convert_temperature(temperature, base): """ - Convert the temperature to the specified base. This is needed because - the package `quantities` does not differentiate between ``degC`` and - ``degK``. + Obsolete with the transition to Pint from Quantities. :param temperature: A quantity with units of Kelvin, Celsius, or Fahrenheit - :type temperature: `quantities.Quantity` + :type temperature: `pint.Quantity` :param base: A temperature unit to convert to - :type base: `unitquantity.UnitTemperature` + :type base: `pint.Quantity` :return: The converted temperature - :rtype: `quantities.Quantity` + :rtype: `pint.Quantity` """ - # quantities reports equivalence between degC and degK, so a string - # comparison is needed - newval = assume_units(temperature, pq.degC) - if newval.units == pq.degF and str(base).split(" ")[1] == 'degC': - return_val = ((newval.magnitude - 32.0) * 5.0 / 9.0) * base - elif str(newval.units).split(" ")[1] == 'K' and str(base).split(" ")[1] == 'degC': - return_val = (newval.magnitude - 273.15) * base - elif str(newval.units).split(" ")[1] == 'K' and base == pq.degF: - return_val = (newval.magnitude / 1.8 - 459 / 57) * base - elif str(newval.units).split(" ")[1] == 'degC' and base == pq.degF: - return_val = (newval.magnitude * 9.0 / 5.0 + 32.0) * base - elif newval.units == pq.degF and str(base).split(" ")[1] == 'K': - return_val = ((newval.magnitude + 459.57) * 5.0 / 9.0) * base - elif str(newval.units).split(" ")[1] == 'degC' and str(base).split(" ")[1] == 'K': - return_val = (newval.magnitude + 273.15) * base - elif str(newval.units).split(" ")[1] == 'degC' and str(base).split(" ")[1] == 'degC': - return_val = newval - elif newval.units == pq.degF and base == pq.degF: - return_val = newval - elif str(newval.units).split(" ")[1] == 'K' and str(base).split(" ")[1] == 'K': - return_val = newval - else: - raise ValueError( - "Unable to convert " + str(newval.units) + " to " + str(base)) - return return_val + newval = assume_units(temperature, u.degC) + return newval.to(base) -def split_unit_str(s, default_units=pq.dimensionless, lookup=None): +def split_unit_str(s, default_units=u.dimensionless, lookup=None): """ Given a string of the form "12 C" or "14.7 GHz", returns a tuple of the numeric part and the unit part, irrespective of how many (if any) whitespace characters appear between. By design, the tuple should be such that it can be unpacked into - :func:`pq.Quantity`:: + :func:`u.Quantity`:: - >>> pq.Quantity(*split_unit_str("1 s")) + >>> u.Quantity(*split_unit_str("1 s")) array(1) * s For this reason, the second element of the tuple may be a unit or @@ -100,7 +104,7 @@ def split_unit_str(s, default_units=pq.dimensionless, lookup=None): :param callable lookup: If specified, this function is called on the units part of the input string. If `None`, no lookup is performed. Lookups are never performed on the default units. - :rtype: `tuple` of a `float` and a `str` or `pq.Quantity` + :rtype: `tuple` of a `float` and a `str` or `u.Quantity` """ if lookup is None: lookup = lambda x: x @@ -116,19 +120,18 @@ def split_unit_str(s, default_units=pq.dimensionless, lookup=None): if match.groups()[1] is None: val, _, units = match.groups() else: - val = float(match.groups()[0]) * 10**float(match.groups()[1][1:]) + val = float(match.groups()[0]) * 10 ** float(match.groups()[1][1:]) units = match.groups()[2] if units is None: return float(val), default_units - else: - return float(val), lookup(units) - else: - try: - return float(s), default_units - except ValueError: - raise ValueError("Could not split '{}' into value " - "and units.".format(repr(s))) + + return float(val), lookup(units) + + try: + return float(s), default_units + except ValueError: + raise ValueError(f"Could not split '{repr(s)}' into value and units.") def rproperty(fget=None, fset=None, doc=None, readonly=False, writeonly=False): @@ -149,20 +152,41 @@ def rproperty(fget=None, fset=None, doc=None, readonly=False, writeonly=False): return property(fget=fget, fset=None, doc=doc) elif writeonly: return property(fget=None, fset=fset, doc=doc) - else: - return property(fget=fget, fset=fset, doc=doc) + return property(fget=fget, fset=fset, doc=doc) -def bool_property(name, inst_true, inst_false, doc=None, readonly=False, - writeonly=False, set_fmt="{} {}"): + +def bool_property( + command, + set_cmd=None, + inst_true="ON", + inst_false="OFF", + doc=None, + readonly=False, + writeonly=False, + set_fmt="{} {}", +): """ Called inside of SCPI classes to instantiate boolean properties of the device cleanly. For example: - >>> my_property = bool_property("BEST:PROPERTY", "ON", "OFF") # doctest: +SKIP - - :param str name: Name of the SCPI command corresponding to this property. + >>> my_property = bool_property( + ... "BEST:PROPERTY", + ... inst_true="ON", + ... inst_false="OFF" + ... ) # doctest: +SKIP + + This will result in "BEST:PROPERTY ON" or "BEST:PROPERTY OFF" being sent + when setting, and "BEST:PROPERTY?" being sent when getting. + + :param str command: Name of the SCPI command corresponding to this property. + If parameter set_cmd is not specified, then this parameter is also used + for both getting and setting. + :param str set_cmd: If not `None`, this parameter sets the command string + to be used when sending commands with no return values to the + instrument. This allows for non-symmetric properties that have different + strings for getting vs setting a property. :param str inst_true: String returned and accepted by the instrument for `True` values. :param str inst_false: String returned and accepted by the instrument for @@ -179,21 +203,34 @@ def bool_property(name, inst_true, inst_false, doc=None, readonly=False, """ def _getter(self): - return self.query(name + "?").strip() == inst_true + return self.query(command + "?").strip() == inst_true def _setter(self, newval): if not isinstance(newval, bool): - raise TypeError("Bool properties must be specified with a " - "boolean value") - self.sendcmd(set_fmt.format(name, inst_true if newval else inst_false)) - - return rproperty(fget=_getter, fset=_setter, doc=doc, readonly=readonly, - writeonly=writeonly) + raise TypeError("Bool properties must be specified with a " "boolean value") + self.sendcmd( + set_fmt.format( + command if set_cmd is None else set_cmd, + inst_true if newval else inst_false, + ) + ) + + return rproperty( + fget=_getter, fset=_setter, doc=doc, readonly=readonly, writeonly=writeonly + ) -def enum_property(name, enum, doc=None, input_decoration=None, - output_decoration=None, readonly=False, writeonly=False, - set_fmt="{} {}"): +def enum_property( + command, + enum, + set_cmd=None, + doc=None, + input_decoration=None, + output_decoration=None, + readonly=False, + writeonly=False, + set_fmt="{} {}", +): """ Called inside of SCPI classes to instantiate Enum properties of the device cleanly. @@ -203,7 +240,13 @@ def enum_property(name, enum, doc=None, input_decoration=None, Example: my_property = bool_property("BEST:PROPERTY", enum_class) - :param str name: Name of the SCPI command corresponding to this property. + :param str command: Name of the SCPI command corresponding to this property. + If parameter set_cmd is not specified, then this parameter is also used + for both getting and setting. + :param str set_cmd: If not `None`, this parameter sets the command string + to be used when sending commands with no return values to the + instrument. This allows for non-symmetric properties that have different + strings for getting vs setting a property. :param type enum: Class derived from `Enum` representing valid values. :param callable input_decoration: Function called on responses from the instrument before passing to user code. @@ -218,7 +261,12 @@ def enum_property(name, enum, doc=None, input_decoration=None, non-query to the instrument. The default is "{} {}" which places a space between the SCPI command the associated parameter. By switching to "{}={}" an equals sign would instead be used as the separator. + :param str get_cmd: If not `None`, this parameter sets the command string + to be used when reading/querying from the instrument. If used, the name + parameter is still used to set the command for pure-write commands to + the instrument. """ + def _in_decor_fcn(val): if input_decoration is None: return val @@ -234,7 +282,7 @@ def _out_decor_fcn(val): return output_decoration(val) def _getter(self): - return enum(_in_decor_fcn(self.query("{}?".format(name)).strip())) + return enum(_in_decor_fcn(self.query(f"{command}?").strip())) def _setter(self, newval): try: # First assume newval is Enum.value @@ -244,19 +292,38 @@ def _setter(self, newval): newval = enum(newval) except ValueError: raise ValueError("Enum property new value not in enum.") - self.sendcmd(set_fmt.format(name, _out_decor_fcn(enum(newval).value))) - - return rproperty(fget=_getter, fset=_setter, doc=doc, readonly=readonly, - writeonly=writeonly) + self.sendcmd( + set_fmt.format( + command if set_cmd is None else set_cmd, + _out_decor_fcn(enum(newval).value), + ) + ) + + return rproperty( + fget=_getter, fset=_setter, doc=doc, readonly=readonly, writeonly=writeonly + ) -def unitless_property(name, format_code='{:e}', doc=None, readonly=False, - writeonly=False, set_fmt="{} {}"): +def unitless_property( + command, + set_cmd=None, + format_code="{:e}", + doc=None, + readonly=False, + writeonly=False, + set_fmt="{} {}", +): """ Called inside of SCPI classes to instantiate properties with unitless numeric values. - :param str name: Name of the SCPI command corresponding to this property. + :param str command: Name of the SCPI command corresponding to this property. + If parameter set_cmd is not specified, then this parameter is also used + for both getting and setting. + :param str set_cmd: If not `None`, this parameter sets the command string + to be used when sending commands with no return values to the + instrument. This allows for non-symmetric properties that have different + strings for getting vs setting a property. :param str format_code: Argument to `str.format` used in sending values to the instrument. :param str doc: Docstring to be associated with the new property. @@ -271,29 +338,44 @@ def unitless_property(name, format_code='{:e}', doc=None, readonly=False, """ def _getter(self): - raw = self.query("{}?".format(name)) + raw = self.query(f"{command}?") return float(raw) def _setter(self, newval): - if isinstance(newval, pq.Quantity): - if newval.units == pq.dimensionless: + if isinstance(newval, u.Quantity): + if newval.units == u.dimensionless: newval = float(newval.magnitude) else: raise ValueError strval = format_code.format(newval) - self.sendcmd(set_fmt.format(name, strval)) + self.sendcmd(set_fmt.format(command if set_cmd is None else set_cmd, strval)) - return rproperty(fget=_getter, fset=_setter, doc=doc, readonly=readonly, - writeonly=writeonly) + return rproperty( + fget=_getter, fset=_setter, doc=doc, readonly=readonly, writeonly=writeonly + ) -def int_property(name, format_code='{:d}', doc=None, readonly=False, - writeonly=False, valid_set=None, set_fmt="{} {}"): +def int_property( + command, + set_cmd=None, + format_code="{:d}", + doc=None, + readonly=False, + writeonly=False, + valid_set=None, + set_fmt="{} {}", +): """ Called inside of SCPI classes to instantiate properties with unitless numeric values. - :param str name: Name of the SCPI command corresponding to this property. + :param str command: Name of the SCPI command corresponding to this property. + If parameter set_cmd is not specified, then this parameter is also used + for both getting and setting. + :param str set_cmd: If not `None`, this parameter sets the command string + to be used when sending commands with no return values to the + instrument. This allows for non-symmetric properties that have different + strings for getting vs setting a property. :param str format_code: Argument to `str.format` used in sending values to the instrument. :param str doc: Docstring to be associated with the new property. @@ -310,13 +392,19 @@ def int_property(name, format_code='{:d}', doc=None, readonly=False, """ def _getter(self): - raw = self.query("{}?".format(name)) + raw = self.query(f"{command}?") return int(raw) + if valid_set is None: + def _setter(self, newval): strval = format_code.format(newval) - self.sendcmd(set_fmt.format(name, strval)) + self.sendcmd( + set_fmt.format(command if set_cmd is None else set_cmd, strval) + ) + else: + def _setter(self, newval): if newval not in valid_set: raise ValueError( @@ -324,16 +412,28 @@ def _setter(self, newval): "must be one of {}.".format(newval, valid_set) ) strval = format_code.format(newval) - self.sendcmd(set_fmt.format(name, strval)) + self.sendcmd( + set_fmt.format(command if set_cmd is None else set_cmd, strval) + ) - return rproperty(fget=_getter, fset=_setter, doc=doc, readonly=readonly, - writeonly=writeonly) + return rproperty( + fget=_getter, fset=_setter, doc=doc, readonly=readonly, writeonly=writeonly + ) -def unitful_property(name, units, format_code='{:e}', doc=None, - input_decoration=None, output_decoration=None, - readonly=False, writeonly=False, set_fmt="{} {}", - valid_range=(None, None)): +def unitful_property( + command, + units, + set_cmd=None, + format_code="{:e}", + doc=None, + input_decoration=None, + output_decoration=None, + readonly=False, + writeonly=False, + set_fmt="{} {}", + valid_range=(None, None), +): """ Called inside of SCPI classes to instantiate properties with unitful numeric values. This function assumes that the instrument only accepts @@ -342,7 +442,13 @@ def unitful_property(name, units, format_code='{:e}', doc=None, for instruments where the units can change dynamically due to front-panel interaction or due to remote commands. - :param str name: Name of the SCPI command corresponding to this property. + :param str command: Name of the SCPI command corresponding to this property. + If parameter set_cmd is not specified, then this parameter is also used + for both getting and setting. + :param str set_cmd: If not `None`, this parameter sets the command string + to be used when sending commands with no return values to the + instrument. This allows for non-symmetric properties that have different + strings for getting vs setting a property. :param units: Units to assume in sending and receiving magnitudes to and from the instrument. :param str format_code: Argument to `str.format` used in sending the @@ -367,6 +473,7 @@ def unitful_property(name, units, format_code='{:e}', doc=None, The valid set is inclusive of the values provided. :type valid_range: `tuple` or `list` of `int` or `float` """ + def _in_decor_fcn(val): if input_decoration is None: return val @@ -382,36 +489,49 @@ def _out_decor_fcn(val): return output_decoration(val) def _getter(self): - raw = _in_decor_fcn(self.query("{}?".format(name))) - return pq.Quantity(*split_unit_str(raw, units)).rescale(units) + raw = _in_decor_fcn(self.query(f"{command}?")) + return u.Quantity(*split_unit_str(raw, units)).to(units) def _setter(self, newval): min_value, max_value = valid_range if min_value is not None: - if hasattr(min_value, '__call__'): - min_value = min_value(self) + if callable(min_value): + min_value = min_value(self) # pylint: disable=not-callable if newval < min_value: - raise ValueError("Unitful quantity is too low. Got {}, minimum " - "value is {}".format(newval, min_value)) + raise ValueError( + f"Unitful quantity is too low. Got {newval}, " + f"minimum value is {min_value}" + ) if max_value is not None: - if hasattr(max_value, '__call__'): - max_value = max_value(self) + if callable(max_value): + max_value = max_value(self) # pylint: disable=not-callable if newval > max_value: - raise ValueError("Unitful quantity is too high. Got {}, maximum" - " value is {}".format(newval, max_value)) + raise ValueError( + f"Unitful quantity is too high. Got {newval}, " + f"maximum value is {max_value}" + ) # Rescale to the correct unit before printing. This will also # catch bad units. - strval = format_code.format( - assume_units(newval, units).rescale(units).item()) - self.sendcmd(set_fmt.format(name, _out_decor_fcn(strval))) + strval = format_code.format(assume_units(newval, units).to(units).magnitude) + self.sendcmd( + set_fmt.format( + command if set_cmd is None else set_cmd, _out_decor_fcn(strval) + ) + ) - return rproperty(fget=_getter, fset=_setter, doc=doc, readonly=readonly, - writeonly=writeonly) + return rproperty( + fget=_getter, fset=_setter, doc=doc, readonly=readonly, writeonly=writeonly + ) -def bounded_unitful_property(name, units, min_fmt_str="{}:MIN?", - max_fmt_str="{}:MAX?", - valid_range=("query", "query"), **kwargs): +def bounded_unitful_property( + command, + units, + min_fmt_str="{}:MIN?", + max_fmt_str="{}:MAX?", + valid_range=("query", "query"), + **kwargs, +): """ Called inside of SCPI classes to instantiate properties with unitful numeric values which have upper and lower bounds. This function in turn calls @@ -423,7 +543,13 @@ def bounded_unitful_property(name, units, min_fmt_str="{}:MIN?", the one created by `unitful_property`, one for the minimum value, and one for the maximum value. - :param str name: Name of the SCPI command corresponding to this property. + :param str command: Name of the SCPI command corresponding to this property. + If parameter set_cmd is not specified, then this parameter is also used + for both getting and setting. + :param str set_cmd: If not `None`, this parameter sets the command string + to be used when sending commands with no return values to the + instrument. This allows for non-symmetric properties that have different + strings for getting vs setting a property. :param units: Units to assume in sending and receiving magnitudes to and from the instrument. :param str min_fmt_str: Specify the string format to use when sending a @@ -449,34 +575,51 @@ def bounded_unitful_property(name, units, min_fmt_str="{}:MIN?", def _min_getter(self): if valid_range[0] == "query": - return pq.Quantity(*split_unit_str(self.query(min_fmt_str.format(name)), units)) - else: - return assume_units(valid_range[0], units).rescale(units) + return u.Quantity( + *split_unit_str(self.query(min_fmt_str.format(command)), units) + ) + + return assume_units(valid_range[0], units).to(units) def _max_getter(self): if valid_range[1] == "query": - return pq.Quantity(*split_unit_str(self.query(max_fmt_str.format(name)), units)) - else: - return assume_units(valid_range[1], units).rescale(units) + return u.Quantity( + *split_unit_str(self.query(max_fmt_str.format(command)), units) + ) + + return assume_units(valid_range[1], units).to(units) new_range = ( None if valid_range[0] is None else _min_getter, - None if valid_range[1] is None else _max_getter + None if valid_range[1] is None else _max_getter, ) return ( - unitful_property(name, units, valid_range=new_range, **kwargs), + unitful_property(command, units, valid_range=new_range, **kwargs), property(_min_getter) if valid_range[0] is not None else None, - property(_max_getter) if valid_range[1] is not None else None + property(_max_getter) if valid_range[1] is not None else None, ) -def string_property(name, bookmark_symbol='"', doc=None, readonly=False, - writeonly=False, set_fmt="{} {}{}{}"): +def string_property( + command, + set_cmd=None, + bookmark_symbol='"', + doc=None, + readonly=False, + writeonly=False, + set_fmt="{} {}{}{}", +): """ Called inside of SCPI classes to instantiate properties with a string value. - :param str name: Name of the SCPI command corresponding to this property. + :param str command: Name of the SCPI command corresponding to this property. + If parameter set_cmd is not specified, then this parameter is also used + for both getting and setting. + :param str set_cmd: If not `None`, this parameter sets the command string + to be used when sending commands with no return values to the + instrument. This allows for non-symmetric properties that have different + strings for getting vs setting a property. :param str doc: Docstring to be associated with the new property. :param bool readonly: If `True`, the returned property does not have a setter. @@ -492,22 +635,31 @@ def string_property(name, bookmark_symbol='"', doc=None, readonly=False, bookmark_length = len(bookmark_symbol) def _getter(self): - string = self.query("{}?".format(name)) - string = string[ - bookmark_length:-bookmark_length] if bookmark_length > 0 else string + string = self.query(f"{command}?") + string = ( + string[bookmark_length:-bookmark_length] if bookmark_length > 0 else string + ) return string def _setter(self, newval): self.sendcmd( - set_fmt.format(name, bookmark_symbol, newval, bookmark_symbol)) + set_fmt.format( + command if set_cmd is None else set_cmd, + bookmark_symbol, + newval, + bookmark_symbol, + ) + ) + + return rproperty( + fget=_getter, fset=_setter, doc=doc, readonly=readonly, writeonly=writeonly + ) - return rproperty(fget=_getter, fset=_setter, doc=doc, readonly=readonly, - writeonly=writeonly) # CLASSES ##################################################################### -class ProxyList(object): +class ProxyList: """ This is a special class used to generate lists of objects where the valid keys are defined by the `valid_set` init parameter. This allows an @@ -535,9 +687,10 @@ def __init__(self, parent, proxy_cls, valid_set): self._valid_set = valid_set # FIXME: This only checks the next level up the chain! - if hasattr(valid_set, '__bases__'): + if hasattr(valid_set, "__bases__"): self._isenum = (Enum in valid_set.__bases__) or ( - IntEnum in valid_set.__bases__) + IntEnum in valid_set.__bases__ + ) else: self._isenum = False @@ -557,14 +710,15 @@ def __getitem__(self, idx): except ValueError: pass if not isinstance(idx, self._valid_set): - raise IndexError("Index out of range. Must be " - "in {}.".format(self._valid_set)) - else: - idx = idx.value + raise IndexError( + "Index out of range. Must be " "in {}.".format(self._valid_set) + ) + idx = idx.value else: if idx not in self._valid_set: - raise IndexError("Index out of range. Must be " - "in {}.".format(self._valid_set)) + raise IndexError( + "Index out of range. Must be " "in {}.".format(self._valid_set) + ) return self._proxy_cls(self._parent, idx) def __len__(self): diff --git a/instruments/yokogawa/__init__.py b/instruments/yokogawa/__init__.py index fa7b6084f..9f0b4fe5c 100644 --- a/instruments/yokogawa/__init__.py +++ b/instruments/yokogawa/__init__.py @@ -1,9 +1,8 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module containing Yokogawa instruments """ -from __future__ import absolute_import +from .yokogawa6370 import Yokogawa6370 from .yokogawa7651 import Yokogawa7651 diff --git a/instruments/yokogawa/yokogawa6370.py b/instruments/yokogawa/yokogawa6370.py new file mode 100644 index 000000000..6c4ffbc02 --- /dev/null +++ b/instruments/yokogawa/yokogawa6370.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python +""" +Provides support for the Yokogawa 6370 optical spectrum analyzer. +""" + +# IMPORTS ##################################################################### + + +from enum import IntEnum, Enum + +from instruments.units import ureg as u + +from instruments.abstract_instruments import OpticalSpectrumAnalyzer +from instruments.util_fns import ( + enum_property, + unitful_property, + unitless_property, + bounded_unitful_property, + ProxyList, +) + + +# CLASSES ##################################################################### + + +class Yokogawa6370(OpticalSpectrumAnalyzer): + + """ + The Yokogawa 6370 is a optical spectrum analyzer. + + Example usage: + + >>> import instruments as ik + >>> import instruments.units as u + >>> inst = ik.yokogawa.Yokogawa6370.open_visa('TCPIP0:192.168.0.35') + >>> inst.start_wl = 1030e-9 * u.m + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Set data Format to binary + self.sendcmd(":FORMat:DATA REAL,64") # TODO: Find out where we want this + + # INNER CLASSES # + + class Channel(OpticalSpectrumAnalyzer.Channel): + + """ + Class representing the channels on the Yokogawa 6370. + + This class inherits from `OpticalSpectrumAnalyzer.Channel`. + + .. warning:: This class should NOT be manually created by the user. It + is designed to be initialized by the `Yokogawa6370` class. + """ + + def __init__(self, parent, idx): + self._parent = parent + self._name = idx + + # METHODS # + + def data(self, bin_format=True): + cmd = f":TRAC:Y? {self._name}" + self._parent.sendcmd(cmd) + data = self._parent.binblockread(data_width=8, fmt=">> import instruments as ik + >>> osa = ik.yokogawa.Yokogawa6370.open_gpibusb('/dev/ttyUSB0') + >>> dat = osa.channel["A"].data # Gets the data of channel 0 + + :rtype: `list`[`~Yokogawa6370.Channel`] + """ + return ProxyList(self, Yokogawa6370.Channel, Yokogawa6370.Traces) + + start_wl, start_wl_min, start_wl_max = bounded_unitful_property( + ":SENS:WAV:STAR", + u.meter, + doc=""" + The start wavelength in m. + """, + valid_range=(600e-9, 1700e-9), + ) + + stop_wl, stop_wl_min, stop_wl_max = bounded_unitful_property( + ":SENS:WAV:STOP", + u.meter, + doc=""" + The stop wavelength in m. + """, + valid_range=(600e-9, 1700e-9), + ) + + bandwidth = unitful_property( + ":SENS:BAND:RES", + u.meter, + doc=""" + The bandwidth in m. + """, + ) + + span = unitful_property( + ":SENS:WAV:SPAN", + u.meter, + doc=""" + A floating point property that controls the wavelength span in m. + """, + ) + + center_wl = unitful_property( + ":SENS:WAV:CENT", + u.meter, + doc=""" + A floating point property that controls the center wavelength m. + """, + ) + + points = unitless_property( + ":SENS:SWE:POIN", + doc=""" + An integer property that controls the number of points in a trace. + """, + ) + + sweep_mode = enum_property( + ":INIT:SMOD", + SweepModes, + input_decoration=int, + doc=""" + A property to control the Sweep Mode as one of Yokogawa6370.SweepMode. + Effective only after a self.start_sweep().""", + ) + + active_trace = enum_property( + ":TRAC:ACTIVE", + Traces, + doc=""" + The active trace of the OSA of enum Yokogawa6370.Traces. Determines the + result of Yokogawa6370.data() and Yokogawa6370.wavelength().""", + ) + + # METHODS # + + def data(self): + """ + Function to query the active Trace data of the OSA. + """ + return self.channel[self.active_trace].data() + + def wavelength(self): + """ + Query the wavelength axis of the active trace. + """ + return self.channel[self.active_trace].wavelength() + + def start_sweep(self): + """ + Triggering function for the Yokogawa 6370. + + After changing the sweep mode, the device needs to be triggered before it will update. + """ + self.sendcmd("*CLS;:init") diff --git a/instruments/yokogawa/yokogawa7651.py b/instruments/yokogawa/yokogawa7651.py index c5779ab06..5a60a1725 100644 --- a/instruments/yokogawa/yokogawa7651.py +++ b/instruments/yokogawa/yokogawa7651.py @@ -1,22 +1,16 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Provides support for the Yokogawa 7651 power supply. """ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division from enum import IntEnum -import quantities as pq +from instruments.units import ureg as u -from instruments.abstract_instruments import ( - PowerSupply, - PowerSupplyChannel, -) +from instruments.abstract_instruments import PowerSupply from instruments.abstract_instruments import Instrument from instruments.util_fns import assume_units, ProxyList @@ -31,22 +25,19 @@ class Yokogawa7651(PowerSupply, Instrument): Example usage: >>> import instruments as ik - >>> import quantities as pq + >>> import instruments.units as u >>> inst = ik.yokogawa.Yokogawa7651.open_gpibusb("/dev/ttyUSB0", 1) - >>> inst.voltage = 10 * pq.V + >>> inst.voltage = 10 * u.V """ - def __init__(self, filelike): - super(Yokogawa7651, self).__init__(filelike) - # INNER CLASSES # - class Channel(PowerSupplyChannel): + class Channel(PowerSupply.Channel): """ Class representing the only channel on the Yokogawa 7651. - This class inherits from `PowerSupplyChannel`. + This class inherits from `PowerSupply.Channel`. .. warning:: This class should NOT be manually created by the user. It is designed to be initialized by the `Yokogawa7651` class. @@ -68,15 +59,18 @@ def mode(self): :type: `Yokogawa7651.Mode` """ - raise NotImplementedError('This instrument does not support ' - 'querying the operation mode.') + raise NotImplementedError( + "This instrument does not support " "querying the operation mode." + ) @mode.setter def mode(self, newval): if not isinstance(newval, Yokogawa7651.Mode): - raise TypeError("Mode setting must be a `Yokogawa7651.Mode` " - "value, got {} instead.".format(type(newval))) - self._parent.sendcmd('F{};'.format(newval.value)) + raise TypeError( + "Mode setting must be a `Yokogawa7651.Mode` " + "value, got {} instead.".format(type(newval)) + ) + self._parent.sendcmd(f"F{newval.value};") self._parent.trigger() @property @@ -87,18 +81,20 @@ def voltage(self): Querying the voltage is not supported by this instrument. - :units: As specified (if a `~quantities.quantity.Quantity`) or + :units: As specified (if a `~pint.Quantity`) or assumed to be of units Volts. - :type: `~quantities.quantity.Quantity` with units Volt + :type: `~pint.Quantity` with units Volt """ - raise NotImplementedError('This instrument does not support ' - 'querying the output voltage setting.') + raise NotImplementedError( + "This instrument does not support " + "querying the output voltage setting." + ) @voltage.setter def voltage(self, newval): - newval = assume_units(newval, pq.volt).rescale(pq.volt).magnitude + newval = assume_units(newval, u.volt).to(u.volt).magnitude self.mode = self._parent.Mode.voltage - self._parent.sendcmd('SA{};'.format(newval)) + self._parent.sendcmd(f"SA{newval};") self._parent.trigger() @property @@ -109,18 +105,20 @@ def current(self): Querying the current is not supported by this instrument. - :units: As specified (if a `~quantities.quantity.Quantity`) or + :units: As specified (if a `~pint.Quantity`) or assumed to be of units Amps. - :type: `~quantities.quantity.Quantity` with units Amp + :type: `~pint.Quantity` with units Amp """ - raise NotImplementedError('This instrument does not support ' - 'querying the output current setting.') + raise NotImplementedError( + "This instrument does not support " + "querying the output current setting." + ) @current.setter def current(self, newval): - newval = assume_units(newval, pq.amp).rescale(pq.amp).magnitude + newval = assume_units(newval, u.amp).to(u.amp).magnitude self.mode = self._parent.Mode.current - self._parent.sendcmd('SA{};'.format(newval)) + self._parent.sendcmd(f"SA{newval};") self._parent.trigger() @property @@ -133,16 +131,17 @@ def output(self): :type: `bool` """ - raise NotImplementedError('This instrument does not support ' - 'querying the output status.') + raise NotImplementedError( + "This instrument does not support " "querying the output status." + ) @output.setter def output(self, newval): if newval is True: - self._parent.sendcmd('O1;') + self._parent.sendcmd("O1;") self._parent.trigger() else: - self._parent.sendcmd('O0;') + self._parent.sendcmd("O0;") self._parent.trigger() # ENUMS # @@ -151,6 +150,7 @@ class Mode(IntEnum): """ Enum containing valid output modes for the Yokogawa 7651 """ + voltage = 1 current = 5 @@ -180,12 +180,13 @@ def voltage(self): Querying the voltage is not supported by this instrument. - :units: As specified (if a `~quantities.quantity.Quantity`) or assumed + :units: As specified (if a `~pint.Quantity`) or assumed to be of units Volts. - :type: `~quantities.quantity.Quantity` with units Volt + :type: `~pint.Quantity` with units Volt """ - raise NotImplementedError('This instrument does not support querying ' - 'the output voltage setting.') + raise NotImplementedError( + "This instrument does not support querying " "the output voltage setting." + ) @voltage.setter def voltage(self, newval): @@ -198,12 +199,13 @@ def current(self): Querying the current is not supported by this instrument. - :units: As specified (if a `~quantities.quantity.Quantity`) or assumed + :units: As specified (if a `~pint.Quantity`) or assumed to be of units Amps. - :type: `~quantities.quantity.Quantity` with units Amp + :type: `~pint.Quantity` with units Amp """ - raise NotImplementedError('This instrument does not support querying ' - 'the output current setting.') + raise NotImplementedError( + "This instrument does not support querying " "the output current setting." + ) @current.setter def current(self, newval): @@ -218,4 +220,4 @@ def trigger(self): After changing any parameters of the instrument (for example, output voltage), the device needs to be triggered before it will update. """ - self.sendcmd('E;') + self.sendcmd("E;") diff --git a/license/AUTHOR.TXT b/license/AUTHOR.TXT index aa0485eef..b8dfa8a70 100644 --- a/license/AUTHOR.TXT +++ b/license/AUTHOR.TXT @@ -7,4 +7,4 @@ Steven Casagrande scasagrande@galvant.ca twitter.com/stevecasagrande -2012-2016 +2012-2022 diff --git a/matlab/open_instrument.m b/matlab/open_instrument.m index e2309911f..6b83523ea 100644 --- a/matlab/open_instrument.m +++ b/matlab/open_instrument.m @@ -8,23 +8,23 @@ % @classmethods. This presents two drawbacks: first, we need to manage % the Python globals() dict directly, and second, we need to do string % manipulation to make the line of source code to evaluate. - + % To manage globals() ourselves, we need to make a new dict() that we will % pass to py.eval. namespace = py.dict(); - + % Next, py.eval doesn't respect MATLAB's import py.* command-form function. % Thus, we need to use the __import__ built-in function to return the module % object for InstrumentKit. We'll save it directly into our new namespace, % so that it becomes a global for the next py.eval. Recall that d{'x'} on the % MATLAB side corresponds to d['x'] on the Python side, for d a Python dict(). namespace{'ik'} = py.eval('__import__("instruments")', namespace); - + % Finally, we're equipped to run the open_from_uri @classmethod. To do so, % we want to evaluate a line that looks like: % ik.holzworth.Holzworth.HS9000.open_from_uri(r"serial:/dev/ttyUSB0") % We use r to cut down on accidental escaping errors, but importantly, this will % do *nothing* to cut down intentional abuse of eval. instrument = py.eval(['ik.' name '.open_from_uri(r"' uri '")'], namespace); - + end diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..346ac06f8 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +[build-system] +requires = ["setuptools>=45", "wheel", "setuptools_scm>=6.2"] +build-backend = "setuptools.build_meta" + +[tool.setuptools_scm] +write_to = "instruments/_version.py" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 113c7a6a3..000000000 --- a/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -numpy -pyserial -quantities -future>=0.15 -enum34 -python-vxi11>=0.8 -pyusb -python-usbtmc -pyyaml diff --git a/setup.cfg b/setup.cfg index 5e4090017..faf95b21b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,58 @@ -[wheel] +[metadata] +name = instrumentkit +version = attr: instruments._version.version +description = Test and measurement communication library +author = Steven Casagrande +author_email = stevencasagrande@gmail.com +url = https://www.github.com/Galvant/InstrumentKit +long_description = file: README.rst +long_description_content_type = text/x-rst +license = AGPLv3 +classifiers = + Development Status :: 4 - Beta + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + Operating System :: OS Independent + License :: OSI Approved :: GNU Affero General Public License v3 + Intended Audience :: Science/Research + Intended Audience :: Manufacturing + Topic :: Scientific/Engineering + Topic :: Scientific/Engineering :: Interface Engine/Protocol Translator + Topic :: Software Development :: Libraries + +[options] +include_package_data = True +packages = find: +install_requires = + pint>=0.16.1 + pyserial>=3.3 + python-usbtmc + python-vxi11>=0.8 + pyusb>=1.0 + pyvisa>=1.9 + ruamel.yaml>=0.16,<0.17 + +[options.extras_require] +numpy = numpy +dev = + coverage + hypothesis~=5.49.0 + mock + pytest-cov + pytest-mock + pytest-xdist + pytest~=6.2.5 + pyvisa-sim + six + +[options.packages.find] +exclude = + instruments.tests + +[bdist_wheel] universal = 1 diff --git a/setup.py b/setup.py deleted file mode 100644 index c928c9608..000000000 --- a/setup.py +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -Python setup.py file for the InstrumentKit project -""" - -# IMPORTS #################################################################### - -import codecs -import os -import re - -from setuptools import setup, find_packages - -# SETUP VALUES ############################################################### - -NAME = "instruments" -PACKAGES = find_packages() -META_PATH = os.path.join("instruments", "__init__.py") -CLASSIFIERS = [ - "Development Status :: 4 - Beta", - "Programming Language :: Python", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", - "Operating System :: OS Independent", - "License :: OSI Approved :: GNU Affero General Public License v3", - "Intended Audience :: Science/Research", - "Intended Audience :: Manufacturing", - "Topic :: Scientific/Engineering", - "Topic :: Scientific/Engineering :: Interface Engine/Protocol Translator", - "Topic :: Software Development :: Libraries" -] -INSTALL_REQUIRES = [ - "numpy", - "pyserial", - "quantities", - "enum34", - "future", - "python-vxi11", - "python-usbtmc", - "pyusb", - "pyyaml" -] -EXTRAS_REQUIRE = { - 'VISA': ["pyvisa"] -} - -# HELPER FUNCTONS ############################################################ - -HERE = os.path.abspath(os.path.dirname(__file__)) - - -def read(*parts): - """ - Build an absolute path from *parts* and and return the contents of the - resulting file. Assume UTF-8 encoding. - """ - with codecs.open(os.path.join(HERE, *parts), "rb", "utf-8") as f: - return f.read() - -META_FILE = read(META_PATH) - - -def find_meta(meta): - """ - Extract __*meta*__ from META_FILE. - """ - meta_match = re.search( - r"^__{meta}__ = ['\"]([^'\"]*)['\"]".format(meta=meta), - META_FILE, re.M - ) - if meta_match: - return meta_match.group(1) - raise RuntimeError("Unable to find __{meta}__ string.".format(meta=meta)) - -# MAIN ####################################################################### - -if __name__ == "__main__": - setup( - name=find_meta("title"), - version=find_meta("version"), - url=find_meta("uri"), - author=find_meta("author"), - author_email=find_meta("email"), - packages=PACKAGES, - install_requires=INSTALL_REQUIRES, - extras_require=EXTRAS_REQUIRE, - description=find_meta("description"), - classifiers=CLASSIFIERS - ) diff --git a/tox.ini b/tox.ini index ae996789c..a59a40409 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,16 @@ [tox] -envlist = py27,py33,py34,py35 +envlist = py{36,37,38,39,310},py{36,37,38,39,310}-numpy,pylint +isolated_build = true + [testenv] -deps = -rdev-requirements.txt -commands = nosetests +extras = dev +deps = + numpy: numpy +commands = pytest -n auto --cov=instruments {toxinidir}/instruments/tests/ {posargs:} + +[testenv:precommit] +basepython = python3 +deps = + pre-commit +commands = + pre-commit run --all --show-diff-on-failure