Repository: Galvant/InstrumentKit Branch: main Commit: c976204e016b Files: 347 Total size: 2.0 MB Directory structure: gitextract_gysdejrm/ ├── .coveragerc ├── .github/ │ └── workflows/ │ ├── deploy.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── README.rst ├── doc/ │ ├── Makefile │ ├── examples/ │ │ ├── .ipynb_checkpoints/ │ │ │ └── ex_keithley6514-checkpoint.ipynb │ │ ├── ex_generic_scpi.ipynb │ │ ├── ex_generic_scpi.py │ │ ├── ex_hp3325.py │ │ ├── ex_hp3456.out │ │ ├── ex_hp3456.py │ │ ├── ex_keithley195.ipynb │ │ ├── ex_keithley195.py │ │ ├── ex_keithley6514.ipynb │ │ ├── ex_maui.ipynb │ │ ├── ex_oscilloscope_waveform.ipynb │ │ ├── ex_oscilloscope_waveform.py │ │ ├── ex_qubitekk_gui.py │ │ ├── ex_qubitekkcc.py │ │ ├── ex_qubitekkcc_simple.py │ │ ├── ex_tekdpo70000.ipynb │ │ ├── ex_thorlabslcc.py │ │ ├── ex_thorlabssc10.py │ │ ├── ex_thorlabstc200.py │ │ ├── ex_topticatopmode.py │ │ ├── example2.py │ │ ├── minghe/ │ │ │ └── ex_minghe_mhs5200.py │ │ ├── qubitekk/ │ │ │ └── ex_qubitekk_mc1.py │ │ ├── srs_DG645.ipynb │ │ └── srs_DG645.py │ ├── make.bat │ └── source/ │ ├── acknowledgements.rst │ ├── apiref/ │ │ ├── agilent.rst │ │ ├── aimtti.rst │ │ ├── comet.rst │ │ ├── config.rst │ │ ├── delta_elektronika.rst │ │ ├── dressler.rst │ │ ├── fluke.rst │ │ ├── generic_scpi.rst │ │ ├── gentec-eo.rst │ │ ├── glassman.rst │ │ ├── hcp.rst │ │ ├── holzworth.rst │ │ ├── hp.rst │ │ ├── index.rst │ │ ├── instrument.rst │ │ ├── keithley.rst │ │ ├── lakeshore.rst │ │ ├── mettler_toledo.rst │ │ ├── minghe.rst │ │ ├── newport.rst │ │ ├── ondax.rst │ │ ├── oxford.rst │ │ ├── pfeiffer.rst │ │ ├── phasematrix.rst │ │ ├── picowatt.rst │ │ ├── qubitekk.rst │ │ ├── rigol.rst │ │ ├── srs.rst │ │ ├── sunpower.rst │ │ ├── tektronix.rst │ │ ├── teledyne.rst │ │ ├── thorlabs.rst │ │ ├── toptica.rst │ │ └── yokogawa.rst │ ├── conf.py │ ├── devguide/ │ │ ├── code_style.rst │ │ ├── design_philosophy.rst │ │ ├── index.rst │ │ ├── testing.rst │ │ └── util_fns.rst │ ├── index.rst │ └── intro.rst ├── license/ │ ├── AUTHOR.TXT │ └── LICENSE.TXT ├── matlab/ │ ├── matlab-example.ipynb │ └── open_instrument.m ├── pyproject.toml ├── src/ │ └── instruments/ │ ├── __init__.py │ ├── abstract_instruments/ │ │ ├── __init__.py │ │ ├── comm/ │ │ │ ├── __init__.py │ │ │ ├── abstract_comm.py │ │ │ ├── file_communicator.py │ │ │ ├── gpib_communicator.py │ │ │ ├── loopback_communicator.py │ │ │ ├── serial_communicator.py │ │ │ ├── serial_manager.py │ │ │ ├── socket_communicator.py │ │ │ ├── usb_communicator.py │ │ │ ├── usbtmc_communicator.py │ │ │ ├── visa_communicator.py │ │ │ └── vxi11_communicator.py │ │ ├── electrometer.py │ │ ├── function_generator.py │ │ ├── instrument.py │ │ ├── multimeter.py │ │ ├── optical_spectrum_analyzer.py │ │ ├── oscilloscope.py │ │ ├── power_supply.py │ │ └── signal_generator/ │ │ ├── __init__.py │ │ ├── channel.py │ │ ├── signal_generator.py │ │ └── single_channel_sg.py │ ├── agilent/ │ │ ├── __init__.py │ │ ├── agilent33220a.py │ │ └── agilent34410a.py │ ├── aimtti/ │ │ ├── __init__.py │ │ └── aimttiel302p.py │ ├── comet/ │ │ ├── __init__.py │ │ └── cito_plus_1310.py │ ├── config.py │ ├── delta_elektronika/ │ │ ├── __init__.py │ │ └── psc_eth.py │ ├── dressler/ │ │ ├── __init__.py │ │ └── cesar_1312.py │ ├── errors.py │ ├── fluke/ │ │ ├── __init__.py │ │ └── fluke3000.py │ ├── generic_scpi/ │ │ ├── __init__.py │ │ ├── scpi_function_generator.py │ │ ├── scpi_instrument.py │ │ └── scpi_multimeter.py │ ├── gentec_eo/ │ │ ├── __init__.py │ │ └── blu.py │ ├── glassman/ │ │ ├── __init__.py │ │ └── glassmanfr.py │ ├── hcp/ │ │ ├── __init__.py │ │ ├── tc038.py │ │ └── tc038d.py │ ├── holzworth/ │ │ ├── __init__.py │ │ └── holzworth_hs9000.py │ ├── hp/ │ │ ├── __init__.py │ │ ├── hp3325a.py │ │ ├── hp3456a.py │ │ ├── hp6624a.py │ │ ├── hp6632b.py │ │ ├── hp6652a.py │ │ └── hpe3631a.py │ ├── keithley/ │ │ ├── __init__.py │ │ ├── keithley195.py │ │ ├── keithley2182.py │ │ ├── keithley485.py │ │ ├── keithley580.py │ │ ├── keithley6220.py │ │ └── keithley6514.py │ ├── lakeshore/ │ │ ├── __init__.py │ │ ├── lakeshore336.py │ │ ├── lakeshore340.py │ │ ├── lakeshore370.py │ │ └── lakeshore475.py │ ├── mettler_toledo/ │ │ ├── __init__.py │ │ └── mt_sics.py │ ├── minghe/ │ │ ├── __init__.py │ │ └── mhs5200a.py │ ├── named_struct.py │ ├── newport/ │ │ ├── __init__.py │ │ ├── agilis.py │ │ ├── errors.py │ │ ├── newport_pmc8742.py │ │ └── newportesp301.py │ ├── ondax/ │ │ ├── __init__.py │ │ └── lm.py │ ├── optional_dep_finder.py │ ├── oxford/ │ │ ├── __init__.py │ │ └── oxforditc503.py │ ├── pfeiffer/ │ │ ├── __init__.py │ │ └── tpg36x.py │ ├── phasematrix/ │ │ ├── __init__.py │ │ └── phasematrix_fsw0020.py │ ├── picowatt/ │ │ ├── __init__.py │ │ └── picowattavs47.py │ ├── qubitekk/ │ │ ├── __init__.py │ │ ├── cc1.py │ │ └── mc1.py │ ├── rigol/ │ │ ├── __init__.py │ │ └── rigolds1000.py │ ├── srs/ │ │ ├── __init__.py │ │ ├── srs345.py │ │ ├── srs830.py │ │ ├── srsctc100.py │ │ └── srsdg645.py │ ├── sunpower/ │ │ ├── __init__.py │ │ └── cryotel_gt.py │ ├── tektronix/ │ │ ├── __init__.py │ │ ├── tekawg2000.py │ │ ├── tekdpo4104.py │ │ ├── tekdpo70000.py │ │ ├── tektds224.py │ │ └── tektds5xx.py │ ├── teledyne/ │ │ ├── __init__.py │ │ └── maui.py │ ├── thorlabs/ │ │ ├── __init__.py │ │ ├── _abstract.py │ │ ├── _cmds.py │ │ ├── _packets.py │ │ ├── lcc25.py │ │ ├── pm100usb.py │ │ ├── sc10.py │ │ ├── tc200.py │ │ ├── thorlabs_utils.py │ │ └── thorlabsapt.py │ ├── toptica/ │ │ ├── __init__.py │ │ ├── topmode.py │ │ └── toptica_utils.py │ ├── units.py │ ├── util_fns.py │ └── yokogawa/ │ ├── __init__.py │ ├── yokogawa6370.py │ └── yokogawa7651.py ├── tests/ │ ├── __init__.py │ ├── test_abstract_inst/ │ │ ├── __init__.py │ │ ├── test_electrometer.py │ │ ├── test_function_generator.py │ │ ├── test_multimeter.py │ │ ├── test_optical_spectrum_analyzer.py │ │ ├── test_oscilloscope.py │ │ ├── test_power_supply.py │ │ └── test_signal_generator/ │ │ ├── test_channel.py │ │ ├── test_signal_generator.py │ │ └── test_single_channel_sg.py │ ├── test_agilent/ │ │ ├── __init__.py │ │ ├── test_agilent_33220a.py │ │ └── test_agilent_34410a.py │ ├── test_aimtti/ │ │ ├── __init__.py │ │ └── test_aimttiel302p.py │ ├── test_base_instrument.py │ ├── test_comet/ │ │ ├── __init__.py │ │ └── test_cito_plus_1310.py │ ├── test_comm/ │ │ ├── __init__.py │ │ ├── test_file.py │ │ ├── test_gpibusb.py │ │ ├── test_loopback.py │ │ ├── test_serial.py │ │ ├── test_socket.py │ │ ├── test_usb_communicator.py │ │ ├── test_usbtmc.py │ │ ├── test_visa_communicator.py │ │ └── test_vxi11.py │ ├── test_config.py │ ├── test_delta_elektronika/ │ │ ├── __init__.py │ │ └── test_psc_eth.py │ ├── test_dressler/ │ │ ├── __init__.py │ │ └── test_cesar_1312.py │ ├── test_fluke/ │ │ ├── __init__.py │ │ └── test_fluke3000.py │ ├── test_generic_scpi/ │ │ ├── __init__.py │ │ ├── test_scpi_function_generator.py │ │ ├── test_scpi_instrument.py │ │ └── test_scpi_multimeter.py │ ├── test_gentec_eo/ │ │ ├── __init__.py │ │ └── test_blu.py │ ├── test_glassman/ │ │ ├── __init__.py │ │ └── test_glassmanfr.py │ ├── test_hcp/ │ │ ├── __init__.py │ │ ├── test_tc038.py │ │ └── test_tc038d.py │ ├── test_holzworth/ │ │ ├── __init__.py │ │ └── test_holzworth_hs9000.py │ ├── test_hp/ │ │ ├── __init__.py │ │ ├── test_hp3325a.py │ │ ├── test_hp3456a.py │ │ ├── test_hp6624a.py │ │ ├── test_hp6632b.py │ │ ├── test_hp6652a.py │ │ └── test_hpe3631a.py │ ├── test_keithley/ │ │ ├── __init__.py │ │ ├── test_keithley195.py │ │ ├── test_keithley2182.py │ │ ├── test_keithley485.py │ │ ├── test_keithley580.py │ │ ├── test_keithley6220.py │ │ └── test_keithley6514.py │ ├── test_lakeshore/ │ │ ├── __init__.py │ │ ├── test_lakeshore336.py │ │ ├── test_lakeshore340.py │ │ ├── test_lakeshore370.py │ │ └── test_lakeshore475.py │ ├── test_mettler_toledo/ │ │ ├── __init__.py │ │ └── test_mt_sics.py │ ├── test_minghe/ │ │ └── test_minghe_mhs5200a.py │ ├── test_named_struct.py │ ├── test_newport/ │ │ ├── __init__.py │ │ ├── test_agilis.py │ │ ├── test_errors.py │ │ ├── test_newport_pmc8742.py │ │ └── test_newportesp301.py │ ├── test_ondax/ │ │ └── test_lm.py │ ├── test_oxford/ │ │ ├── __init__.py │ │ └── test_oxforditc503.py │ ├── test_package.py │ ├── test_pfeiffer/ │ │ ├── __init__.py │ │ └── test_tpg36x.py │ ├── test_phasematrix/ │ │ ├── __init__.py │ │ └── test_phasematrix_fsw0020.py │ ├── test_picowatt/ │ │ ├── __init__.py │ │ └── test_picowatt_avs47.py │ ├── test_property_factories/ │ │ ├── __init__.py │ │ ├── test_bool_property.py │ │ ├── test_bounded_unitful_property.py │ │ ├── test_enum_property.py │ │ ├── test_int_property.py │ │ ├── test_rproperty.py │ │ ├── test_string_property.py │ │ ├── test_unitful_property.py │ │ └── test_unitless_property.py │ ├── test_qubitekk/ │ │ ├── __init__.py │ │ ├── test_qubitekk_cc1.py │ │ └── test_qubitekk_mc1.py │ ├── test_rigol/ │ │ └── test_rigolds1000.py │ ├── test_split_str.py │ ├── test_srs/ │ │ ├── __init__.py │ │ ├── test_srs345.py │ │ ├── test_srs830.py │ │ ├── test_srsctc100.py │ │ └── test_srsdg645.py │ ├── test_sunpower/ │ │ ├── __init__.py │ │ └── test_cryotel_gt.py │ ├── test_tektronix/ │ │ ├── __init__.py │ │ ├── test_tekawg2000.py │ │ ├── test_tekdpo4104.py │ │ ├── test_tekdpo70000.py │ │ ├── test_tektronix_tds224.py │ │ └── test_tktds5xx.py │ ├── test_teledyne/ │ │ ├── __init__.py │ │ └── test_maui.py │ ├── test_test_utils.py │ ├── test_thorlabs/ │ │ ├── __init__.py │ │ ├── test_abstract.py │ │ ├── test_packets.py │ │ ├── test_thorlabs_apt.py │ │ ├── test_thorlabs_lcc25.py │ │ ├── test_thorlabs_pm100usb.py │ │ ├── test_thorlabs_sc10.py │ │ ├── test_thorlabs_tc200.py │ │ └── test_utils.py │ ├── test_toptica/ │ │ ├── __init__.py │ │ ├── test_toptica_topmode.py │ │ └── test_toptica_utils.py │ ├── test_util_fns.py │ └── test_yokogawa/ │ ├── __init__.py │ ├── test_yokogawa7651.py │ └── test_yokogawa_6370.py └── tox.ini ================================================ FILE CONTENTS ================================================ ================================================ FILE: .coveragerc ================================================ [paths] source = src */site-packages [report] omit = */python?.?/* */site-packages/nose/* *__init__* exclude_lines = pragma: no cover if self._testing: if not self._testing: raise NotImplementedError raise AssertionError [run] branch = false parallel = true source = instruments relative_files = True ================================================ FILE: .github/workflows/deploy.yml ================================================ 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 }} ================================================ FILE: .github/workflows/test.yml ================================================ name: Testing on: push: branches: [ main ] pull_request: jobs: static-checks: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 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.9 TOXENV: "py39" - python-version: 3.9 TOXENV: "py39-numpy" - python-version: "3.10" TOXENV: "py310" - python-version: "3.11" TOXENV: "py311" - python-version: "3.12" TOXENV: "py312" - python-version: "3.13" TOXENV: "py313" steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 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: Upload coverage to Codecov if: github.repository_owner == 'instrumentkit' uses: codecov/codecov-action@v4 env: TOXENV: ${{ matrix.TOXENV }} CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} with: env_vars: TOXENV fail_ci_if_error: true flags: unittests name: codecov-umbrella verbose: true files: ./coverage.xml ================================================ FILE: .gitignore ================================================ ## Temporary files ## *~ *.tmp ## OS generated files ## .DS_Store* desktop.ini Thumbs.db ## Build directories ## doc/_build ## Venv .venv/ .python-version ## setup.py generated files ## MANIFEST # The following ignores are drawn from github@github:gitignore/Python.gitignore. *.py[cod] # C extensions *.so # Packages .egg/ *.egg *.egg-info dist build eggs parts bin var sdist develop-eggs .installed.cfg lib lib64 # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox nosetests.xml .pytest_cache coverage.xml #Translations *.mo #Mr Developer .mr.developer.cfg #pycharm generated .idea # VS Code IDE internals .vscode/ # nosetests metadata .noseids # Hypothesis files .hypothesis/ # version file generated by setuptools_scm src/instruments/_version.py ================================================ FILE: .pre-commit-config.yaml ================================================ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: no-commit-to-branch args: [--branch, main] - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - id: debug-statements - repo: https://github.com/psf/black-pre-commit-mirror rev: 26.3.1 hooks: - id: black - repo: https://github.com/asottile/pyupgrade rev: v3.21.2 hooks: - id: pyupgrade args: [ --py39-plus ] ================================================ FILE: .readthedocs.yml ================================================ version: 2 build: os: ubuntu-24.04 tools: python: "3.13" sphinx: builder: html configuration: doc/source/conf.py fail_on_warning: false python: install: - method: pip path: . extra_requirements: - docs ================================================ FILE: README.rst ================================================ InstrumentKit ============= .. image:: https://github.com/instrumentkit/InstrumentKit/workflows/Testing/badge.svg?branch=main :target: https://github.com/instrumentkit/InstrumentKit :alt: Github Actions build status .. image:: https://codecov.io/gh/instrumentkit/InstrumentKit/branch/main/graph/badge.svg?token=Q2wcdW3t4A :target: https://codecov.io/gh/instrumentkit/InstrumentKit :alt: Codecov code coverage .. image:: https://readthedocs.org/projects/instrumentkit/badge/?version=latest :target: https://readthedocs.org/projects/instrumentkit/?badge=latest :alt: Documentation .. image:: https://img.shields.io/pypi/v/instrumentkit.svg?maxAge=86400 :target: https://pypi.python.org/pypi/instrumentkit :alt: PyPI version .. image:: https://img.shields.io/pypi/pyversions/instrumentkit.svg?maxAge=2592000 :alt: Python versions InstrumentKit is an open source Python library designed to help the end-user get straight into communicating with their equipment via a PC. InstrumentKit aims to accomplish this by providing a connection- and vendor-agnostic API. Users can freely swap between a variety of connection types (ethernet, gpib, serial, usb) without impacting their code. Since the API is consistent across similar instruments, a user can, for example, upgrade from their 1980's multimeter using GPIB to a modern Keysight 34461a using ethernet with only a single line change. Supported means of communication are: - Galvant Industries GPIBUSB adapter (``open_gpibusb``) - Serial (``open_serial``) - Sockets (``open_tcpip``) - VISA (``open_visa``) - 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. If you have any problems or have code you wish to contribute back to the project please feel free to open an issue or a pull request! Installation ------------ The ``instruments`` package can be installed from this repository by the following means: From Git: .. code-block:: console $ git clone git@github.com:instrumentkit/InstrumentKit.git $ cd InstrumentKit $ pip install -e . From Github using pip: .. code-block:: console $ pip install -e git+https://www.github.com/instrumentkit/InstrumentKit.git From pypi using pip (the latest stable release): .. code-block:: console $ pip install instrumentkit From pypi using pip (the latest pre-release): .. code-block:: console $ pip install instrumentkit --pre Usage Example ------------- To open a connection to a generic SCPI-compatible multimeter using a Galvant Industries' GPIBUSB adapter: .. code-block:: python >>> import instruments as ik >>> inst = ik.generic_scpi.SCPIMultimeter.open_gpibusb("/dev/ttyUSB0", 1) From there, various built-in properties and functions can be called. For example, the instrument's identification information can be retrieved by calling the name property: .. code-block:: python >>> print(inst.name) Or, since in the demo we connected to an ``SCPIMultimeter``, we can preform multimeter-specific tasks, such as switching functions, and taking a measurement reading: .. code-block:: python >>> reading = inst.measure(inst.Mode.voltage_dc) >>> 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 send, one can use the following functions to do so: .. code-block:: python >>> inst.sendcmd("DATA") # Send command with no response >>> resp = inst.query("*IDN?") # Send command and retrieve response Python Version Compatibility ---------------------------- At this time, Python 3.9, 3.10, 3.11, 3.12, and 3.13 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. Documentation ------------- You can find the project documentation at our ReadTheDocs pages located at http://instrumentkit.readthedocs.org/en/latest/index.html Contributing ------------ The InstrumentKit team always welcome additional contributions to the project. However, we ask that you please review our contributing developer guidelines which can be found in the documentation. We also suggest that you look at 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. 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``: .. code-block:: console $ 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. ================================================ FILE: doc/Makefile ================================================ # Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/GPIBUSBAdapterDriverLibrary.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/GPIBUSBAdapterDriverLibrary.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/GPIBUSBAdapterDriverLibrary" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/GPIBUSBAdapterDriverLibrary" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." ================================================ FILE: doc/examples/.ipynb_checkpoints/ex_keithley6514-checkpoint.ipynb ================================================ { "metadata": { "name": "" }, "nbformat": 3, "nbformat_minor": 0, "worksheets": [ { "cells": [ { "cell_type": "heading", "level": 1, "metadata": {}, "source": [ "InstrumentKit Example Library" ] }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Keithley 6514 Electrometer" ] }, { "cell_type": "code", "collapsed": false, "input": [ "%pylab inline\n", "import instruments as ik" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "Populating the interactive namespace from numpy and matplotlib\n" ] } ], "prompt_number": 1 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here, our device is connected via a gpibusb adaptor, on COM5, address 14." ] }, { "cell_type": "code", "collapsed": false, "input": [ "elec=ik.keithley.Keithley6514.open_gpibusb('COM5', 14)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 2 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we query a few standard bits of information:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "print elec.query('*IDN?')\n", "print \"Mode: {}\".format(elec.mode.value)\n", "print \"Unit: {}\".format(elec.unit)\n", "print \"Upper Range Limit: {}\".format(elec.input_range)\n", "print \"Zero Check: {}\".format(elec.zero_check)\n", "print \"Zero Correct: {}\".format(elec.zero_correct)" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "KEITHLEY INSTRUMENTS INC.,MODEL 6514,1344784,A12 Aug 29 2008 15:40:25/A02 /D\n", "Mode: CURR:DC\n", "Unit: 1 A (ampere)" ] }, { "output_type": "stream", "stream": "stdout", "text": [ "\n", "Upper Range Limit: 2.1e-10 A" ] }, { "output_type": "stream", "stream": "stdout", "text": [ "\n", "Zero Check: False\n", "Zero Correct: False" ] }, { "output_type": "stream", "stream": "stdout", "text": [ "\n" ] } ], "prompt_number": 3 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Set up the zero checking/correcting:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "elec.zero_check = False\n", "elec.zero_correct = True" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 6 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Take a single reading:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "reading, timestamp = elec.read()\n", "print \"Current Reading: {}\".format(reading)" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "Current Reading: 2.485788e-11 A\n" ] } ], "prompt_number": 7 } ], "metadata": {} } ] } ================================================ FILE: doc/examples/ex_generic_scpi.ipynb ================================================ { "metadata": { "name": "ex_generic_scpi" }, "nbformat": 3, "nbformat_minor": 0, "worksheets": [ { "cells": [ { "cell_type": "heading", "level": 1, "metadata": {}, "source": [ "InstrumentKit Library Examples" ] }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Generic SCPI Instrument" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In this example, we will demonstrate how to connect to a generic SCPI \n", "instrument and query its identification information." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We start by importing the InstrumentKit package." ] }, { "cell_type": "code", "collapsed": false, "input": [ "import instruments as ik" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Next, we open our connection to the instrument. Here we use the generic \n", "SCPIInstrument class and open the connection using the Galvant Industries'\n", "GPIBUSB adapter. Our connection is made to the virtual serial port located at\n", "/dev/ttyUSB0 and GPIB address 1" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The connection method used will have to be changed to match your setup." ] }, { "cell_type": "code", "collapsed": true, "input": [ "inst = ik.generic_scpi.SCPIInstrument.open_gpibusb('/dev/ttyUSB0', 1)" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now that we are connected to the instrument, we can query its identification\n", "information. We will do this by using the SCPIInstrument property 'name'." ] }, { "cell_type": "code", "collapsed": true, "input": [ "print inst.name" ], "language": "python", "metadata": {}, "outputs": [] } ], "metadata": {} } ] } ================================================ FILE: doc/examples/ex_generic_scpi.py ================================================ # 3.0 # # InstrumentKit Library Examples # # Generic SCPI Instrument # # In this example, we will demonstrate how to connect to a generic SCPI # instrument and query its identification information. # # We start by importing the InstrumentKit package. # import instruments as ik # # 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 # # The connection method used will have to be changed to match your setup. # inst = ik.generic_scpi.SCPIInstrument.open_gpibusb("/dev/ttyUSB0", 1) # # Now that we are connected to the instrument, we can query its identification # information. We will do this by using the SCPIInstrument property 'name'. # print(inst.name) ================================================ FILE: doc/examples/ex_hp3325.py ================================================ import logging import instruments as ik fcngen = ik.hp.HP3325a.open_gpibusb("COM4", 17, model="pl") logging.basicConfig(level=logging.DEBUG) fcngen._file.debug = True fcngen.amplitude = 2.0 # V fcngen.frequency = 512.53 # Hz fcngen.function = fcngen.Function.square print(f"Actual voltage={fcngen.amplitude} V") print(f"Actual frequency={fcngen.frequency} Hz") ================================================ FILE: doc/examples/ex_hp3456.out ================================================ DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'T4' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'W6STG' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'R1W' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'W10STN' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'W1STI' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'S0F4' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'M2' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'T3' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: -> '+000.1055E+0,+000.1043E+0,+000.1005E+0,+000.1014E+0,+000.0993E+0,+000.1037E+0,+000.0995E+0,+000.1002E+0,+000.1041E+0,+000.1025E+0' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'REV' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: -> '+04.93111E-6' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'REC' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: -> '+10.00000E+0' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'REM' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: -> '+102.1000E-3' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'W10STI' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'REN' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: -> '+10.00000E+0' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'REG' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: -> '+06.00000E+0' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'REI' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: -> '+10.00000E+0' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'RED' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: -> '-000.0000E+0' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'REM' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: -> '+102.1000E-3' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'REV' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: -> '+04.93111E-6' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'REC' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: -> '+10.00000E+0' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'REL' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: -> '+099.3000E-3' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'REU' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: -> '+105.5000E-3' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'RER' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: -> '+0600.000E+0' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'REY' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: -> '+1.000000E+0' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'REZ' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: -> '+105.5000E-3' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'W100STI' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'REC' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: -> '+10.00000E+0' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'S1F1W1STNT3' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: -> '+00.00000E-3' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'S0F4W1STNT3' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: -> '+000.1010E+0' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'W1STI' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'R2W' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'S0F1W1STNT3' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: -> '+000.0031E-3' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'R3W' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'S0F1W1STNT3' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: -> '+0.000003E+0' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'R4W' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'S0F1W1STNT3' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: -> '+00.00002E+0' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'R5W' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'S0F1W1STNT3' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: -> '+000.0003E+0' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'R6W' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'S0F1W1STNT3' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: -> '+0000.002E+0' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'M0' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'S0F4W1STNT3' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: -> '+0000.003E+3' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'M3' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'S0F4W1STNT3' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: -> '+0000.000E+3' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'M0' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'S0F4W1STNT3' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: -> '-0000.002E+3' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'Z0' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'FL0' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'R1W' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'S0F1W1STNT3' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: -> '-000.0017E-3' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'Z1' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'S0F1W1STNT3' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: -> '+000.0014E-3' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'FL1' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'S0F1W1STNT3' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: -> '+000.0007E-3' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'Z0' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- 'S0F1W1STNT3' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: <- '' DEBUG:instruments.abstract_instruments.comm.gi_gpib: -> '+000.0002E-3' 10 4.93111e-06 10 0.1021 n = 10.0 g = 6.0 p = 10.0 d = -0.0 s 0.1021 4.93111e-06 10 0.0993 0.1055 600.0 1.0 0.1055 10 0.0 0.101 ohm 3.1e-06 V 3e-06 V 2e-05 V 0.0003 V 0.002 V 3.0 ohm 0.0 ohm -2.0 ohm -1.7e-06 V 1.4e-06 V 7e-07 V 2e-07 V ================================================ FILE: doc/examples/ex_hp3456.py ================================================ #!/usr/bin/python import logging import time import instruments as ik import instruments.units as u dmm = ik.hp.HP3456a.open_gpibusb("/dev/ttyUSB0", 22) logging.basicConfig(level=logging.DEBUG) dmm._file.debug = True dmm.trigger_mode = dmm.TriggerMode.hold dmm.number_of_digits = 6 dmm.auto_range() n = 10 dmm.number_of_readings = n dmm.nplc = 1 dmm.mode = dmm.Mode.resistance_2wire dmm.math_mode = dmm.MathMode.statistic dmm.trigger() time.sleep(n * 0.04) v = dmm.fetch(dmm.Mode.resistance_2wire) print(len(v)) print(dmm.variance) print(dmm.count) print(dmm.mean) # Read registers dmm.nplc = 10 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) print(dmm.lower) print(dmm.upper) print(dmm.r) print(dmm.y) print(dmm.z) # Walk through input range dmm.nplc = 100 print(dmm.count) print(dmm.measure(dmm.Mode.ratio_dcv_dcv)) print(dmm.measure(dmm.Mode.resistance_2wire)) dmm.nplc = 1 for i in range(-1, 4): value = (10**i) * u.volt dmm.input_range = value print(dmm.measure(dmm.Mode.dcv)) # Walk through relative / null mode dmm.relative = False print(dmm.measure(dmm.Mode.resistance_2wire)) dmm.relative = True print(dmm.measure(dmm.Mode.resistance_2wire)) dmm.relative = False print(dmm.measure(dmm.Mode.resistance_2wire)) # Measure with autozero off dmm.autozero = 0 dmm.filter = 0 dmm.auto_range() print(dmm.measure(dmm.Mode.dcv)) dmm.autozero = 1 print(dmm.measure(dmm.Mode.dcv)) dmm.filter = 1 print(dmm.measure(dmm.Mode.dcv)) dmm.autozero = 0 print(dmm.measure(dmm.Mode.dcv)) ================================================ FILE: doc/examples/ex_keithley195.ipynb ================================================ { "metadata": { "name": "ex_keithley195" }, "nbformat": 3, "nbformat_minor": 0, "worksheets": [ { "cells": [ { "cell_type": "heading", "level": 1, "metadata": {}, "source": [ "InstrumentKit Library Examples" ] }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Keithley 195 Multimeter" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In this example, we will demonstrate how to connect to a Keithley 195\n", "multimeter and transfer a single reading to the PC." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We start by importing the InstrumentKit package." ] }, { "cell_type": "code", "collapsed": false, "input": [ "import instruments as ik" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Next, we open our connection to the instrument. Here we use the \n", "Keithley195 class and open the connection using Galvant Industries'\n", "GPIBUSB adapter. Our connection is made to the virtual serial port located at\n", "/dev/ttyUSB0 and GPIB address 16." ] }, { "cell_type": "code", "collapsed": false, "input": [ "dmm = ik.keithley.Keithley195.open_gpibusb('/dev/ttyUSB0', 1)" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now, we retreive the measurement currently displayed on the front panel." ] }, { "cell_type": "code", "collapsed": false, "input": [ "print dmm.measure()" ], "language": "python", "metadata": {}, "outputs": [] } ], "metadata": {} } ] } ================================================ FILE: doc/examples/ex_keithley195.py ================================================ # 3.0 # # InstrumentKit Library Examples # # Keithley 195 Multimeter # # In this example, we will demonstrate how to connect to a Keithley 195 # multimeter and transfer a single reading to the PC. # # We start by importing the InstrumentKit package. # import instruments as ik # # 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) # # Now, we retreive the measurement currently displayed on the front panel. # print(dmm.measure()) ================================================ FILE: doc/examples/ex_keithley6514.ipynb ================================================ { "metadata": { "name": "" }, "nbformat": 3, "nbformat_minor": 0, "worksheets": [ { "cells": [ { "cell_type": "heading", "level": 1, "metadata": {}, "source": [ "InstrumentKit Example Library" ] }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Keithley 6514 Electrometer" ] }, { "cell_type": "code", "collapsed": false, "input": [ "%pylab inline\n", "import instruments as ik\n", "import time, datetime" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "Populating the interactive namespace from numpy and matplotlib\n" ] } ], "prompt_number": 1 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here, our device is connected via a gpibusb adaptor, on COM5, address 14." ] }, { "cell_type": "code", "collapsed": false, "input": [ "elec=ik.keithley.Keithley6514.open_gpibusb('COM5', 14)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 2 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we query a few standard bits of information:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "print elec.query('*IDN?')\n", "print \"Mode: {}\".format(elec.mode.value)\n", "print \"Unit: {}\".format(elec.unit)\n", "print \"Upper Range Limit: {}\".format(elec.input_range)\n", "print \"Zero Check: {}\".format(elec.zero_check)\n", "print \"Zero Correct: {}\".format(elec.zero_correct)" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "KEITHLEY INSTRUMENTS INC.,MODEL 6514,1344784,A12 Aug 29 2008 15:40:25/A02 /D\n", "Mode: RES\n", "Unit: 1 ohm (ohm)" ] }, { "output_type": "stream", "stream": "stdout", "text": [ "\n", "Upper Range Limit: 2100000.0 ohm" ] }, { "output_type": "stream", "stream": "stdout", "text": [ "\n", "Zero Check: False\n", "Zero Correct: False" ] }, { "output_type": "stream", "stream": "stdout", "text": [ "\n" ] } ], "prompt_number": 3 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Use the auto config function to set up a resistance measurement." ] }, { "cell_type": "code", "collapsed": false, "input": [ "elec.auto_config(elec.Mode.resistance)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 4 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Check the status of things again:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "print elec.query('*IDN?')\n", "print \"Mode: {}\".format(elec.mode.value)\n", "print \"Unit: {}\".format(elec.unit)\n", "print \"Upper Range Limit: {}\".format(elec.input_range)\n", "print \"Zero Check: {}\".format(elec.zero_check)\n", "print \"Zero Correct: {}\".format(elec.zero_correct)" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "KEITHLEY INSTRUMENTS INC.,MODEL 6514,1344784,A12 Aug 29 2008 15:40:25/A02 /D\n", "Mode: RES\n", "Unit: 1 ohm (ohm)" ] }, { "output_type": "stream", "stream": "stdout", "text": [ "\n", "Upper Range Limit: 2100000.0 ohm" ] }, { "output_type": "stream", "stream": "stdout", "text": [ "\n", "Zero Check: False\n", "Zero Correct: False" ] }, { "output_type": "stream", "stream": "stdout", "text": [ "\n" ] } ], "prompt_number": 5 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Take a single reading:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "reading, timestamp = elec.read()\n", "print \"Current Reading: {}\".format(reading)" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "Current Reading: 675582.8 ohm\n" ] } ], "prompt_number": 6 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Take 100 readings and plot them:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "timestamps = np.empty(100)\n", "readings = np.empty(100)\n", "for idx in xrange(100):\n", " readings[idx], timestamps[idx] = elec.read()" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 7 }, { "cell_type": "code", "collapsed": false, "input": [ "plot(timestamps, readings)\n", "xlabel('time (s)')\n", "ylabel('({})'.format(elec.unit.name))" ], "language": "python", "metadata": {}, "outputs": [ { "metadata": {}, "output_type": "pyout", "prompt_number": 8, "text": [ "" ] }, { "metadata": {}, "output_type": "display_data", "png": "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\nYII=\n", "text": [ "" ] } ], "prompt_number": 8 }, { "cell_type": "code", "collapsed": false, "input": [ "with open(r'C:\\Users\\fpga\\Documents\\log.txt','w') as f:\n", " f.write('Time,Resistance (Ohms)\\n')\n", " \n", "elec.sendcmd('SYST:TIME:RESET')\n", "\n", "while True:\n", " with open(r'C:\\Users\\fpga\\Documents\\log.txt','a') as f:\n", " reading, timestamp = elec.read()\n", " \n", " f.write('{},{:e}\\n'.format(timestamp,reading.item()))\n", " f.flush()" ], "language": "python", "metadata": {}, "outputs": [ { "ename": "KeyboardInterrupt", "evalue": "", "output_type": "pyerr", "traceback": [ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m\n\u001b[1;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)", "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m()\u001b[0m\n\u001b[0;32m 6\u001b[0m \u001b[1;32mwhile\u001b[0m \u001b[0mTrue\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 7\u001b[0m \u001b[1;32mwith\u001b[0m \u001b[0mopen\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34mr'C:\\Users\\fpga\\Documents\\log.txt'\u001b[0m\u001b[1;33m,\u001b[0m\u001b[1;34m'a'\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;32mas\u001b[0m \u001b[0mf\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 8\u001b[1;33m \u001b[0mreading\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtimestamp\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0melec\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mread\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 9\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 10\u001b[0m \u001b[0mf\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mwrite\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'{},{:e}\\n'\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mformat\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mtimestamp\u001b[0m\u001b[1;33m,\u001b[0m\u001b[0mreading\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mitem\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", "\u001b[1;32mC:\\Users\\fpga\\AppData\\Local\\Enthought\\Canopy\\User\\lib\\site-packages\\instruments\\keithley\\keithley6514.pyc\u001b[0m in \u001b[0;36mread\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m 224\u001b[0m '''\n\u001b[0;32m 225\u001b[0m \u001b[1;31m# TODO: figure out what to do with the status info\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 226\u001b[1;33m \u001b[0mraw\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mquery\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'READ?'\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 227\u001b[0m \u001b[0mreading\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtimestamp\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mstatus\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_parse_measurement\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mraw\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 228\u001b[0m \u001b[1;32mreturn\u001b[0m \u001b[0mreading\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtimestamp\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", "\u001b[1;32mC:\\Users\\fpga\\AppData\\Local\\Enthought\\Canopy\\User\\lib\\site-packages\\instruments\\abstract_instruments\\instrument.pyc\u001b[0m in \u001b[0;36mquery\u001b[1;34m(self, cmd, size)\u001b[0m\n\u001b[0;32m 111\u001b[0m \u001b[1;33m:\u001b[0m\u001b[0mrtype\u001b[0m\u001b[1;33m:\u001b[0m \u001b[1;33m`\u001b[0m\u001b[0mstr\u001b[0m\u001b[1;33m`\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 112\u001b[0m \"\"\"\n\u001b[1;32m--> 113\u001b[1;33m \u001b[1;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_file\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mquery\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mcmd\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0msize\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 114\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 115\u001b[0m \u001b[1;31m## PROPERTIES ##\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", "\u001b[1;32mC:\\Users\\fpga\\AppData\\Local\\Enthought\\Canopy\\User\\lib\\site-packages\\instruments\\abstract_instruments\\gi_gpib.pyc\u001b[0m in \u001b[0;36mquery\u001b[1;34m(self, msg, size)\u001b[0m\n\u001b[0;32m 200\u001b[0m '''\n\u001b[0;32m 201\u001b[0m '''\n\u001b[1;32m--> 202\u001b[1;33m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msendcmd\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mmsg\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 203\u001b[0m \u001b[1;32mif\u001b[0m \u001b[1;34m'?'\u001b[0m \u001b[1;32mnot\u001b[0m \u001b[1;32min\u001b[0m \u001b[0mmsg\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 204\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_file\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msendcmd\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'+read'\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", "\u001b[1;32mC:\\Users\\fpga\\AppData\\Local\\Enthought\\Canopy\\User\\lib\\site-packages\\instruments\\abstract_instruments\\gi_gpib.pyc\u001b[0m in \u001b[0;36msendcmd\u001b[1;34m(self, msg)\u001b[0m\n\u001b[0;32m 186\u001b[0m \u001b[1;32mreturn\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 187\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_file\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msendcmd\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'+a:'\u001b[0m \u001b[1;33m+\u001b[0m \u001b[0mstr\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_gpib_address\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 188\u001b[1;33m \u001b[0mtime\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msleep\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;36m0.01\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 189\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_file\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msendcmd\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'+eoi:{}'\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mformat\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_eoi\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 190\u001b[0m \u001b[0mtime\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msleep\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;36m0.01\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", "\u001b[1;31mKeyboardInterrupt\u001b[0m: " ] } ], "prompt_number": 5 } ], "metadata": {} } ] } ================================================ FILE: doc/examples/ex_maui.ipynb ================================================ { "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 } ================================================ FILE: doc/examples/ex_oscilloscope_waveform.ipynb ================================================ { "metadata": { "name": "ex_oscilloscope_waveform" }, "nbformat": 3, "nbformat_minor": 0, "worksheets": [ { "cells": [ { "cell_type": "heading", "level": 1, "metadata": {}, "source": [ "InstrumentKit Library Examples" ] }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Tektronix DPO 4104 Oscilloscope" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In this example, we will demonstrate how to connect to a Tektronix DPO 4104 \n", "oscilloscope and transfer the waveform from channel 1 into memory." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We start by importing the InstrumentKit and numpy packages. In this example \n", "we require numpy because the waveforms will be returned as numpy arrays." ] }, { "cell_type": "code", "collapsed": false, "input": [ "import instruments as ik\n", "import numpy as np" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Next, we open our connection to the oscilloscope. Here we use the associated\n", "TekDPO4104 class and open the connection via TCP/IP to address 192.168.0.2 on\n", "port 8080." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The connection method used will have to be changed to match your setup." ] }, { "cell_type": "code", "collapsed": false, "input": [ "tek = ik.tektronix.TekTDS224.open_tcpip('192.168.0.2', 8080)" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now that we are connected to the instrument, we can transfer the waveform \n", "from the oscilloscope. Note that Python channel[0] specifies the physical\n", "channel 1. This is due to Python's zero-based numbering vs Tektronix's\n", "one-based numbering." ] }, { "cell_type": "code", "collapsed": false, "input": [ "[x,y] = tek.channel[0].read_waveform()" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "With the waveform now in memory, any other data analysis can be performed.\n", "Here we simply compute the mean value from the y-data." ] }, { "cell_type": "code", "collapsed": false, "input": [ "print np.mean(y)" ], "language": "python", "metadata": {}, "outputs": [] } ], "metadata": {} } ] } ================================================ FILE: doc/examples/ex_oscilloscope_waveform.py ================================================ # 3.0 # # InstrumentKit Library Examples # # Tektronix DPO 4104 Oscilloscope # # 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 require numpy because the waveforms will be returned as numpy arrays. # import instruments as ik import numpy as np # # Next, we open our connection to the oscilloscope. Here we use the associated # TekDPO4104 class and open the connection via TCP/IP to address 192.168.0.2 on # port 8080. # # The connection method used will have to be changed to match your setup. # tek = ik.tektronix.TekTDS224.open_tcpip("192.168.0.2", 8080) # # 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() # # With the waveform now in memory, any other data analysis can be performed. # Here we simply compute the mean value from the y-data. # print(np.mean(y)) ================================================ FILE: doc/examples/ex_qubitekk_gui.py ================================================ #!/usr/bin/python # Qubitekk Coincidence Counter example import matplotlib 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 import instruments as ik import tkinter as tk import re def clear_counts(*args): cc.clear_counts() def getvalues(i): # set counts labels chan1counts.set(cc.channel[0].count) chan2counts.set(cc.channel[1].count) coinc_counts.set(cc.channel[2].count) # add count values to arrays for plotting chan1vals.append(chan1counts.get()) chan2vals.append(chan1counts.get()) coincvals.append(chan1counts.get()) if cc.channel[0].count < 0: chan1counts.set("Overflow") if cc.channel[1].count < 0: chan2counts.set("Overflow") if cc.channel[2].count < 0: coinc_counts.set("Overflow") 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) a.legend([p1, p2, p3], ["Coincidences", "Channel 1", "Channel 2"]) 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) def gate_enable(): if gate_enabled.get(): cc.gate = True else: cc.gate = False def subtract_enable(): if subtract_enabled.get(): cc.subtract = True else: cc.subtract = False def trigger_enable(): if trigger_enabled.get(): cc.trigger = cc.TriggerMode.continuous else: cc.trigger = cc.TriggerMode.start_stop def parse(*args): cc.dwell_time = float(re.sub("[A-z]", "", dwell_time.get())) cc.window = float(re.sub("[A-z]", "", window.get())) def reset(*args): cc.reset() dwell_time.set(cc.dwell_time) window.set(cc.window) 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) # i is used to keep track of time i = 0 # read counts every 0.5 seconds time_diff = 0.5 root = tk.Tk() root.title("Qubitekk Coincidence Counter Control Software") # set up the Tkinter grid layout mainframe = tk.Frame(root) mainframe.padding = "3 3 12 12" mainframe.grid(column=0, row=0, sticky=(tk.N, tk.W, tk.E, tk.S)) mainframe.columnconfigure(0, weight=1) mainframe.rowconfigure(0, weight=1) # set up the label text dwell_time = tk.StringVar() dwell_time.set(cc.dwell_time) window = tk.StringVar() window.set(cc.window) chan1counts = tk.StringVar() chan1counts.set(cc.channel[0].count) chan2counts = tk.StringVar() chan2counts.set(cc.channel[1].count) coinc_counts = tk.StringVar() coinc_counts.set(cc.channel[2].count) gate_enabled = tk.IntVar() subtract_enabled = tk.IntVar() trigger_enabled = tk.IntVar() # set up the initial checkbox value for the gate enable if cc.gate: gate_enabled.set(1) else: gate_enabled.set(0) # set up the initial checkbox value for the trigger enable if cc.subtract: subtract_enabled.set(1) else: subtract_enabled.set(0) # set up the initial checkbox value for the trigger enable if cc.trigger: trigger_enabled.set(1) else: trigger_enabled.set(0) # set up the plotting area f = Figure(figsize=(10, 8), dpi=100) a = f.add_subplot(111, axisbg="black") t = [] coincvals = [] chan1vals = [] chan2vals = [] # a tk.DrawingArea canvas = FigureCanvasTkAgg(f, mainframe) 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.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) 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) # in 100 milliseconds, get the counts values off of the coincidence counter root.after(int(time_diff * 1000), getvalues, i) # start the GUI root.mainloop() ================================================ FILE: doc/examples/ex_qubitekkcc.py ================================================ #!/usr/bin/python from sys import platform as _platform import instruments as ik import instruments.units as u def main(): 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") if __name__ == "__main__": while True: main() ================================================ FILE: doc/examples/ex_qubitekkcc_simple.py ================================================ #!/usr/bin/python # Qubitekk Coincidence Counter example from sys import platform as _platform import instruments as ik 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) else: cc = ik.qubitekk.CC1.open_serial("COM8", 19200, timeout=1) 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(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") ================================================ FILE: doc/examples/ex_tekdpo70000.ipynb ================================================ { "metadata": { "name": "" }, "nbformat": 3, "nbformat_minor": 0, "worksheets": [ { "cells": [ { "cell_type": "code", "collapsed": false, "input": [ "%pylab inline" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "Populating the interactive namespace from numpy and matplotlib\n" ] } ], "prompt_number": 16 }, { "cell_type": "code", "collapsed": false, "input": [ "from __future__ import division" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 17 }, { "cell_type": "code", "collapsed": false, "input": [ "import instruments as ik" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 1 }, { "cell_type": "code", "collapsed": false, "input": [ "scope=ik.tektronix.TekDPO70000Series.open_tcpip('192.168.0.4',4005)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 2 }, { "cell_type": "code", "collapsed": false, "input": [ "scope._file._debug = True" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 3 }, { "cell_type": "code", "collapsed": false, "input": [ "scope._file.timeout = 2" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 4 }, { "cell_type": "code", "collapsed": false, "input": [ "scope.query('*IDN?')" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ " <- '*IDN?\\n' \n", " -> 'TEKTRONIX,DPO71254C,B130126,CF:91.1CT FV:5.3.0 Build 83\\n'\n" ] }, { "metadata": {}, "output_type": "pyout", "prompt_number": 5, "text": [ "'TEKTRONIX,DPO71254C,B130126,CF:91.1CT FV:5.3.0 Build 83\\n'" ] } ], "prompt_number": 5 }, { "cell_type": "code", "collapsed": false, "input": [ "ch0 = scope.channel[0]" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 6 }, { "cell_type": "code", "collapsed": false, "input": [ "ch0.bandwidth" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ " <- 'CH1:BAN?\\n' \n", " -> '12.5000E+9\\n'\n" ] }, { "metadata": {}, "output_type": "pyout", "prompt_number": 7, "text": [ "array(12500000000.0) * Hz" ] } ], "prompt_number": 7 }, { "cell_type": "code", "collapsed": false, "input": [ "_.to('GHz')" ], "language": "python", "metadata": {}, "outputs": [ { "metadata": {}, "output_type": "pyout", "prompt_number": 8, "text": [ "array(12.5) * GHz" ] } ], "prompt_number": 8 }, { "cell_type": "code", "collapsed": false, "input": [ "scope.outgoing_waveform_encoding = scope.WaveformEncoding.binary" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ " <- 'WFMO:ENC BINARY\\n' \n" ] } ], "prompt_number": 9 }, { "cell_type": "code", "collapsed": false, "input": [ "scope.select_fastest_encoding()" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ " <- 'DAT:ENC FAS\\n' \n" ] } ], "prompt_number": 10 }, { "cell_type": "code", "collapsed": false, "input": [ "scope.acquire_mode_actual" ], "language": "python", "metadata": {}, "outputs": [ { "ename": "timeout", "evalue": "timed out", "output_type": "pyerr", "traceback": [ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m\n\u001b[1;31mtimeout\u001b[0m Traceback (most recent call last)", "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m()\u001b[0m\n\u001b[1;32m----> 1\u001b[1;33m \u001b[0mscope\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0macquire_mode_actual\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[1;32mC:\\Users\\fpga\\AppData\\Local\\Enthought\\Canopy\\User\\lib\\site-packages\\instruments\\util_fns.pyc\u001b[0m in \u001b[0;36mgetter\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m 109\u001b[0m \u001b[1;32mreturn\u001b[0m \u001b[0mval\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0moutput_decoration\u001b[0m \u001b[1;32mis\u001b[0m \u001b[0mNone\u001b[0m \u001b[1;32melse\u001b[0m \u001b[0moutput_decoration\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mval\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 110\u001b[0m \u001b[1;32mdef\u001b[0m \u001b[0mgetter\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 111\u001b[1;33m \u001b[1;32mreturn\u001b[0m \u001b[0menum\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0min_decor_fcn\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mquery\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m\"{}?\"\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mformat\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mname\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mstrip\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 112\u001b[0m \u001b[1;32mdef\u001b[0m \u001b[0msetter\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mnewval\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 113\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msendcmd\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m\"{} {}\"\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mformat\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mname\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mout_decor_fcn\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0menum\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mnewval\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mvalue\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", "\u001b[1;32mC:\\Users\\fpga\\AppData\\Local\\Enthought\\Canopy\\User\\lib\\site-packages\\instruments\\abstract_instruments\\instrument.pyc\u001b[0m in \u001b[0;36mquery\u001b[1;34m(self, cmd, size)\u001b[0m\n\u001b[0;32m 111\u001b[0m \u001b[1;33m:\u001b[0m\u001b[0mrtype\u001b[0m\u001b[1;33m:\u001b[0m \u001b[1;33m`\u001b[0m\u001b[0mstr\u001b[0m\u001b[1;33m`\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 112\u001b[0m \"\"\"\n\u001b[1;32m--> 113\u001b[1;33m \u001b[1;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_file\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mquery\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mcmd\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0msize\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 114\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 115\u001b[0m \u001b[1;31m## PROPERTIES ##\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", "\u001b[1;32mC:\\Users\\fpga\\AppData\\Local\\Enthought\\Canopy\\User\\lib\\site-packages\\instruments\\abstract_instruments\\socketwrapper.pyc\u001b[0m in \u001b[0;36mquery\u001b[1;34m(self, msg, size)\u001b[0m\n\u001b[0;32m 139\u001b[0m '''\n\u001b[0;32m 140\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msendcmd\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mmsg\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 141\u001b[1;33m \u001b[0mresp\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mread\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0msize\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 142\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_debug\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 143\u001b[0m \u001b[1;32mprint\u001b[0m \u001b[1;34m\" -> {}\"\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mformat\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mrepr\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mresp\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", "\u001b[1;32mC:\\Users\\fpga\\AppData\\Local\\Enthought\\Canopy\\User\\lib\\site-packages\\instruments\\abstract_instruments\\socketwrapper.pyc\u001b[0m in \u001b[0;36mread\u001b[1;34m(self, size)\u001b[0m\n\u001b[0;32m 103\u001b[0m \u001b[0mc\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;36m0\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 104\u001b[0m \u001b[1;32mwhile\u001b[0m \u001b[0mc\u001b[0m \u001b[1;33m!=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_terminator\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 105\u001b[1;33m \u001b[0mc\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_conn\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mrecv\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;36m1\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 106\u001b[0m \u001b[0mresult\u001b[0m \u001b[1;33m+=\u001b[0m \u001b[0mc\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 107\u001b[0m \u001b[1;32mreturn\u001b[0m \u001b[0mbytes\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mresult\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", "\u001b[1;31mtimeout\u001b[0m: timed out" ] }, { "output_type": "stream", "stream": "stdout", "text": [ " <- 'ACQ:MOD:ACT?\\n' \n" ] } ], "prompt_number": 11 }, { "cell_type": "code", "collapsed": false, "input": [ "wav0 = ch0.read_waveform()" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ " <- 'DAT:SOU?\\n' \n", " -> 'CH1\\n'\n", " <- 'DAT:SOU CH1\\n' \n", " <- 'DAT:ENC FAS\\n' \n", " <- 'WFMO:BYT_N?\\n' \n", " -> '2\\n'" ] }, { "output_type": "stream", "stream": "stdout", "text": [ "\n", " <- 'WFMO:BN_F?\\n' \n", " -> 'RI\\n'\n", " <- 'WFMO:BYT_O?\\n' \n", " -> 'LSB\\n'\n", " <- 'CURV?\\n' \n", " <- 'CH1:SCALE?\\n' \n", " -> '100.0000E-3\\n'\n", " <- 'CH1:POS?\\n' \n", " -> '0.0000\\n'\n", " <- 'CH1:OFFS?\\n' \n", " -> '0.0000\\n'\n", " <- 'DAT:SOU CH1\\n' \n" ] } ], "prompt_number": 13 }, { "cell_type": "code", "collapsed": false, "input": [ "plot(wav0.magnitude)" ], "language": "python", "metadata": {}, "outputs": [ { "metadata": {}, "output_type": "pyout", "prompt_number": 15, "text": [ "[]" ] }, { "metadata": {}, "output_type": "display_data", "png": "iVBORw0KGgoAAAANSUhEUgAAAZAAAAEACAYAAACd2SCPAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJztnXt4VcW9978JdxW5yL0hUCDkApREgVAVCBQRylEQPVV6\n6mkLvo1QK1Wxam/S9xw9Xo49VFTM43uwHi+19VhKj1Z8QI3ghQQKag8GBSRivRLkEjRgSOb9Y5zs\n2ZOZWbPWXnvvtXd+n+fJs/dea9bMrJlZ85vv/GZWchhjDARBEAThk9x0Z4AgCILITMiAEARBEIEg\nA0IQBEEEggwIQRAEEQgyIARBEEQgyIAQBEEQgUjYgGzatAnFxcUoKCjAqlWrtGFuuukmjBgxAmed\ndRZ27drVdnzRokUYOHAgxo0bFxe+sbER8+bNQ35+PubPn49jx44lmk2CIAgiZBI2IMuWLUNVVRU2\nbtyIe++9Fw0NDXHna2trsXnzZmzbtg3Lly/H8uXL2859//vfx/r169vFuXr1auTn52P37t3Iy8vD\n/fffn2g2CYIgiJBJyIAcOXIEADB16lQMGzYMs2bNQk1NTVyYmpoaXHLJJejbty8WLlyIurq6tnNT\npkxBnz592sVbW1uLxYsXo1u3bli0aFG7OAmCIIj0k5AB2bp1K4qKitp+l5SUYMuWLXFhamtrUVJS\n0va7f//+2Lt3r3O8RUVFqK2tTSSbBEEQRBJIuhOdMQb1bSk5OTme1xAEQRDRpnMiF0+cOBHXX399\n2++dO3di9uzZcWHKy8vx5ptv4vzzzwcAHDhwACNGjPCMt66uDmVlZairq8PEiRPbhRk1apSnkiEI\ngiDiGTlyJPbs2RNKXAkpkF69egHgK7Hq6+uxYcMGlJeXx4UpLy/Hk08+iYMHD+Kxxx5DcXGxZ7zl\n5eVYs2YNmpqasGbNGkyePLldmL1797apm47+d/PNN6c9D1H5o7KgsqCysP+FOfBOeApr5cqVqKys\nxMyZM7F06VL069cPVVVVqKqqAgBMmjQJ5557LiZMmIC77roLd955Z9u1CxcuxNlnn423334bQ4cO\nxYMPPggAWLJkCfbv34/CwkK8//77uPLKKxPNJkEQBBEyCU1hAcC0adPiVlYBQGVlZdzv2267Dbfd\ndlu7a3/3u99p4+zZsyfWrVuXaNYIgiCIJEI70bOAioqKdGchMlBZxKCyiEFlkRxyGGMZueQpJycH\nGZp1giCItBFm30kKhCAIgggEGRCCIAgiEGRACIIgiECQASEIgiACQQaEIAiCCAQZEIIgCCIQZEAI\ngiCIQJABIQiCIAJBBoQgCIIIBBkQgiAIIhBkQAiCIIhAkAEhCIIgAkEGhCCIOBgDtm5Ndy6ITIDe\nxksQRBxvvgmMGcMNCZF90Nt4CYJIGi0t6c4BkSmQASEIIo5c6hUIR6ipEAQRR05OunNAZApkQAiC\niIMUCOEKNRWCIOIgA0K4Qk2FIIg4aAqLcIUMCEEQcZACIVyhpkIQRBykQAhXyIAQBBGHMCC0kZDw\nggwIQRBxkAEhXCEDQhBEHMJw0I50wgsyIARBxCEMSGtrevNBRB8yIARBxEEKhHCFDAhBEHGQAiFc\nIQNCEIQWUiCEF2RACIKIgxQI4QoZEIIg4iAfCOEKGRCCIOIgBUK4QgaEIIg4SIEQrpABIQhCCykQ\nwouEDcimTZtQXFyMgoICrFq1ShvmpptuwogRI3DWWWdh165dnteuWLECeXl5KCsrQ1lZGdavX59o\nNgmCcISmsAhXEjYgy5YtQ1VVFTZu3Ih7770XDQ0Ncedra2uxefNmbNu2DcuXL8fy5cuN1x48eBAA\nkJOTg2uvvRY7duzAjh07MHv27ESzSRCEIzSFRbiSkAE5cuQIAGDq1KkYNmwYZs2ahZqamrgwNTU1\nuOSSS9C3b18sXLgQdXV1xmu3bNnSdh2jN7kRRFogBUK4kpAB2bp1K4qKitp+l5SUxBkBgCuQkpKS\ntt/9+/fH3r17Pa9dtWoVJk+ejNtvvx2NjY2JZJMgCB+QAiFc6ZzsBBhj7dREjsd/rFmyZAl++ctf\n4ujRo7j++utRVVUVN/UlWLFiRdv3iooKVFRUhJFlgujQkALJLqqrq1FdXZ2UuHNYAnNFR44cQUVF\nBXbs2AEA+NGPfoTZs2dj7ty5bWFWrVqFkydP4pprrgEAjBw5Env37sXhw4cxffp067UA8Prrr2Pp\n0qV4+eWX4zOek0PTXASRBP72N+BrXwN27gSkyQMiSwiz70xoCqtXr14A+Gqq+vp6bNiwAeXl5XFh\nysvL8eSTT+LgwYN47LHHUFxcDADo3bu38doPP/wQAHDy5Ek89thj+OY3v5lINgmC8AEpEMKVhKew\nVq5cicrKSjQ3N+Pqq69Gv379UFVVBQCorKzEpEmTcO6552LChAno27cvHnnkEeu1AHDDDTfgtdde\nQ9euXTF16lQsWbIk0WwSBOEI+UAIVxKawkonNIVFEMnhtdeAsjJg+3b+SWQXkZnCIggi+yAFQrhC\nBoQgCC3kAyG8IANCEEQcpEAIV8iAEAQRB63CIlwhA5JBvPMO8OW2GQDAhg3A0aP+42EMWLUK+Oij\n8PLmwmuvAXv2xH5//jnwl7+4X79rF9+boOOPfwT++lfztfv2mc8zxq/3gjHgnnuAL1eZp4TGRuCu\nu4DmZv573Trg5MnkpplsBfLyy6ktQ7+89Rbwv/+rP9fSAvzpT7Hf6jMp8/TTwCuvhJevJ5+M1Y2O\n2lpg//7w0nOCZSgZnPXAjBjBmHzbAGMrVviP54sv+LVPPhle3lxYtoyxf//32O/Vq+Pvx4tu3czh\nAca+8Q3ztcXF5msPHWLstNO8029u5nE88YR32LB49VWeZn09/z1sGGN79iQ3zdpanuYLLyQnfoCx\nCy9MTtxhcOqp5rby7ruMDRkS+60+kzIAb3dhATD22Wf285Mnu8QTXt9JCiSDCGtKQcST6imK1tbE\n0vRaeWgbmduudc1XOspNTTPRMvRDR10l76eteNXFiRPh5Eng8RaolEMGJIMI64FOl5O0pSU+zdyI\ntD41X7Zw8mcqUOvKNa9hpNlRDYgNtfy9yigqbTxZZPntZRdhjTzT5SRVR29RGU1FWYGodZUKBUIG\nxIxfBRKVNp4syIBkENmmQKLycJEC0adJBqQ9fhVIWG08qnVCBiSDCKvxRMUHErYBCRpflBVIOnwg\ntIzXjFr+qTIg6XpmvSADkkFkmwIJe3446MMq8uT1cJICIdLlA4lqnZABySDIB5IcXEd3HcUHoqZN\nxEiXD4QMCJEwusYTpEFFRYFExYC4KgtSIITf8icDQkQG8oHYScQHIn8mGi5MyAcSLdLlA4lqnZAB\nySAy3QeiPnypXCNvK7tMUyC0jDd9qArEqy7CauMinajVCRmQDCLTfSBRncKKsgLR+UBoCit9tLby\ncnEtI5rCIiIDKRA7iU5heZWHa7gwSYcCUdMmYqhKgAwIkTFkug8k2QokVct40+UDESPfVCmQqM23\nRwF1GpN8IETGkG0KJJXYHuRMmcJKVScS1dFuFNAtarAR9kbCqNUJGZAMImwDkm4FEpUXzWWKEz1V\n6ZMBMeNXgdBGQiIyZLoTnTYS+keuq1SlH9XOKgqodUA+ECJjyPQprKj7QEiB6NMmYqTbBxK1OiED\nkkFkuhOdNhL6R7eBkDYSpo90K5Co1QkZkAwi2xQI+UC8IR9ItFDrgDYSEhkD+UCSQ5QVCPlAokW6\nFUjU6oQMSAaR6QrEZEBS8VBkw6tMUrUPJaqdVRQgH0g8ZEAyiEz3gZj+l0JY+egoPhDaSJg+0q1A\nolYnZEAyiGxTIGF3iNn+KpNU74SP2mg3CvjdSEg+ECIyZLoPJNkKJChRfpWJzgdCTvT0QVNY8ZAB\nySCyTYGkKx8qpED0aUats4oC6Z7CilqdkAHJIHSdb5AGRT6QeKKsQMgHEi3SrUCiVidkQDKURFYE\nRUWBpGNEr4MUiD7NqI12o4CrAhHHSYEQkSQRFUE+kHiirEDS4QNR0yZiuCqQsMuOnOhEqGSDAknl\nFFY2vM6dNhKmH79tJWy/ZdTqJGEDsmnTJhQXF6OgoACrVq3ShrnpppswYsQInHXWWdi1a5fntY2N\njZg3bx7y8/Mxf/58HDt2LNFsZg1iWWAmKpDWVr0CSYUhy6aNhLQKK33o6kA3OHH1kbgS1TpJ2IAs\nW7YMVVVV2LhxI+699140NDTEna+trcXmzZuxbds2LF++HMuXLzdee/DgQQDA6tWrkZ+fj927dyMv\nLw/3339/otnMGkRjTWQ6I12+B/XfsYY9ou4oGwnpZYrpI11tJap1kpABOXLkCABg6tSpGDZsGGbN\nmoWampq4MDU1NbjkkkvQt29fLFy4EHV1dcZrt2zZAoAbncWLF6Nbt25YtGhRuzg7MqKTTGQ+Pirv\nworKMt5MUyA0hZU+dG1At1mQFIgDW7duRVFRUdvvkpKSNiMgqK2tRUlJSdvv/v37Y+/evdZr5XNF\nRUWora1NJJtZRRgKhJzo8URZgZATPVro2oBO+YbtA4mqE71zshNgjIEpd51jmGsQx9XwJh56CPju\nd/Xnfvc7Xtjf/jZQXw9s2wZccok+7HvvAbffDkyfDhw+DPTrB3zyCbB7N1BZCfzpT0BREQ9XXw+s\nWAE0NgI/+QnwrW8BTzwBXHklMGkS8PzzwMMPAz/4ATB6NHDffcDUqcB//RewciXwwAPA3/4GDBsG\nfPwx8G//BnTqBNxxB/Av/8Lzc/w48P/+H/9eUAD06QOsXy/KiH+KTmTDBmDdOv7Zty+P94wz+H28\n+CJw4YXARRfF7vXhhwHhhmpp4fe4ejXw61/zNE8/HTh0iIfp1Am44gp+7yrvvAPcfz/w+ef8vv77\nv2P39f77QOfOQP/+wPDhwB//yMt2/36gWzdgyRKenpcC+fd/B559Frj2WmDOnNjxJUuAuXN5ejNm\nAFVVsbK55x5g1Chg9mx+X0OGAO++G7v2978HLr00lu6qVUDPnvx3czNw9dXAqafyemltBZYv53kF\nYg/x734H/OpXwBdfAC+9xOta5oYbgIoK4JlngB49gMsvB8aO5ef+/Gdg7Vqexg038Hi6deNl2KUL\nD3PjjXxUO39+rIw+/BD413/l32+5hbetN97geZgwgdfjddfx+L7/fZ7/n/+cXzNwIK+PvXuBwkJe\nL2ecweOaP59/bt3K8ztjBvCHP/BjL7/M296RI7ytnjgBlJTwuhw8OP6eW1v5PVx7LfDII8BrrwF3\n3glccw0Pf+AAb0uCffuAZcv4PY8fz8tIsHYtr9t33+Xlt3Ilb/9PPgmccgrQuzf/3L+fl+OttwKf\nfQb8z/8A3/sev+devYAf/YiXwY03AqedBvziF7w8zz8fmDIlPv+ffcaf0QMHeJsG+Od11/H4TjmF\n31vfvvFtAQBOngSWLgV++UteT7//PfDlbHxch9/SwttX3778uZ44kT/3c+cCDQ3AtGlAcXEs/J49\nPO2RI3nbBHjdvPYav5/nnuPt909/4s87AGzZwu931Chg0CBeZsuWAaWlvJ9S6y1hWAIcPnyYlZaW\ntv2+6qqr2FNPPRUX5u6772a//vWv236PGDGCMcbYoUOHjNcuWLCAbd++nTHG2LZt29jFF1/cLm0A\nDLiZ3Xwz/3vhhRfizufmMibu7r//m7F588z38fTTPGyvXvxz5Ej+CTC2cCH/HDcudmz7dsZeeol/\nv/hi/nnHHTyu66/nad9yC2O//S0/d9NN/HPnTsaKihi76qpYXK+9xthf/hLLK2OMvfMOY0OH8mPn\nnsvYtdfGwnfvzsN89BH//c//zPOQkxPL/7BhjJ13HmPl5Yx997tqucX+Lr2UsUcf5d9bW+PPib8H\nH9SX2Y9/HAvz6af6awHGfvhD/fF9+3jcAGN79ujTkMMzxljXrrHfpaX889/+LXbsggv459lnx64f\nPJixAQN4uctxMcbYwYOMnX46Y1VV/Pjvf8/LUZTx4cPx4R99lLHZsxkbNIiHveyy+PNyvufOjaV3\nzz2xc4sWMfa97zF26qk8juJixnr2ZOzjj9vf9/LljD3wQKxO5fLIyYl979Qplg+AsZtvjt2PfE15\nOWPDhzN25pnty2L58vZtX/f3jW8wtn59+3v+7DNeP4wxNnMmD9vc3L4O1XoFGJsxIz6uIUP48bFj\nGevRg7ev//N/eFs+7bT212/fztjmzYxNnhyL/7TTYt83boxP95//uX3+d+5kbPTo+Hh37+aftbX8\nmZSf95dean8vL7zA2P/9v/HHvv71WBpHj8af++1veV/x7W/r86XWn/wn+qVjx3gbV8+PHcvYD37A\n47/66hfYzTffzICbWd++N7MEu/04EprC6tWrFwC+mqq+vh4bNmxAeXl5XJjy8nI8+eSTOHjwIB57\n7DEUf2lie/fubby2vLwca9asQVNTE9asWYPJkycbcrACK1bwv4qKirgz8ghBnTpRYYx/njwZ/1sX\nRo4TiI0M5LnRrl3j05OnHlpa+AhIjUeN25RfkQ/GgAEDgHPO4XmQ02SMfx892n7fslM7kWkZWxqm\neOW0deXtFZerP+LkSR7GtFJGzkdzM1cDpjhbW/nIfdo0t3R1tLRwRdqjB08vL4+Pol3q24Q6B6+2\nTYFoD7q0RBxedWG6Xj4ulJTrVJsIr5Kfz8tJxD1lCtC9u1u+TjnFLW3T9eKY+BTlIspU1669+g1d\n/GpfoUvfK98ijVNO4e0XiJVb167AyJEVWLFiBYAV6NdvhXekPkh4CmvlypWorKxEc3Mzrr76avTr\n1w9VX84pVFZWYtKkSTj33HMxYcIE9O3bF4888oj1WgBYsmQJvvOd76CwsBBnnnkmbr/99oTyqDpv\nVUQF+OlE5U5H/t3ayh8I02ojcV495ye/Is85OXyaqblZn2aXLnw6zHYPXgbEpXO35dV0rrXVrXM0\n5cWP36K1Ve/oFOfkDrdLl/Ydr6ClhcfTqZN3uiYDIvIi6i03l/95lb+tjFTjqLZNgWgjurRU35oJ\n0/XycZFXXQfoMjgTeenUKVY2otxMb7c1veXAFd19yW1MNSBB7k0Xv/rc6tK3nVMXpXTqxL/n5HjH\nHwYJG5Bp06a1rawSVFZWxv2+7bbbcNtttzldCwA9e/bEunXrEs1aG64KxI9z0qZAunSJj0s+J86r\n5/zkV+RZPFCi4xNzt3I+PvvMfg9eI3nXUZDfc/LIKYgB8bNyqrXVXYF07tx+tZPovFpb+QMqFJ5X\nuqbjomNsbubfO3VKTIGo92ZSIKJd6tJy/edeNgUiJlBM6ZviVzs4kRdhaEWanTqZl2qbFmi4ki4F\novYVuvRt59R7Fn1LTo53/GHQIXaie43og0zjpEuByCPFnJx4A6JTIF737XXvLmWSqAJxLXcvBWJa\nDWMyIDoF0rlzLC01jaAKRM23bPi9FIhL2wxTgXh1vDYFop7XGRDbtWpe5LIR5WYyIGq+/BoQLwWi\n3pOuU/a6t2QrkNbWWPtNlQLpEAbEVYGoElz9rsYJuCkQmwExjeZ0eVHzrE5hqWl6jT5cfCDJVCBe\n96iiMyBy/LpOUBgIVwWiTpvIaQgFIs7b8q2rf3FcqI5EFIh8P34MiHy/MmH4QNTzrgpEjU+ewhLG\nOp0KRDeFZVIgarp+FIjJz2rKr5o3eQorN5cUSGh4KZBk+0BsU1imUYtLh2JTICIdr1GM11RQKhSI\n34ddjtcrf14KRJ1yEaNeuUNRFYg4r0NdkCGuk9P0o0CS4QOxTWG5lqfuuHo+6BSWMGZyWcvl5pIv\nv6NumwLRTWFFxQeiGs2M84FkAq4GxA9eCkTXmIMoEFueZWdsZ6UmU6lAbHm1GaZEDIgfH4hJgagd\nnawIbApEnt5SUTsaOR05jkQViIxfH0giCuTkyfANSBgKRDWMQaawwlAgtmNq/F7PqMtzpU7byfXo\nMohMlA6hQLymsIKOoAGzApHTC1OBmHwg6ugslQokSNnKo7ogDTwMBaJ2tGEoEF3nqbaFID6QVCkQ\nr463uTn8KSz13lUfiKxAbFNYamfqB51h9atAdNOaNlXk9Yy6PFeq0ZT7B5dBZKJ0CAPiV4HIjdSr\nchNVICYD4qJAZB+IGK3JcYShQJI5heXSOZrwo0C87k1MN508qVcg8qd8XkeyFIianlxmpn0g6lJi\nmwLRxaujudldgeiWMtuuFegUiCi3qDjRTQpErUcXBSLiMg0ETPlVwwgjK9JNxRRWhzAgXgrE1Rkq\nsDWo1tb4paByHCIf8nSTaTTn1weiUyCdOyeuQFxGLy7OPt3xMKawvDoNm4F0VSByWq4+kKAKRF3A\nEeZGQtEeEvGBJEOBqPHpfCByuamIe0pkCkv3zMn3pPq2TAbEZuhNCiTIs2d6bmUlKfoAUiAJ4leB\nqNfa4kv2Mt6cHPPcqosC8ZpHTacCCduJLuLRjVJto2EvH4g6hSXO69Lxo0BOnmy/LFi9n7Cd6F4K\nxKuz8aNAEp3CUlfEmRSIOG+bLvJCvV426nJb9XKi+zEgXs+oqwLRrcgjBRIifhWITXaq8YmOSZ3C\ncvWBmOKXK93UWMWITHREOh+I6b5l56QpDdtx1zDJUiBypy6wxaczIEEUiOjU5PzLeK3CUhWIHJ8u\nbJBlvCYFYNsHosu3DvFqGJUwFYhpI6HJByLOh6lAxEY8cc7Vie5nCstLIQRRIHJ78eoDwqBDGBAv\nBeL3XCoUiOm36kSXX4nhR4GI6YwoKJAgIySbAtERpgJxcXq7+kDU+NSwXvUDhONEF2UXZQViMiA6\nBeKq3tQ45Dz4VSC6fsamisJSIAJ5ACn3NaRAEiQZPhB1lGVTIDYDohs16ZxtujzLPhDxsMnX2EYf\nYuTj1UElU4GINIMoENnxLfBbjzYFIncGqgJJxImuUyCyE11tN6ZOUDeoUM/pfCCA3Zh6KZBEfSC6\ncvNSIOJ5s01h6RSIzgCYsCkQ24BRJogCSdQHYmoHpEBCxEuB+PWByJ1fGBsJ1Y5UTdM0TWNTICId\n0327TmGlQoGEZUBcwuvyplMgcoeiUyCm8vJyoouRtIsCcZ3mczUgcrqmfLsYkGQrEDEQkqf3RLmZ\n/vuf+oyr6k1dnKDLgzqal+s9qj4Q1eipeSEFEgKJGJCgCkQnXU0KRG0MppGUmmfdXLqcZqoUiK1s\nbYbJ1jm6LCeVP72ucTUgqVAgtpcpuioQ3X3Iaah5AOLTNeU7UQOSDB+IiwJRp+ZkNaCqC1P+5byp\nCiRZPpAwX2VCCiRJeE1h+R1Bi443J4c/cOp8qc6JLsKKjl+OXx3p2hSIzgcir+aRr3HxgYShQLzK\n1uT4tPlAvAyI6OgSMSByR5eTE44C0U0FJaJARPnZ7k01MqYOXKSrKwtXFejlRA9Dgag+EFmB6BB1\nJcejTmHpZgR0+ZfzoFMg4nkP6gNR95h5KRDbvhc1H+QDSRLJUiBCScgGw6RAunSJdfRq/DYFIq7X\n5Vn4QIBoKxDdPwzSTd2p18kG0VRHrgbEVI8iDvF/QMJSILp05Dj8KJDcXDcFovPpyKhtxJRHL1ym\nsEz/V8U0IJOxKRBTfmwKRD1ny7+cB51RBMydsosCUaevvV5lYvpnW7p+xqRAyIAkiJcCCeoDkQ2I\n/ACJzlketYgHSn0I/CoQ1Qcib7pKlw/ES4HoHgKv+X3R0XrlIywFItdPGD4QXTpyHH58IF4GRMSv\nqjKdAjEZED9+KNsIXpSbyYAk6gMx5cemQNRziSgQwNzpu/hA1Olrr1eZmAyIzvjofCDJ3khIL1NE\n8hSI/GDLI1w1/qAKRDYaOgViazxRUCA2AyI6Wq80xPSTvGxTh5cPxI8CMfmlTPdiUyDyXgc5LRGW\nMe//P2IyHKYpLNd8m0hEgfj1gYi68OsDkeMNokBUBaMaEBcFok49kgLJUMLwgciV46VA1B2/4m25\nOgPiV4HoXqYImDcSmu4tlT4QLwVieqBdFYipU5RxVSC6jYR+FIjXFI0ch7z4waRARDno2qF6b14G\nxDaFFSUFIpCNqx8For6WJYgCURWMiwJRB6rqfqEgCkR9y7Yal0mByIY8mQqkQxiQMBSIKkXlypVH\n+joFok6RqPHbFIhtFZZwogP+X6YoP5givO3+bXgpEN1D4FeBmPIhl6lXfKZ866awTArEayOhqwJR\nl197+UBs9SC3M92nIFMUiDqFJSsQ2xJcNR75vVVRUSDys5CoAlHfdSc/L6RAQiRsH4j8kADeCkRM\nYemc6HJHpevMTcZP5wPROdG9FIiXAUmmAhFxmzoF9X50o2+h7uR8uI5y5SlGWSGqU1jyZ6JOdD8K\nRDYgLipZdNi6FWpAeE70RBSIrh7VtHVOdJsCEQZCTGOKMpOX2Kvl6nVPav3L15heUqoO9lQDolMg\ntheeuvhAbFNYpEBCIhEFYmpsLj4Q0whXjd82haUaP91GQsDsRPdSIGFMYXl1bkF9IOr96JSMzoCo\nHYZXvkX9yK9zDzqF5TXC1ikQOT617tVl36ZlneI+5E912i7VCsS0XFj3z8/UfAL+nOjqsyNvmFSf\nf5salfPpdworqA9EpKMzrIk60UmBhEAiCsQ0heWiQExz7Gr8tiksmwKRVYdpGW+iCsRl9GILI9SX\n7hqvVVjq/eg6P9mAyFMWfpRTKpfx6hSIHJ9uCsvFxyPuQ/cpiIoPRDeNKxP0VSZyXKoC8ZrC0vlA\ngkxh+VUgXq8yCcOJTgokQbw6FNs5XaeeTgWi20gI+Fcgwm+TbAViMiDyQ2l6oF0ViAindmKu+Q5z\nGa9X5yQMo5+NhGEZkKj4QLwUiG0joUnhiTjkTbWpViDyswy0n3r0UiC6+PwoENpImCQSmcLSPYwu\nCkRuTMlSIC4bCVOhQGxlK+5dd424zlWBuE5huSoQnQFJhQIJspHQhUQUiJ9OxmZAhOEVPiVdOLUe\n/SgQU35kAyKeC/lfLXgpEPWYOrCTzydLgahthxRIRPA7hSWH1c0neykQdY+FvIw3UQUi51lVIKJz\ncGk8YfpAbGVrMiBeU1g6BeI1heVXgegMvB8Fosu3iw8kyEZCF1KlQGxTWC4KRPcMyAR9lQkQb0D8\nKBDdFJbbPPNOAAAgAElEQVRfBZKoD0TFrwIxbSQkBZIgfhWIbCyirEBUH4gcJ2BvPIkqEHVkZcKm\nQPz6QKKiQMJYxut3I6ELqdqJHvYUlk2B+NlIKOKS31ItzvtVIHK+5LYKuCsQdcpNNQg6I6DmybQP\nRFYgIo10bCTsEDvRvUakagHLDV/3MIpO389GwqA+ELXxy8dVBSLnWZb/OhLdSKj6hEy4KBDTA+2i\nQMTKKfFdXOtHOflVIOK8qTMzpaPGIfKeKh+IuldIJp1OdLXM5A7Rz0ZCQK9A5GfRK/+mewqykVB+\nHYo473cjoYsC0Q3EhGGhKawQ8KtAvAyI6Hi9nOhhKBC18ct5Vn0gcp51zl75PhN9lYn6YJgI6gMR\nHa0c3s8yXpeHJlMUSJhTWFFUIOoIX/7uZyMhYHai+1Ug4u3ZuvwlspHQrwJx8YHofCi0kTBE/PpA\nXAyIHwUibyTUjb7UTlztcGwGRKdA5P0M8rXyfaq7rU2NzOV4Mnwg6sjbNBJPZArLpkB0ZSOrh6AG\nRI7Dz0ZCF1w2Etoc0a7YRuviXxb4caKrHa2pnXptJBTpeykQL5Uo8mRStDYfiKpAkrWMV/QppoEY\nbSQMEdtIV3fcZQorTAWijj5VBWJqrCYfiEmByN9dl/G6HA+qQLyc6HJZid/qtFGyFIiubGT14OIb\nkvMup+mqQISRDXMZb7IViE4BqOmo92NTynJdmHwgqor38oGYpp9k5Ne566awXBSI6gMxKRDZUKl5\nMhkQ0WZ1U61yXsgH4sH8+cCRI+2Piwdlxgzgww/597IyYNAg4J13gK98JRa2X7/4a7/4IvZd9zCu\nXAk0NQHnnst/d+8O7NwJTJ8O/P3vQLduvPLWruXnT5wATj0VWL8eGDgwPq2nngIOHeLff/YzYPRo\n4LnnYvfw4YfAp5/GRqnHjwPnnAMUFbVXIOLhOnECOP103ng+/5znKycHyM+PpSsa3wsv8O+/+IW+\nfP/3f3kZigehZ09g7lzgf/4nFmbx4th3Ue7CR9PczMtD5dprgaFDY2F/8ANeRp9/zvP/5pvxD893\nvsN/y6O63FxeV6L+ROf10UfAggXmqaYpU3gbGD48dl3nzjyukyeBrl2Bn/wkVud33cX/qquByy7j\n50UdAUBFBc/XhRcCF1zQPr1XXuF1cP75MUPYtStPr0uXWD7/8z+BK66IXffGG8BLLwElJbFj+flA\nfX37NIBYfk+ciJW9TJcu7Tvv6dP5MVNHBfCyESN7xoCqKl7/o0fHwsiqR11lJupg+nTgk0/i4x40\niNfXFVcA773H20BNDT/XtSv/u+MO4ODB9savWzd+rytXAnv28GPnnBNrQ5WV/FhLS3x9vf02r4sh\nQ2Jl2dgYH3dLC38eu3QBHnkk1oeI8jh2DJg6Nf6aX/2K9wuCvDxehxMnAuPH87o866z4NGSl9uij\nwAcf8DLu25eXYffu0NLczM/dcgu/X4Bfl5cH7N8fP0vS0hJ7Rt9+Wx9fYFiGAoABjHXpwtjzz7f/\n++tf+d/06Yzxoo3/u+kmHm72bMaKihj7p39irKaGnxswgLE33mDs/PPjr+nUiYcVaWzYwI/feSdj\nmzbxYy+/zNj27YyNH89Y376MPfooYy+9xFhDAz+/axfP/65djF1zDb/+O99h7LHHGOvTJz69YcMY\ne+UVxl58kbHy8vhzhYWMVVQwdvAg/33llYzt28fY66/z+3jtNZ5ObS1P9+tf5+FWrOCf//RPjG3b\nxtgLLzC2ezcP88orjFVX8/NduzJ2zz38+9lnx+65Rw/GFixg7IYbGHv1VR4PwNizzzK2dStj/fsz\nNmUK/z5oED/3t7/xcnnvPcZ27mSsspIff/xxHnbduvb1s2gRL6P6esY2b+Zp19Ux1rkz/wMYe+IJ\nXu6ffsrYAw8w9pWvMDZ0KL+P559n7Lnn4uN8/XWej6FD26e3cCH/fPBBnqa434kT+fEZM/jnH//I\nWFMTL7cXXoil9S//wtgFFzD29ts8/upqxvbs4b+rqxm74w7GZs5k7LTTGDt6lLEPP+TXNTYy9sMf\nMnb33YyNG8fT+N73GHvrLcaqqvjvr30tViaHDsXK5NVXGfvWt2L38PjjvAyefz7WtsTfyy8zdvIk\nYxMmxI7dfTcPO2YMb9dy+Nxc/nn77fxZ2LqVtzc5zJw5/Pobb4zPQ7dujF10EW8nF13E71+UE8DY\nqFGM7djBy+fddxn7139tXx8PPMDYsWO8nP7hH/ixQ4d4uxdhfv97XgarVumf8W7dGLv4YsZuvZWx\nn/yEsUsv5W1o82bG8vN5mN/+lrHvfz/+ur/9LfZ9717eXuV0Fy7k96ZL8+67eZ2/+iqvQ/nc7bcz\n9sEHvC5ychj75jcZW7s2PsyMGbFyuugixh5+mPcnurT27eNha2v579NP5+X1yiuMDRzIWO/evL1O\nncr7ol/8QlwbXref8QqkSxc+sjExeLD++Nix/LpHH+Wjl8GDgUmT+Llhw4Bx44ARI+Kv6dSJj5hE\netu28c+uXfmoVvDGG9zq9+wJnH12bKQr57OwEDjjDP59xAgermfPmBoR6X396/y7bumj6kQX6chM\nnMg/BwzgnxMmxMLLo6FRo+KvGzIkFt/AgbG8i6mekhJg8mTgoYf48a9/nec/N5ePDCdMiOVt0CBe\n3oLevfnnuefy8heK4rTT+MhO5KewkH8fNix2bU5ObERcWMjrCeD5EdNDoswOHIi/p7w8PrLr04eP\ndmXECDwvj6cn0hw6FNi6lR8X5da9O1cdMseOAVu28Hvp1g2YNi12rqCAl9n69TEFMmgQ/xNxCsUG\n8HY4enRMWQvVMHYsL7vevWP5Ky6OpdPczNXY9OnAq6/G56+8vP1GwnPP5aq8Tx+uAmSEcpg8OVbG\nqpLMy+Npvf9+fB6EAjlxAujVK74sAF5GpaWx32eeiXaMH8/VyKmnxpSqqp4GDuT5M3H66byNiCmu\ns87iyh3gbRXgz93f/x5/nVB8+fn82Rw9Gti4MXa+S5dYO1UpK+P1XVDQPsyZZ/J+5owzYtOWot0J\nNTV4MC9TMcV5yik8Th15efHPPGOxNiVMhbxYprzcXFZByXgfiNf8sOm8OC6WU6prqIH2863qAyi+\nq9JaXsVjc4DqNkyp8ahhBSLPOie6Dvl+dfGpyHmXy1B2/srp6jY02spHXGP6f9+mehMGRI1XzFnL\n96Wma7tn8SCb8mo6L+dX+HV06cjOYC8fgFqmtjrWLd/WhZXbmi4dU9uzlaeaPzkPso9HRa1vXRg5\nXdP96+5JRd7JLl8v5129XsQrxy+Xj2mFmZoXW3mJtqBryyKMbtGNjHqNmCqU/8GanJZtmjIoGW9A\nvDpC0wMvP2i616zrru3cWd9AdJWva7S2POjm622NUeRZ18nb0nJ1yKr5k4/JHYPOkJjOCVRjo/NT\nmMotJyf2IOhWtOk6Ca84AXcDYio/YQRMgwbZGWxqL2qa4lN0Mrq0TQbE1CmaBkDqm3N1dWcqTzUP\n6iIBFbW+vZ49dcBhS18Xj65e1GdPRi0r1cDbDIguDV2a8tJkOU05rKn81HwKZN+gbKDURT9hkvEG\nxAsXBaKOXNURiKBzZ/MoRkZutLYOWx7h6JaG2hSIvNpEDavDlFcT8moZVYHIIyOdsTCdEySqQHSd\nvYjHRYHoBh0mAyF+h6lAdB2L3EGp5W4apar51SlDgW60rj4DuniDKhDbCFqtb12YsBWI+izaFIgu\nftnAuioQU1vKyYktE7bdk27jsS6sQJ45kaewIqlAGhsbMW/ePOTn52P+/Pk4ZpgU3LRpE4qLi1FQ\nUIBVq1Z5Xl9fX48ePXqgrKwMZWVlWLp0adAsAnBTIKoBEegMSJgKRH1lg+kh1uVF5wOx4TJak5E7\numQqEJMBsdWbrrMXu351HY8cxkRYCsRkQGyDirAUiNwh2fKpS0f3v0MAe3nq2oe8uz6sKSz5nuRr\ndapKxaT8bApEF7+rAXFRIHKctnryUiAqOgMi90WRMiCrV69Gfn4+du/ejby8PNx///3acMuWLUNV\nVRU2btyIe++9FwcPHvS8ftSoUdixYwd27NiB++67L2gWAQTzgQi8fCDpVCCJ+kC8sCkQ3UhX/kyX\nAlENiGl0pyMVCkR0QrrpCpsCsdWxqwLRhVefATVPal5dFIC6jDfdCkSezkm1AjENnMQ5m4EQ5Zeo\nAZGd6JEyILW1tVi8eDG6deuGRYsWoUYs3pY48uUykqlTp2LYsGGYNWsWtmzZ4nx9GLgokKj4QGwK\nRI0nqA/EtUEy5qZABOqCADlPNgWSm+tPgZh8IMKJrjPwapy69MJUIKa2ZOpQw1IgNh+ILryqwtU8\nqeFd4nTxgbgoEJMq8esDMT2LLgrEVD6uCkQ3UJDzZZuisrUXE6INiWdKnsKKnALZunUrir5cE1dU\nVITa2lprGAAoKSlpMyC26/ft24fS0lJUVlbi9ddfD5pFAOEqkCj5QESexV8yFYjOgNgavqsCEVNY\nQZ3oUVQgah7k86YOVYyS1TRVA5JKBSLP18v59IrTRYHYFosIvBbHyGG8FIjuWfQzhRVUgdjOJVuB\nqE50YUzCxhrleeedh4/UBeIAbrnlFjDdMM6BnC9rxXT9kCFD8N5776FPnz545plncPnll+ONN94I\nlBbgPrpwWcbrV4G4LuN1USAmH4i4Ppk+EN0Ulq3h66a11DDiM5EpLDWMOn0UNR+Iac67U6f4tx+o\n5Z4OBaKbwkqlAvFjQIIoEJcprEQViO2ciwLxWsarYlrGq053h4nVgGzYsMF47qGHHkJdXR3KyspQ\nV1eHiWLHmsTEiRNx/fXXt/3euXMnZs+e3XZOd33Xrl3RtWtXAMCcOXPws5/9DHv27MEodacbAGAF\njh8HVqwAKioqUFFR0S5EmApE7hzVOGREg/PjRA+qQHT50pEOBWJa9aQqkKBOdC8FoqYbxIC4KhCh\nImwGxFWBqIY32T4Q0VmpeQKSo0D8+kBM16ZKgajlIzay6ghLgXid12HygRw9Wg2gGvfc4x6XK4Gn\nsMrLy7FmzRo0NTVhzZo1mKzZEtqrVy8AfCVWfX09NmzYgPIvt0Oarm9oaEDLl+Z++/btaGpqMhgP\nAFiB7t1XYMWKFVrjAbgrEJd5WFcFIjoEryksuYMIokBcHgKvvJqQ5/KDKhATsjET87WmMCp+fCBq\nGFu+TKN8VYHY7tvmA7F1qGrdq8beRYHk5ARXIK5TWOn2gZjCJEuBqBsJxe9UKBCv8zrkfSCyAenc\nuQJduqzANdesALDCPUIHAhuQJUuWYP/+/SgsLMT777+PK6+8EgDwwQcfYO7cuW3hVq5cicrKSsyc\nORNLly5Fvy/ffGe6ftOmTRg/fjxKS0tx6623oqqqKpH7c1IgQLg+EFOjNcXvokBsMt/2EJjy6jVF\nEIYCMSErk2T6QHRhTLgqENt923wgtg5VrXu1Y3Txgagdjl8FYgrn1QbVOJPtA9EpTJsBCEuByOmk\nwgcSRIEIVB+IXCdhE9it0rNnT6xbt67d8SFDhuDpp59u+z1t2jTU1dU5X79gwQIsWLAgaLba4aJA\ngGAGxKZA/DrRdQrEq/NTfSg2UqVAXPKiGpBkLePVhTFhMhAunbi4zmsZb6IKxDZSlzsKWz5NCsQU\nzjaFJY94Bcl4lYkJLx+Iug/CrwJRy0dMXSVTgYiyCaJABKoPRFaFYZOEKKNFmAZEbWw2BWJ7rYUa\nv6nTdR3JuygQr6kNFdn46RSIrSP10+hlBSJ3LLZ6M01hyZ9yftQwOsJQIF5O9KAKxOa/MikQL+Ut\nf7cZpqBTWKn0gXh12mEpEGHIwzAgYSsQVZmphihZCiTrDYjrFJbLPKwfBSIvszWhdhC6eAQ2R7PL\nqN9rakPFS4HYOhM/Ix1RVoB+FK5iUiByfKbfiSgQPxsJTUbC5kRPlgKxKTLboEI31elnCitRBeLS\nhlwGRWH5QAC7AXFx6PtR8n4ViFpPYkBCCiRBwp7CclUgLqMH0xyrGr/L9WErEC8fSJgKRBgQ2ZFr\nGxVGWYHYfCC2ZbxBNxKq0ytqmzQZRPmcTf3qwqvIYeR/p5wqH0gQBSK3F1cFYvOBuOZF/u71KhPT\nBmdbXsV3+V158n+JDJusNyCuCiTIMl6bApHftGlCHbWocXuNnoMs4xXhvLbx2Hai2+7NrwKRV0+5\nGhDT69zlT6941PNhKhCTD8T2xgOdAvFSqPIxkwLRGUTTFJlObajHvIyN+m+BgyoQPz6QIArEjwFR\n+wrd/xkPokBsBkKcd+301XsT7VA2VKRAAhCmAlFH17aH1Ja2Gr8uHlXteF3vqkBcG5FtI6EtHr8N\nVSgQ9TULLlNYrgrEVjbytARgngILwwdiut6kQNSpE921cljbSy7l8KoBNg0UgPY+EN1uZtWI6YyU\njMvr3MNwootzXpt6vZSVqgR1rwQJokD8pO2FTpkJBSLiIQMSgGQqEK9pAlcDootH/K9s1+tdFUgY\nU1i2ePxKZaFA1I7JZvhNq7DkTzm87RUOIq5ENxK6KBDT9aoCMSkE2+hf7rx1ccjhVWNoq2dVgcid\np9p+xdSO3AZc2oKfKSxdGFsapiksNYxL/DYD4vVcqOdM9WOblbBhMiByvmgKKwCuCsQ2PSD/dmkE\nrgbElocuXeLT8nKih61AbE50Wzx+FYgssV1W/AgFYlqgoOswbWWjGhCvKSwXBWJrSy4KRO18XRSI\nOoVlG9yoxtBWz2qd2DpPYUC8FEgUNhKqYXTYVmF5hbXlV07Ty6i5dvqkQJJEOhSIK2qjk9PrSApE\ndryq6ejC68om3QpEniqxGbYgCsRlI6FJgah1IRsB2wDGVPe6ztOvAonCyxTVMLb41bK0TeMlqkBc\nz5vCA3oF4ve5dCXrDYhrBbkYEFcF4ortAVAViC1/LqMLv3lNpQ9EjA51HZguvFo24rj8Kcfjx4B4\nKRDbfdt8ILYOVRgfU9iwfSCqgtPlTfdcpFKBhOUD8aNATHWv1oOuPYWtQPwO+EzlJcdDCiQAyVQg\n4iEMS4HI+FUgXnlwuV+ZdCgQXTq6+NOpQGz37fUyRV38Ik6dE922yELNnzAgrj4Qr4GQzoCEpUBS\n9TJFMQJ3VSCmQYmsBOVVgDrSpUBM5UUKJEFcK8hlHlY30k9kbtHWGaudpG0HbjIUSFAfiN/yyM3V\nr1P3UiAmA6KrnzB9ILZRrNdGQl38Ik7dFJbANtCQOzfdMl6TD8RrNKwbaJgMiKpA5DYQVIG4tCHb\n7IHAjwJRDYPOiW5q3y7PFymQDCRsBaKbU06WAvEaMSWiQLwIqkD8lodfBRJkCsuPAUlEgXhtJNTF\nL+IU/hPdPdjK3OQDMdW3mNLzGg3rngvTFJYuD7b2FvZGQhthKhBxX7b8ek2nqWmmUoG4lJdfst6A\nuFaQqw8kTAVik+Dqa1PUNOXrk61AdB17WApE9oHoHjBdeNv0nt8pLL/7QFwUSBAfiOjkdCSiQHT3\nY1IgXvVsKstkKBA/PhAbLgpELkPdcRcFYvondLr45DjDUiBe95aM6SugAxiQbFEguiWuyVQg8ohN\nN7WRbAXiNYXlR4Ek4kT3q0AS8YGou5sFtoGGXB8urzKx+UB0x/wokC5d2q+oC1uByNeEpUC8prBc\nFIjJ+OvSkeNMtgIRcSRj+groAAbEtYJc5mHDViC20YHNB6LLe9gKxHRN2AokN1fvA7HFH6YPRF1Z\nE4YCMRkJ0/VeCsSljYatQEw+kFQpEJf27NLO/CgQ06DEjwLRoWubtvYgn0/UByLSSJYCScK/WY8W\n2aJA1DTl65OhQEzXZKICsT3c6oOs1kVYPhAvFSH7QPxeC/jzgbgoEJMPxJYHeRmvrZ0ksgpLvsZV\ngXj9d1BXBSJ8Ry73pMajKlOvqSWbARbx6fKqQ322woQUiEaqC9LpA7HN86t5DluB2JaPhq1AwvKB\nBF3Gq3aUJgd2oj4QEcamQLymsFKpQEx1r2urfhWI6XoZL+OQm+tvCitVPhBXvMrHplBsU6SmtEiB\nBMTLwmeKAknUB+LXgMirsDJdgXg9XDbC8oGIMMlSIJ06hatAdAMrU1n6VSAqQQyIqwKRyzbdPhBd\nmn6n1eR8eR2T0yIFEhDbSFY+n04FYjIgYSoQFZc5W12nnG4FIhSFV72q4U2INE3loXbetnyJfyNq\nMyDJUiDCgHgpa50CsSlNte4TVSCuS0lN4eR/o+sVl/BHef13UFNnrZvCClOBuAxsbHWuy2uQtBKh\nwxgQ16ks3Tn5d5gGxDbKsDnRdfP2YTYQr9GMnLaK37zk5sac6H4UiKlj0tWPbRTsmleTwpHPy8ZQ\nh2n0mpvrpkCCTGGp6dmmsLx8IKk0IF714trOhAJJ9kZCvwbES6nbFFwQBZKsKayMNiCjRwPjxtnD\nnHIK/zzzTP5ZVMQ/xZSEON+9O/88/XRg/Hj+Xa2Unj2B006LP9arVywOU9om1I7h1FNj5wYMaJ+W\nyMPYsfx7t27mfHnlRZSDjrIyYMyY2O8ePWLfRZpdu/LPUaPa50/kpaCAl6dKfn7se04OsG4d0NDA\ny0HEL+pD5dRTgYEDeToypg6+Z8/49MT9ifofOJB/etWhbapTkJsLPP44cPSo+byu8znlFOCVV4BP\nPuG/e/WKP/+Vr/BPuR7U/PXtC3zxRSyMOD5iRHz4nj1525LrxTRdpR4T1wpE3Yu2IOdBpK/m+cwz\nY+3XhtyZy22ssBDo04f/ibai0qUL7xsmTeL3tm8fsHt3fF7y8mLfxfGRI+PjGTaMf8rPZ69e+noo\nLTXfS1lZ+2NyWyguBr72Nf5dPJc6AyzClJXxZ0tm7Nj4ewLi1VqyFAhYhgKANTYy9tln9nCtrYx9\n8gljx44x9vHHjDU2Mvb55/Hn332XsZYW/vvECcZOnuTfV60SkxL878QJxpqb4+OX45I5eJCxQ4fs\nefvjH3m8b7zBfx86xK/7+GOezokTsbCzZ/OwR44w1tTE2Acf8E+Rh9ZWe1otLYwdOMC/NzXF7lfH\nsWO8nBjj18hhP/uMsQ8/jP1ubY3lg7H48mtq4vnVIcrtxhv5ff30p4yNHs1YWRm/fxPiOrXc9+/n\n8cye3T78yZOMNTTEjp08yfP58cf8uygXXV3K5Waqa0HXroxdeCFjVVX68716tc8fY7E2+MEH7dMQ\ndWsrk48/5vfzzjuMffFF/PEvvmDs009jx5qb+TE5nZUredmNGxdr65ddxj/FvTMWK7fPP+d1K7e5\nTz7h5/ft422kpYWx+vr27VJuWzIHDvDjR4/yuNTykev9yBF9OZ04weM/fJjfY3MzY9u38/soKIgP\nf+JE/POpltWhQ4wdP86/v/suj+P662Ppvv8+T6uhgR9rbo5vY4Kmpvg2JrjgAsZOOYV/b2zkccnP\n5be+xVhOTiz88eM8nk8+4eWrluHnn/OyEwCMdenCvw8YwNj06bFwYXb7Ge1E9xp1A3wE1b8//y6P\n8OXz8ghVjKbEORn5nEA3GgH4aMwlb0BsdNC7tzmsGE2IkePgwd55kMnNBfr1499No3uBXE7iGsEp\np8SP1nNy4uOTy6h7d3NaIs9ipNunT2ykJI9yTdep92xSCCLcGWfEh+3UKZaOuEddOcrl5lXOYipK\nVRDyedP8taqS1PzbykSc++pX9cf79Ikd0/1PC3kaTD3msowXiD1jw4fHjokRvIzuGQTi25lOXcr1\nrqsHcUx9RsV9yGUgwslh1bKSn0V5OlCkM2RI+/uR25hAtH/1OZLbgq4fU9uKUFuinFV0ZSKmROW4\nXPoKP2T0FFaySZrsU+J3mZ/0O8eaKchSPZG5Wi9fVypQHdm688mai04EnQ/AZcouE/Dy2aUqDl2c\nXj6QRNuK6DPIB5Imkv3w2JyjKn6XCWYKsrMwkbnaKHR4ubnxS2l159Np4EzoBjJRKM8w8DNIS2Yc\nuji9VmEl2lZS4QOJYHOODqkyIKRAwlMg6ezwXBRIFA2IToFEQdGFQZQViC2+MBWI10rERMjw5pFc\noqRAstWAdDQFEsUpLFcfSCYSZQXitbw8LINFGwnTBPlAkk+2+UDEnhbT+SiO6LPZBxJGu0iGAvEy\nEGG2FVIgaSJKCiRbfSCyEU1k1BWFEbNQIKZ7iLoCyUYfSBidfzIUiNdgKdG24vIKmjAgA2IhSgYk\nWxWI/IAnokCi0OFluhOdprDscaRSgSTaVuRryYmeJsiJnnyySYFk0zJeP4ObKBOmEz2VCiTRtuK6\nfydRMrx5JJdU+UBIgcSMR6JO9HR2eNmkQLze/ZUpdFQFor7DjBRIGoiSAukIPpCOsIw3ygpEzlu2\nGBBSIKRA0gb5QJJPWAokCiP7TFcgrm9+zSTCVCBhlgUpkA5AlBRIthqQsJbxCtJZTpmuQOQpLHkT\nWiYThgJJhhpLtgKR75cUSJogBZJ8wtpIKEjnVF+mKxDdP94iBZIcOrwCaWxsxLx585Cfn4/58+fj\n2LFj2nCbNm1CcXExCgoKsGrVqrbjTzzxBMaMGYNOnTph+/btcdfcfffdKCgoQElJCV566aWgWUwY\n2kiYfMJWIOnEZSNhFO/PNkrPdAMShcUVOjq8D2T16tXIz8/H7t27kZeXh/vvv18bbtmyZaiqqsLG\njRtx7733oqGhAQAwbtw4rF27FlOnTo0L/8knn+C+++7Dc889h9WrV+Pqq68OmsWEiZIC6QhO9DBG\nSuk0tC4bCaPWkQH6VUbZ4kSPwhsKdHR4BVJbW4vFixejW7duWLRoEWpqatqFOXLkCABg6tSpGDZs\nGGbNmtUWrqioCKNHj253TU1NDWbPno38/HxMmzYNjDE0NjYGzWZCJPshIh9IeBsJBek0tJ068X+d\narqHqL/KRM6baG/ZYkCipvy8OvVE24q6kTByCmTr1q0o+vL/LxYVFaG2ttYaBgBKSkqwZcsWa7y1\ntbUoLi5u+11YWKiNOxUkezMV+UCyT4HIn7rzUevIAH0nmy2KN1OnsBJtK6l6lYn1PxKed955+Oij\njyEtr98AAAvPSURBVNodv+WWW8CS9KTq4s0xDINWrFjR9r2iogIVFRWh5iXZewv8xJ+tBiSsZbyC\nKBiQTHWiy/nOlvaWqQokzCmsw4erUVtbDam7DA2rAdmwYYPx3EMPPYS6ujqUlZWhrq4OEydObBdm\n4sSJuP7669t+79y5E7Nnz7ZmqLy8HBs3bmz7vWvXLm3cQLwBSQapmMJybdjZMiJUCWsjoSDdy3iB\n7HCiZ5sBCcNwZ9IyXjmv/ftXYMyYijYD8qtf/Sp4xAqBi7W8vBxr1qxBU1MT1qxZg8mTJ7cL0+vL\nfw69adMm1NfXY8OGDSgvL28XTlYdkyZNwrPPPov9+/ejuroaubm56Kn+k+QUkQoD4tqws+WBVglb\ngaR7GS+QuQokGw1IVH04yVYgqg8kck70JUuWYP/+/SgsLMT777+PK6+8EgDwwQcfYO7cuW3hVq5c\nicrKSsycORNLly5Fvy//u/zatWsxdOhQbNmyBXPnzsWcOXMAAAMHDsSSJUswY8YMLF26FL/5zW8S\nub+EIAWSfEiBpB/dqzqytb1FhWxZxmudwrLRs2dPrFu3rt3xIUOG4Omnn277PW3aNNTV1bULd9FF\nF+Giiy7Sxr1s2TIsW7YsaNZCI9lLAP2MDLJlRKgil3G2+EAybRmvbqCUre0tKnT4ZbwdgSgpkGx9\noOXRb7YoENsy3igqEDIgqSdbFAgZEAvkA0k+pECiSba2t6jQ4X0gHYEoKZBsnZMOW4GkeyOh/Kk7\nH0UFoiNb21tUIAXSAUjFRsJMGZEmC1Ig0YQUSHvCLBPygXQAUrGRkHwg/DOsV5lE3QdCBoQAvNtC\nmAokkq8y6QhEyQeSrVMK9CqTaEIGpD1h/0Mpr1eZhKVAkjlwIQNiIUoGJFsfaNpIGE2ytb1FBXKi\ndwDIiZ58aCNhNMnW9hYVyIneAaCNhMmHnOjRJFvbW1QgJ3oHIEoKJFsfaNpIGE2ytb1FBVIgHYDe\nvfnnjBmA9G9NQqNXL2DYMLew//APwJQp4ech3Ygy7tkTGDQIGDgweFxDhwJlZeHkKwh9+vBP3f8W\nB4AhQ4D+/VOXH1cGD+afw4fHjn3jG2nJSlLIzQUKChKPZ8yYxOMQDBgQK3cd/fvz9hKUKVOAr32N\nfx88mKeXDHJYsv6xR5LJyclJ2v8kkZGTiOqbPTMdxsIp23T/F710px8GYdVFlMjGe/LC1hbD7DsD\nv0yxo9DRGl46CKuM011X6U4/DLLhHlSy8Z68SNU90xQWQRAEEQgyIARBEEQgyIAQBEEQgSADQhAE\nQQSCDAhBEAQRCDIgBEEQRCDIgBAEQRCBIANCEARBBIIMCEEQBBEIMiAEQRBEIMiAEARBEIEgA0IQ\nBEEEggwIQRAEEQgyIARBEEQgyIAQBEEQgSADQhAEQQSCDAhBEAQRCDIgBEEQRCDIgBAEQRCBIANC\nEARBBIIMCEEQBBEIMiAEQRBEIAIbkMbGRsybNw/5+fmYP38+jh07pg23adMmFBcXo6CgAKtWrWo7\n/sQTT2DMmDHo1KkTtm/f3na8vr4ePXr0QFlZGcrKyrB06dKgWSQIgiCSSGADsnr1auTn52P37t3I\ny8vD/fffrw23bNkyVFVVYePGjbj33nvR0NAAABg3bhzWrl2LqVOntrtm1KhR2LFjB3bs2IH77rsv\naBY7DNXV1enOQmSgsohBZRGDyiI5BDYgtbW1WLx4Mbp164ZFixahpqamXZgjR44AAKZOnYphw4Zh\n1qxZbeGKioowevTooMkTEvRwxKCyiEFlEYPKIjkENiBbt25FUVERAG4MamtrrWEAoKSkBFu2bPGM\ne9++fSgtLUVlZSVef/31oFkkCIIgkkhn28nzzjsPH330Ubvjt9xyCxhjScnQkCFD8N5776FPnz54\n5plncPnll+ONN95ISloEQRBEArCALFiwgG3fvp0xxti2bdvYxRdf3C7M4cOHWWlpadvvq666ij31\n1FNxYSoqKthf//pXYzplZWVs9+7d7Y6PHDmSAaA/+qM/+qM/H38jR44M2u23w6pAbJSXl2PNmjW4\n4447sGbNGkyePLldmF69egHgK7Hy8/OxYcMG3Hzzze3CMUnNNDQ0oE+fPm2rs5qamjBq1Kh21+zZ\nsydo1gmCIIgQCOwDWbJkCfbv34/CwkK8//77uPLKKwEAH3zwAebOndsWbuXKlaisrMTMmTOxdOlS\n9OvXDwCwdu1aDB06FFu2bMHcuXMxZ84cAMCLL76I8ePHo7S0FLfeeiuqqqoSuT+CIAgiSeQwliRn\nBkEQBJHVZOROdNPmxGzlvffew/Tp0zFmzBhUVFTgscceA2DfzHn33XejoKAAJSUleOmll9KV9aTQ\n0tKCsrIyXHDBBQA6bjkAwGeffYbvfve7GD16NEpKSlBTU9Mhy+OBBx7A2WefjbPOOgs//vGPAXSc\ndrFo0SIMHDgQ48aNazsW5N7r6upw5plnYsSIEfjZz37mlnho3pQUUlpayl588UVWX1/PCgsL2YED\nB9KdpaTy4Ycfsh07djDGGDtw4AD76le/yo4ePcpuv/12dtVVV7Hjx4+zH/7wh+zOO+9kjDH28ccf\ns8LCQvbuu++y6upqVlZWls7sh85dd93Fvv3tb7MLLriAMcY6bDkwxth1113Hfv7zn7OmpibW3NzM\nDh8+3OHK4+DBg2z48OHs2LFjrKWlhc2ZM4etX7++w5TDpk2b2Pbt29nYsWPbjgW59zlz5rDHH3+c\nNTQ0sHPOOYdt3brVM+2MUyC2zYnZyqBBg1BaWgoA6NevH8aMGYOtW7caN3PW1NRg9uzZyM/Px7Rp\n08AYQ2NjYzpvITT+/ve/4y9/+QuuuOKKtsUXHbEcBBs3bsRPf/pTdO/eHZ07d0avXr06XHn06NED\njDEcOXIETU1N+Pzzz9G7d+8OUw5TpkxBnz594o75uXehTt566y1ceumlOOOMM7BgwQKnfjXjDEjQ\nzYnZwp49e7Bz505MmjTJuJmzpqYGxcXFbdcUFhZqN3pmItdccw3uvPNO5ObGmm5HLAeAG9Pjx49j\nyZIlKC8vx+23346mpqYOVx49evTA6tWrMXz4cAwaNAjnnHMOysvLO1w5yPi595qaGuzZswcDBgxo\nO+7ar2acAenINDY24tJLL8V//Md/4LTTTvO1mTMnJyeJOUsNTz31FAYMGICysrK4e+9o5SA4fvw4\n3n77bVx88cWorq7Gzp078Yc//KHDlceBAwewZMkSvPnmm6ivr8err76Kp556qsOVg0yi9+56fcYZ\nkIkTJ2LXrl1tv3fu3Kndg5JtNDc34+KLL8bll1+OefPmAeBlUVdXB4A7wCZOnAiA79F58803267d\ntWtX27lM5pVXXsGf//xnfPWrX8XChQvx/PPP4/LLL+9w5SAYNWoUCgsLccEFF6BHjx5YuHAh1q9f\n3+HKo7a2FpMnT8aoUaNwxhln4B//8R+xefPmDlcOMn7vfdSoUfj444/bjr/55ptO/WrGGRB5c2J9\nfT02bNiA8vLyNOcquTDGsHjxYowdO7ZthQkQ28zZ1NQUt5lz0qRJePbZZ7F//35UV1cjNzcXPXv2\nTFf2Q+PWW2/Fe++9h3379uHxxx/HjBkz8PDDD3e4cpApKChATU0NWltb8fTTT2PmzJkdrjymTJmC\nbdu24dNPP8WJEyfwzDPPYNasWR2uHGSC3HtRUREef/xxNDQ0YO3atW79agiLAFJOdXU1KyoqYiNH\njmS/+c1v0p2dpLN582aWk5PDxo8fz0pLS1lpaSl75pln2NGjR9mFF17Ihg4dyubNm8caGxvbrlm5\nciUbOXIkKy4uZps2bUpj7pNDdXV12yqsjlwOb731FisvL2fjx49n1113HTt27FiHLI8HH3yQTZ06\nlU2YMIH9/Oc/Zy0tLR2mHC677DI2ePBg1rVrV5aXl8fWrFkT6N537tzJysrK2PDhw9mNN97olDZt\nJCQIgiACkXFTWARBEEQ0IANCEARBBIIMCEEQBBEIMiAEQRBEIMiAEARBEIEgA0IQBEEEggwIQRAE\nEQgyIARBEEQg/j8lnQrZEk2SlgAAAABJRU5ErkJggg==\n", "text": [ "" ] } ], "prompt_number": 15 }, { "cell_type": "code", "collapsed": false, "input": [], "language": "python", "metadata": {}, "outputs": [] } ], "metadata": {} } ] } ================================================ FILE: doc/examples/ex_thorlabslcc.py ================================================ # 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.mode = llc.Mode.voltage1 print("The current frequency is: ", lcc.frequency) print("The current voltage is: ", lcc.voltage1) ================================================ FILE: doc/examples/ex_thorlabssc10.py ================================================ # 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) 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("Setting shutter open time to 50 ms") sc.open_time = 50 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("Setting shutter close time to 50 ms") sc.open_time = 50 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("Setting repeat count to 8") sc.repeat = 8 print("The repeat count is: ", sc.repeat) print("setting mode to auto") sc.mode = sc.Mode.auto ================================================ FILE: doc/examples/ex_thorlabstc200.py ================================================ # Thorlabs Temperature Controller example import instruments as ik import instruments.units as u 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 print("The current mode is: ", tc.mode) tc.enable = True print("The current enabled state is: ", tc.enable) tc.p = 200 print("The current p gain is: ", tc.p) tc.i = 2 print("The current i gain is: ", tc.i) tc.d = 2 print("The current d gain is: ", tc.d) tc.degrees = u.degF print("The current degrees settings is: ", tc.degrees) tc.sensor = tc.Sensor.ptc100 print("The current sensor setting is: ", tc.sensor) tc.beta = 3900 print("The current beta settings is: ", tc.beta) tc.max_temperature = 150 * u.degC print("The current max temperature setting is: ", tc.max_temperature) tc.max_power = 1000 * u.mW print("The current max power setting is: ", tc.max_power) ================================================ FILE: doc/examples/ex_topticatopmode.py ================================================ #!/usr/bin/env python """ Toptica Topmode example """ import instruments as ik import instruments.units as u from platform import system if system() == "Windows": tm = ik.toptica.TopMode.open_serial("COM17", 115200) else: 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) print("The current lock state is: ", tm.locked) print("The current interlock state is: ", tm.interlock) print("The current fpga state is: ", tm.fpga_status) print("The current temperature state is: ", tm.temperature_status) print("The current current state is: ", tm.current_status) print("The laser1's serial number is: ", tm.laser[0].serial_number) print("The laser1's model number is: ", tm.laser[0].model) print("The laser1's wavelength is: ", tm.laser[0].wavelength) print("The laser1's production date is: ", tm.laser[0].production_date) 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 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 current emission state is: ", tm.enable) tm.laser[0].enable = True ================================================ FILE: doc/examples/example2.py ================================================ #!/usr/bin/python # 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 from instruments import * import numpy as np import matplotlib.pyplot as plt 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 ================================================ FILE: doc/examples/minghe/ex_minghe_mhs5200.py ================================================ #!/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) ================================================ FILE: doc/examples/qubitekk/ex_qubitekk_mc1.py ================================================ #!/usr/bin/python # Qubitekk Motor controller example from time import sleep from instruments.qubitekk import MC1 import instruments.units as u if __name__ == "__main__": mc1 = MC1.open_serial(vid=1027, pid=24577, baud=9600, timeout=1) mc1.step_size = 25 * u.ms mc1.inertia = 10 * u.ms print("step size:", mc1.step_size) print("inertial force: ", mc1.inertia) print("Firmware", mc1.firmware) print("Motor controller type: ", mc1.controller) print("centering") mc1.center() while mc1.is_centering(): 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 * 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)) mc1.move(x_pos) while mc1.move_timeout > 0: sleep(0.5) sleep(1) x_pos += mc1.increment ================================================ FILE: doc/examples/srs_DG645.ipynb ================================================ { "metadata": { "name": "srs_DG645" }, "nbformat": 3, "nbformat_minor": 0, "worksheets": [ { "cells": [ { "cell_type": "heading", "level": 1, "metadata": {}, "source": [ "GPIB-USB Communication Library Examples" ] }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Stanford Research Systems DG645 Digital Delay Generator" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In this example, we will demonstrate how to connect to an SRS DG645 digital delay generator, and how to set a new delay pattern." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We start by importing the `srs` package from within the main `instruments` package, along with the `quantities` package\n", "that is used to track physical quantities." ] }, { "cell_type": "code", "collapsed": false, "input": [ "from instruments.srs import SRSDG645\n", "import instruments.units as u" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 2 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Next, we open the instrument, assuming that it is connected via GPIB. Note that you may have to change this line\n", "to match your setup." ] }, { "cell_type": "code", "collapsed": false, "input": [ "ddg = SRSDG645.open_gpibusb('/dev/ttyUSB0', 15)" ], "language": "python", "metadata": {}, "outputs": [ { "ename": "AttributeError", "evalue": "'NoneType' object has no attribute 'terminator'", "output_type": "pyerr", "traceback": [ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m\n\u001b[1;31mAttributeError\u001b[0m Traceback (most recent call last)", "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m()\u001b[0m\n\u001b[1;32m----> 1\u001b[1;33m \u001b[0mddg\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mSRSDG645\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mopen_gpibusb\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'/dev/ttyUSB0'\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;36m15\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[1;32mC:\\Users\\cgranade\\AppData\\Local\\Enthought\\Canopy\\User\\lib\\site-packages\\instruments\\abstract_instruments\\instrument.pyc\u001b[0m in \u001b[0;36mopen_gpibusb\u001b[1;34m(cls, port, gpib_address, timeout, writeTimeout)\u001b[0m\n\u001b[0;32m 294\u001b[0m \u001b[0mtimeout\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mtimeout\u001b[0m\u001b[1;33m,\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 295\u001b[0m writeTimeout=writeTimeout)\n\u001b[1;32m--> 296\u001b[1;33m \u001b[1;32mreturn\u001b[0m \u001b[0mcls\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mgi_gpib\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mGPIBWrapper\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mser\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mgpib_address\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 297\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 298\u001b[0m \u001b[1;33m@\u001b[0m\u001b[0mclassmethod\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", "\u001b[1;32mC:\\Users\\cgranade\\AppData\\Local\\Enthought\\Canopy\\User\\lib\\site-packages\\instruments\\abstract_instruments\\gi_gpib.pyc\u001b[0m in \u001b[0;36m__init__\u001b[1;34m(self, filelike, gpib_address)\u001b[0m\n\u001b[0;32m 49\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_terminator\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;36m10\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 50\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_eoi\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;36m1\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 51\u001b[1;33m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_file\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mterminator\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;34m'\\r'\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 52\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_strip\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;36m0\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 53\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n", "\u001b[1;31mAttributeError\u001b[0m: 'NoneType' object has no attribute 'terminator'" ] }, { "output_type": "stream", "stream": "stdout", "text": [ "Serial connection error. Connection not added to serial manager. Error message:None\n" ] } ], "prompt_number": 3 }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can now set the delay on channel $A$ to be $B + 10\\ \\mu\\text{s}$." ] }, { "cell_type": "code", "collapsed": false, "input": [ "ddg.channel[ddg.Channels.A].delay = (ddg.Channels.B, u.Quantity(10, 'us'))" ], "language": "python", "metadata": {}, "outputs": [ { "ename": "NameError", "evalue": "name 'ddg' is not defined", "output_type": "pyerr", "traceback": [ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m\n\u001b[1;31mNameError\u001b[0m Traceback (most recent call last)", "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m()\u001b[0m\n\u001b[1;32m----> 1\u001b[1;33m \u001b[0mddg\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mchannel\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mddg\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mChannels\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mA\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mdelay\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;33m(\u001b[0m\u001b[0mddg\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mChannels\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mB\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mpq\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mQuantity\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;36m10\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m'us'\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[1;31mNameError\u001b[0m: name 'ddg' is not defined" ] } ], "prompt_number": 5 }, { "cell_type": "code", "collapsed": false, "input": [], "language": "python", "metadata": {}, "outputs": [] } ], "metadata": {} } ] } ================================================ FILE: doc/examples/srs_DG645.py ================================================ # 3.0 # # InstrumentKit Library Examples # # Stanford Research Systems DG645 Digital Delay Generator # # In this example, we will demonstrate how to connect to an SRS DG645 digital delay generator, and how to set a new delay pattern. # # 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 instruments.units as u # # Next, we open the instrument, assuming that it is connected via GPIB. Note that you may have to change this line # to match your setup. # ddg = SRSDG645.open_gpibusb("/dev/ttyUSB0", 15) # # We can now set the delay on channel $A$ to be $B + 10\ \mu\text{s}$. # ddg.channel[ddg.Channels.A].delay = (ddg.Channels.B, u.Quantity(10, "us")) # ================================================ FILE: doc/make.bat ================================================ @ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source set I18NSPHINXOPTS=%SPHINXOPTS% source if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\GPIBUSBAdapterDriverLibrary.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\GPIBUSBAdapterDriverLibrary.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end ================================================ FILE: doc/source/acknowledgements.rst ================================================ ================ Acknowledgements ================ Here I've done my best to keep a list of all those who have made a contribution to this project. All names listed below are the Github account names associated with their commits. 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. - 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 ================================================ FILE: doc/source/apiref/agilent.rst ================================================ .. TODO: put documentation license header here. .. currentmodule:: instruments.agilent ======= Agilent ======= :class:`Agilent33220a` Function Generator ========================================= .. autoclass:: Agilent33220a :members: :undoc-members: :class:`Agilent34410a` Digital Multimeter ========================================= .. autoclass:: Agilent34410a :members: :undoc-members: ================================================ FILE: doc/source/apiref/aimtti.rst ================================================ .. currentmodule:: instruments.aimtti ======= Aim-TTi ======= :class:`AimTTiEL302P` Power Supply ========================================= .. autoclass:: AimTTiEL302P :members: :undoc-members: ================================================ FILE: doc/source/apiref/comet.rst ================================================ .. currentmodule:: instruments.comet ===== Comet ===== :class:`CitoPlus1310` RF Generator ========================================= .. autoclass:: CitoPlus1310 :members: :undoc-members: ================================================ FILE: doc/source/apiref/config.rst ================================================ ========================== Configuration File Support ========================== .. currentmodule:: instruments The `instruments` package provides support for loading instruments from a configuration file, so that instrument parameters can be abstracted from the software that connects to those instruments. Configuration files recognized by `instruments` are `YAML`_ files that specify for each instrument a class responsible for loading that instrument, along with a URI specifying how that instrument is connected. Configuration files are loaded by the use of the `load_instruments` function, documented below. Functions ========= .. autofunction:: load_instruments .. _YAML: http://yaml.org/ ================================================ FILE: doc/source/apiref/delta_elektronika.rst ================================================ .. currentmodule:: instruments.delta_elektronika ================= Delta Elektronika ================= :class:`PscEth` Power Supply over Ethernet controller ===================================================== .. autoclass:: PscEth :members: :undoc-members: ================================================ FILE: doc/source/apiref/dressler.rst ================================================ .. currentmodule:: instruments.dressler ======== Dressler ======== :class:`Cesar1312` RF Generator =============================== .. autoclass:: Cesar1312 :members: :undoc-members: ================================================ FILE: doc/source/apiref/fluke.rst ================================================ .. TODO: put documentation license header here. .. currentmodule:: instruments.fluke ===== Fluke ===== :class:`Fluke3000` Industrial System ==================================== .. autoclass:: Fluke3000 :members: :undoc-members: ================================================ FILE: doc/source/apiref/generic_scpi.rst ================================================ .. TODO: put documentation license header here. .. _apiref-generic_scpi: .. currentmodule:: instruments.generic_scpi ======================== Generic SCPI Instruments ======================== :class:`SCPIInstrument` - Base class for instruments using the SCPI protocol ============================================================================ .. autoclass:: SCPIInstrument :members: :undoc-members: :class:`SCPIMultimeter` - Generic multimeter using SCPI commands ================================================================ .. autoclass:: SCPIMultimeter :members: :undoc-members: :class:`SCPIFunctionGenerator` - Generic multimeter using SCPI commands ======================================================================= .. autoclass:: SCPIFunctionGenerator :members: :undoc-members: ================================================ FILE: doc/source/apiref/gentec-eo.rst ================================================ .. currentmodule:: instruments.gentec_eo ========= Gentec-EO ========= :class:`Blu` Power Meter ======================================= .. autoclass:: Blu :members: :undoc-members: ================================================ FILE: doc/source/apiref/glassman.rst ================================================ .. TODO: put documentation license header here. .. currentmodule:: instruments.glassman ======== Glassman ======== :class:`GlassmanFR` Single Output Power Supply ============================================== .. autoclass:: GlassmanFR :members: :undoc-members: ================================================ FILE: doc/source/apiref/hcp.rst ================================================ .. TODO: put documentation license header here. .. currentmodule:: instruments.hcp ============ HC Photonics ============ :class:`TC038` Crystal oven AC ============================== .. autoclass:: TC038 :members: :undoc-members: :class:`TC038D` Crystal oven DC =============================== .. autoclass:: TC038D :members: :undoc-members: ================================================ FILE: doc/source/apiref/holzworth.rst ================================================ .. TODO: put documentation license header here. .. currentmodule:: instruments.holzworth ========= Holzworth ========= :class:`HS9000` Multichannel frequency synthesizer ================================================== .. autoclass:: HS9000 :members: :undoc-members: ================================================ FILE: doc/source/apiref/hp.rst ================================================ .. TODO: put documentation license header here. .. currentmodule:: instruments.hp =============== Hewlett-Packard =============== :class:`HP3456a` Digital Voltmeter ================================== .. autoclass:: HP3456a :members: :undoc-members: :class:`HP6624a` Power Supply ============================= .. autoclass:: HP6624a :members: :undoc-members: :class:`HP6632b` Power Supply ============================= .. autoclass:: HP6632b :members: :undoc-members: :class:`HP6652a` Single Output Power Supply =========================================== .. autoclass:: HP6652a :members: :undoc-members: :class:`HPe3631a` Power Supply ============================== .. autoclass:: HPe3631a :members: :undoc-members: ================================================ FILE: doc/source/apiref/index.rst ================================================ .. TODO: put documentation license header here. .. _apiref: InstrumentKit API Reference =========================== Contents: .. toctree:: :maxdepth: 2 instrument generic_scpi agilent aimtti comet dressler fluke gentec-eo glassman hcp holzworth hp keithley lakeshore minghe mettler_toledo newport ondax oxford pfeiffer phasematrix picowatt qubitekk rigol srs sunpower tektronix teledyne thorlabs toptica yokogawa config ================================================ FILE: doc/source/apiref/instrument.rst ================================================ .. TODO: put documentation license header here. .. currentmodule:: instruments ======================= Instrument Base Classes ======================= :class:`Instrument` - Base class for instrument communication ============================================================= .. autoclass:: Instrument :members: :undoc-members: :class:`Electrometer` - Abstract class for electrometer instruments =================================================================== .. autoclass:: instruments.abstract_instruments.Electrometer :members: :undoc-members: :class:`FunctionGenerator` - Abstract class for function generator instruments ============================================================================== .. autoclass:: instruments.abstract_instruments.FunctionGenerator :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 =============================================================== .. autoclass:: instruments.abstract_instruments.signal_generator.SignalGenerator :members: :undoc-members: :class:`SingleChannelSG` - Class for Signal Generators with a Single Channel ===================================================================================== .. autoclass:: instruments.abstract_instruments.signal_generator.SingleChannelSG :members: :undoc-members: :class:`SGChannel` - Abstract class for Signal Generator Channels ================================================================= .. autoclass:: instruments.abstract_instruments.signal_generator.SGChannel :members: :undoc-members: ================================================ FILE: doc/source/apiref/keithley.rst ================================================ .. TODO: put documentation license header here. .. currentmodule:: instruments.keithley ======== Keithley ======== :class:`Keithley195` Digital Multimeter ======================================= .. autoclass:: Keithley195 :members: :undoc-members: :class:`Keithley485` Picoammeter ================================ .. autoclass:: Keithley485 :members: :undoc-members: :class:`Keithley580` Microohm Meter =================================== .. autoclass:: Keithley580 :members: :undoc-members: :class:`Keithley2182` Nano-voltmeter ==================================== .. autoclass:: Keithley2182 :members: :undoc-members: .. _user's guide: http://www.keithley.com/products/dcac/sensitive/lowvoltage/?mn=2182A :class:`Keithley6220` Constant Current Supply ============================================= .. autoclass:: Keithley6220 :members: :undoc-members: :class:`Keithley6514` Electrometer ================================== .. autoclass:: Keithley6514 :members: :undoc-members: .. _Keithley 6514: http://www.tunl.duke.edu/documents/public/electronics/Keithley/keithley-6514-electrometer-manual.pdf ================================================ FILE: doc/source/apiref/lakeshore.rst ================================================ .. TODO: put documentation license header here. .. currentmodule:: instruments.lakeshore ========= Lakeshore ========= :class:`Lakeshore336` Cryogenic Temperature Controller ====================================================== .. autoclass:: Lakeshore336 :members: :undoc-members: :class:`Lakeshore340` Cryogenic Temperature Controller ====================================================== .. autoclass:: Lakeshore340 :members: :undoc-members: :class:`Lakeshore370` AC Resistance Bridge ========================================== .. autoclass:: Lakeshore370 :members: :undoc-members: :class:`Lakeshore475` Gaussmeter ================================ .. autoclass:: Lakeshore475 :members: :undoc-members: ================================================ FILE: doc/source/apiref/mettler_toledo.rst ================================================ .. TODO: put documentation license header here. .. currentmodule:: instruments.mettler_toledo ============== Mettler Toledo ============== :class:`MTSICS` MT Standard Interface Communication Software ============================================================ .. autoclass:: MTSICS :members: :undoc-members: ================================================ FILE: doc/source/apiref/minghe.rst ================================================ .. TODO: put documentation license header here. .. currentmodule:: instruments.minghe ====== Minghe ====== :class:`MHS5200` Function Generator =================================== .. autoclass:: MHS5200 :members: :undoc-members: ================================================ FILE: doc/source/apiref/newport.rst ================================================ .. 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: :class:`NewportError` ===================== .. autoclass:: NewportError :members: :undoc-members: :class:`PicoMotorController8742` ================================ .. autoclass:: PicoMotorController8742 :members: :undoc-members: ================================================ FILE: doc/source/apiref/ondax.rst ================================================ .. TODO: put documentation license header here. .. currentmodule:: instruments.ondax ===== Ondax ===== :class:`LM` Ondax SureLock Laser Module ======================================= .. autoclass:: LM :members: :undoc-members: ================================================ FILE: doc/source/apiref/oxford.rst ================================================ .. TODO: put documentation license header here. .. currentmodule:: instruments.oxford ====== Oxford ====== :class:`OxfordITC503` Temperature Controller ============================================ .. autoclass:: OxfordITC503 :members: :undoc-members: ================================================ FILE: doc/source/apiref/pfeiffer.rst ================================================ .. currentmodule:: instruments.pfeiffer =========================== Pfeiffer Vacuum Instruments =========================== :class:`TPG36x` Vacuum Gauge Controller ======================================= .. autoclass:: TPG36x :members: :undoc-members: ================================================ FILE: doc/source/apiref/phasematrix.rst ================================================ .. TODO: put documentation license header here. .. currentmodule:: instruments.phasematrix =========== PhaseMatrix =========== :class:`PhaseMatrixFSW0020` Signal Generator ============================================ .. autoclass:: PhaseMatrixFSW0020 :members: :undoc-members: ================================================ FILE: doc/source/apiref/picowatt.rst ================================================ .. TODO: put documentation license header here. .. currentmodule:: instruments.picowatt ======== Picowatt ======== :class:`PicowattAVS47` Resistance Bridge ======================================== .. autoclass:: PicowattAVS47 :members: :undoc-members: ================================================ FILE: doc/source/apiref/qubitekk.rst ================================================ .. TODO: put documentation license header here. .. currentmodule:: instruments.qubitekk ======== Qubitekk ======== :class:`CC1` Coincidence Counter ================================ .. autoclass:: CC1 :members: :undoc-members: :class:`MC1` Motor Controller ============================= .. autoclass:: MC1 :members: :undoc-members: ================================================ FILE: doc/source/apiref/rigol.rst ================================================ .. TODO: put documentation license header here. .. currentmodule:: instruments.rigol ===== Rigol ===== :class:`RigolDS1000Series` Oscilloscope ======================================= .. autoclass:: RigolDS1000Series :members: :undoc-members: ================================================ FILE: doc/source/apiref/srs.rst ================================================ .. TODO: put documentation license header here. .. currentmodule:: instruments.srs ========================= Stanford Research Systems ========================= :class:`SRS345` Function Generator ================================== .. autoclass:: SRS345 :members: :undoc-members: :class:`SRS830` Lock-In Amplifier ================================= .. autoclass:: SRS830 :members: :undoc-members: :class:`SRSCTC100` Cryogenic Temperature Controller =================================================== .. autoclass:: SRSCTC100 :members: :undoc-members: :class:`SRSDG645` Digital Delay Generator ========================================= .. autoclass:: SRSDG645 :members: :undoc-members: ================================================ FILE: doc/source/apiref/sunpower.rst ================================================ .. currentmodule:: instruments.sunpower ==================== Sunpower Instruments ==================== :class:`CryoTelGT` Cryocooler ============================= .. autoclass:: CryoTelGT :members: :undoc-members: ================================================ FILE: doc/source/apiref/tektronix.rst ================================================ .. TODO: put documentation license header here. .. currentmodule:: instruments.tektronix ========= Tektronix ========= :class:`TekAWG2000` Arbitrary Wave Generator ============================================ .. autoclass:: TekAWG2000 :members: :undoc-members: :class:`TekDPO4104` Oscilloscope ================================ .. autoclass:: TekDPO4104 :members: :undoc-members: :class:`TekDPO70000` Oscilloscope ================================= .. autoclass:: TekDPO70000 :members: :undoc-members: :class:`TekTDS224` Oscilloscope =============================== .. autoclass:: TekTDS224 :members: :undoc-members: :class:`TekTDS5xx` Oscilloscope =============================== .. autoclass:: TekTDS5xx :members: :undoc-members: ================================================ FILE: doc/source/apiref/teledyne.rst ================================================ .. TODO: put documentation license header here. .. currentmodule:: instruments.teledyne =============== Teledyne-LeCroy =============== :class:`MAUI` Oscilloscope Controller ======================================= .. autoclass:: MAUI :members: :undoc-members: ================================================ FILE: doc/source/apiref/thorlabs.rst ================================================ .. TODO: put documentation license header here. .. currentmodule:: instruments.thorlabs ======== ThorLabs ======== :class:`PM100USB` USB Power Meter ================================= .. autoclass:: PM100USB :members: :undoc-members: :class:`ThorLabsAPT` ThorLabs APT Controller ============================================ .. autoclass:: ThorLabsAPT :members: :undoc-members: .. autoclass:: APTPiezoInertiaActuator :members: :undoc-members: .. autoclass:: APTPiezoStage :members: :undoc-members: .. autoclass:: APTStrainGaugeReader :members: :undoc-members: .. autoclass:: APTMotorController :members: :undoc-members: :class:`SC10` Optical Beam Shutter Controller ============================================= .. autoclass:: SC10 :members: :undoc-members: :class:`LCC25` Liquid Crystal Controller ======================================== .. autoclass:: LCC25 :members: :undoc-members: :class:`TC200` Temperature Controller ===================================== .. autoclass:: TC200 :members: :undoc-members: ================================================ FILE: doc/source/apiref/toptica.rst ================================================ .. TODO: put documentation license header here. .. currentmodule:: instruments.toptica ======= Toptica ======= :class:`TopMode` Diode Laser ============================ .. autoclass:: TopMode :members: :undoc-members: .. _Toptica Topmode: http://www.toptica.com/fileadmin/user_upload/products/Diode_Lasers/Industrial_OEM/Single_Frequency/TopMode/toptica_BR_TopMode.pdf ================================================ FILE: doc/source/apiref/yokogawa.rst ================================================ .. 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: ================================================ FILE: doc/source/conf.py ================================================ # # InstrumentKit Library documentation build configuration file, created by # sphinx-quickstart on Fri Apr 5 10:37:03 2013. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. 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("../../")) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # 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", ] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix of source filenames. source_suffix = {".rst": "restructuredtext"} # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # General information about the project. project = "InstrumentKit Library" copyright = "2013-2025, 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 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 # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # 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" # If true, '()' will be appended to :func: etc. cross-reference text. # 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 # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # 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 = "sphinx_rtd_theme" # 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 = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # 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 # 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 # 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"] # 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' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is 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 = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. 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': '', } # 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", "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 # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ( "index", "instrumentkitlibrary", "InstrumentKit Library Documentation", ["Steven Casagrande"], 1, ) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( "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 = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { "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" ================================================ FILE: doc/source/devguide/code_style.rst ================================================ .. _code_style: ============ Coding Style ============ Data Types ========== Numeric Data ------------ 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. Boolean and Enumerated Data --------------------------- If a property or method argument can take exactly two values, of which one can be interpreted in the affirmative, use Python `bool` data types to represent this. Be permissive in what you accept as `True` and `False`, in order to be consistent with Python conventions for truthy and falsey values. This can be accomplished using the `bool` function to convert to Booleans, and is done implicitly by the `if` statement. If a property has more than two permissible values, or the two allowable values are not naturally interpreted as a Boolean (e.g.: positive/negative, AC/DC coupling, etc.), then consider using an `~enum.Enum` or `~enum.IntEnum` as provided by `enum`. The latter is useful in for wrapping integer values that are meaningful to the device. For example, if an instrument can operate in AC or DC mode, use an enumeration like the following:: class SomeInstrument(Instrument): # Define as an inner class. class Mode(Enum): """ When appropriate, document the enumeration itself... """ #: ...and each of the enumeration values. ac = "AC" #: The "#:" notation means that this line documents #: the following member, SomeInstrument.Mode.dc. dc = "DC" # For SCPI-like instruments, enum_property # works well to expose the enumeration. # This will generate commands like "MODE AC" # and "MODE DC". mode = enum_property( name=":MODE", enum=SomeInstrument.Mode, doc=""" And here is the docstring for this property """ ) # To set the mode is now straightforward. ins = SomeInstrument.open_somehow() ins.mode = ins.Mode.ac Note that the enumeration is an inner class, as described below in :ref:`associated_types`. Object Oriented Design ====================== .. _associated_types: Associated Types ---------------- Many instrument classes have associated types, such as channels and axes, so that these properties of the instrument can be manipulated independently of the underlying instrument:: >>> channels = [ins1.channel[0], ins2.channel[3]] Here, the user of ``channels`` need not know or care that the two channels are from different instruments, as is useful for large installations. This lets users quickly redefine their setups with minimal code changes. To enable this, the associated types should be made inner classes that are exposed using :class:`~instruments.util_fns.ProxyList`. For example:: class SomeInstrument(Instrument): # If there's a more appropriate base class, please use it # in preference to 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. # This will be useful in sending commands, and in exposing # via ProxyList. def __init__(self, parent, idx): self._parent = parent self._idx = idx # define some things here... @property def channel(self): return ProxyList(self, SomeInstrument.Channel, range(2)) This defines an instrument with two channels, having labels ``0`` and ``1``. By using an inner class, the channel is clearly associated with the instrument, 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: # stuff class SomeInstrument(Instrument): @property def channel(self): return ProxyList(self, _SomeInstrumentChannel, range(2)) 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: # stuff @property def channel(self): return ProxyList(self, _SomeInstrumentChannel, range(2)) _SomeInstrumentChannel = SomeInstrument.Channel ================================================ FILE: doc/source/devguide/design_philosophy.rst ================================================ ================= Design Philosophy ================= Here, we describe the design philosophy behind InstrumentKit at a high-level. Specific implications of this philosophy for coding style and practices are detailed in :ref:`code_style`. Pythonic ======== InstrumentKit aims to make instruments and devices look and feel native to the Python development culture. Users should not have to worry if a given instrument names channels starting with 1 or 0, because Python itself is zero- based. >>> scope.data_source = scope.channel[0] # doctest: +SKIP Accessing parts of an instrument should be supported in a way that supports standard Python idioms, most notably iteration. >>> for channel in scope.channel: # doctest: +SKIP ... channel.coupling = scope.Coupling.ground Values that can be queried and set should be exposed as properties. Instrument modes that should be entered and exited on a temporary basis should be exposed as context managers. In short, anyone familiar with Python should be able to read InstrumentKit-based programs with little to no confusion. Abstract ======== Users should not have to worry overmuch about the particular instruments that are being used, but about the functionality that instrument exposes. To a large degree, this is enabled by using common base classes, such as :class:`instruments.generic_scpi.SCPIOscilloscope`. While every instrument does offer its own unique functionality, by consolidating common functionality in base classes, users can employ some subset without worrying too much about the particulars. This also extends to communications methods. By consolidating communication logic in the :class:`instruments.abstract_instruments.comm.AbstractCommunicator` class, users can connect instruments however is convienent for them, and can change communications methods without affecting their software very much. Robust ====== Communications with instruments should be handled in such a way that errors are reported in a natural and Python-ic way, such that incorrect or unsafe operations are avoided, and such that all communications are correct. An important consequence of this is that all quantities communicated to or from the instrument should be *unitful*. In this way, users can specify the 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. ================================================ FILE: doc/source/devguide/index.rst ================================================ =============================== InstrumentKit Development Guide =============================== .. toctree:: :maxdepth: 2 design_philosophy code_style testing util_fns Introduction ============ This guide details how InstrumentKit is laid out from a developer's point of view, how to add instruments, communication methods and unit tests. 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 `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 project definitions:: $ pip install -e .[dev] Optional Development Dependencies --------------------------------- In addition to the required dev dependencies, there are optional ones. The package `tox`_ allows you to quickly run the tests against all supported versions of Python, assuming you have them installed. It is suggested that you install ``tox`` and regularly run your tests by calling the simple command:: $ tox More details on running tests can be found in :ref:`testing`. .. _tox: https://tox.readthedocs.org/en/latest/ Contributing Code ================= We love getting new instruments and new functionality! When sending in pull requests, however, it helps us out a lot in maintaining InstrumentKit as a usable library if you can do a couple things for us with your submission: - Make sure code follows `PEP 8`_ as best as possible. This helps keep the code readable and maintainable. - Document properties and methods, including units where appropriate. - Contributed classes should feature complete code coverage to prevent future changes from breaking functionality. This is especially important if the lead developers do not have access to the physical hardware. - Please use :ref:`property_factories` when appropriate, to consolidate parsing logic into a small number of easily-tested functions. This will also reduce the number of tests required to be written. We can help with any and all of these, so please ask, and thank you for helping make InstrumentKit even better. .. _PEP 8: http://legacy.python.org/dev/peps/pep-0008/ ================================================ FILE: doc/source/devguide/testing.rst ================================================ ================================ Testing Instrument Functionality ================================ .. currentmodule:: instruments.tests Overview ======== When developing new instrument classes, or adding functionality to existing instruments, it is important to also add automated checks for the correctness of the new functionality. Such tests serve two distinct purposes: - Ensures that the protocol for each instrument is being followed correctly, even with changes in the underlying InstrumentKit behavior. - Ensures that the API seen by external users is kept stable and consistent. The former is especially important for instrument control, as the developers 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 `pytest`_, a mature and flexible unit-testing framework for Python. When run from the command line via ``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 `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 be contained within its own file. Please see current tests as an example. If the number of tests for a given instrument is numerous, please consider making modules within a manufacturer test subpackage for each particular device. Below, we discuss two distinct kinds of unit tests: those that check that InstrumentKit functionality such as :ref:`property_factories` work correctly for new instruments, and those that check that existing instruments produce correct protocols. tox Based Testing ================= When submitting a PR, tests are run through the tool ``tox``. It helps to provide some isolation between your source code and test code, and gives you quick access to separate dedicated test venvs. While ``tox`` will setup and manage its virtual environments automatically, you will need a copy of each major version of Python you wish to test against. I suggest you use `pyenv`_ to do this. You can install this tool via the `pyenv-installer`_ tool. After installation, each Python version can be installed via the following pattern: .. code-block:: console $ pyenv install 3.9.21 Afterwards, you can use ``tox`` to run the tests under that specific Python environment: .. code-block:: console $ tox -e py39,py39-numpy Here we have specified two ``tox`` environments that will be run: both under Python 3.8, one with ``numpy`` installed and the other without. ``py39`` can be subsituted for other supported versions of Python, assuming they have been installed. You can also run all defined ``tox`` environments by simply running: .. code-block:: console $ tox Mock Instruments ================ TODO Expected Protocols ================== As an example of asserting correctness of implemented protocols, let's consider a simple test case for :class:`instruments.srs.SRSDG645`:: def test_srsdg645_output_level(): """ SRSDG645: Checks getting/setting unitful ouput level. """ with expected_protocol(ik.srs.SRSDG645, [ "LAMP? 1", "LAMP 1,4.0", ], [ "3.2" ], sep="\n" ) as ddg: 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 docstring that will be printed in reports on failing tests, and then has a call to :func:`expected_protocol`. The latter consists of specifying an instrument class, here given as ``ik.srs.DG645``, then a list of expected outputs and playback to check property accessors. Note that :func:`expected_protocol` acts as a context manager, such that it will, at the end of the indented block, assert the correct operation of the contents of that block. In this example, the second argument to :func:`expected_protocol` specifies that the instrument class should have sent out two strings, ``"LAMP? 1"`` and ``LAMP 1,4.0``, during the block, and should act correctly when given an answer of ``"3.2"`` back from the instrument. The third parameter, ``sep`` specifies what will be appended to the end of each lines in the previous parameters. This lets you specify the termination character that will be used in the communication without having to write it out each and every time. Protocol Assertion Functions ---------------------------- .. autofunction:: expected_protocol .. _pytest: https://docs.pytest.org/en/latest/ .. _pyenv: https://github.com/pyenv/pyenv .. _pyenv-installer: https://github.com/pyenv/pyenv-installer ================================================ FILE: doc/source/devguide/util_fns.rst ================================================ ============================= Utility Functions and Classes ============================= .. currentmodule:: instruments.util_fns Unit Handling ============= .. autofunction:: assume_units .. autofunction:: split_unit_str .. autofunction:: convert_temperature Enumerating Instrument Functionality ==================================== To expose parts of an instrument or device in a Python-ic way, the :class:`ProxyList` class can be used to emulate a list type by calling the initializer for some inner class. This is used to expose everything from channels to axes. .. _property_factories: Property Factories ================== To help expose instrument properties in a consistent and predictable manner, InstrumentKit offers several functions that return instances of `property` that are backed by the :meth:`~instruments.Instrument.sendcmd` and :meth:`~instruments.Instrument.query` protocol. These factories assume a command protocol that at least resembles the SCPI style:: -> FOO:BAR? <- 42 -> FOO:BAR 6 -> FOO:BAR? <- 6 It is recommended to use the property factories whenever possible to help reduce the amount of copy-paste throughout the code base. The factories allow for a centralized location for input/output error checking, units handling, and type conversions. In addition, improvements to the property factories benefit all classes that use it. Lets say, for example, that you were writing a class for a power supply. This class might require these two properties: ``output`` and ``voltage``. The first will be used to enable/disable the output on the power supply, while the second will be the desired output voltage when the output is enabled. The first lends itself well to a `bool_property`. The output voltage property corresponds with a physical quantity (voltage, of course) and so it is best to use either `unitful_property` or `bounded_unitful_property`, depending if you wish to bound user input to some set limits. `bounded_unitful_property` can take either hard-coded set limits, or it can query the instrument during runtime to determine what those bounds are, and constrain user input to within them. Examples -------- These properties, when implemented in your class, might look like this:: output = bool_property( "OUT", inst_true="1", inst_false="0", doc=""" Gets/sets the output status of the power supply :type: `bool` """ ) voltage, voltage_min, voltage_max = bounded_unitful_property( voltage = unitful_property( "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 `~pint.Quantity` """ ) The most difficult to use parameters for the property factories are ``input_decoration`` and ``output_decoration``. These are callable objects that will be applied to the data immediately after receiving it from the instrument (input) or before it is inserted into the string that will be sent out to the instrument (output). Using `enum_property` as the simple example, a frequent use case for ``input_decoration`` will be to convert a `str` containing a numeric digit into an actual `int` so that it can be looked up in `enum.IntEnum`. Here is an example of this:: class Mode(IntEnum): """ Enum containing valid output modes of the ABC123 instrument """ foo = 0 bar = 1 bloop = 2 mode = enum_property( "MODE", enum=Mode, input_decoration=int, set_fmt="{}={}", doc=""" Gets/sets the output mode of the ABC123 instrument :rtype: `ABC123.Mode` """ ) So in this example, when querying the ``mode`` property, the string ``MODE?`` will first be sent to the instrument, at which point it will return one of ``"0"``, ``"1"``, or ``"2"``. However, before this value can be used to get the correct enum value, it needs to be converted into an `int`. This is what ``input_decoration`` is used for. Since `int` is callable and can convert a `str` to an `int`, this accomplishes exactly what we're looking for. Pretty much anything callable can be passed into these parameters. Here is an example using a lambda function with a `unitful_property` taken from the `~instruments.thorlabs.TC200` class:: temperature = unitful_property( "tact", 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 `~pint.Quantity`) or assumed to be of units degrees C. :type: `~pint.Quantity` or `int` :return: the temperature (in degrees C) :rtype: `~pint.Quantity` """ ) An alternative to lambda functions is passing in static methods (`staticmethod`). Bool Property ------------- .. autofunction:: bool_property Enum Property ------------- .. autofunction:: enum_property Unitless Property ----------------- .. autofunction:: unitless_property Int Property ------------ .. autofunction:: int_property Unitful Property ---------------- .. autofunction:: unitful_property Bounded Unitful Property ------------------------ .. autofunction:: bounded_unitful_property 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 ================================================ FILE: doc/source/index.rst ================================================ .. TODO: put documentation license header here. Welcome to InstrumentKit Library's documentation! ================================================= Contents: .. toctree:: :maxdepth: 1 intro apiref/index devguide/index acknowledgements Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` ================================================ FILE: doc/source/intro.rst ================================================ .. TODO: put documentation license header here. ============ Introduction ============ **InstrumentKit** allows for the control of scientific instruments in a platform-independent manner, abstracted from the details of how the instrument is connected. In particular, InstrumentKit supports connecting to instruments via serial port (including USB-based virtual serial connections), GPIB, USBTMC, TCP/IP or by using the VISA layer. Installing ========== Dependencies ------------ Most of the required and optional dependencies can be obtained using ``pip``. Getting Started =============== Instruments and Instrument Classes ---------------------------------- Each make and model of instrument that is supported by InstrumentKit is represented by a specific class, as documented in the :ref:`apiref`. Instruments that offer common functionality, such as multimeters, are represented by base classes, such that specific instruments can be exchanged without affecting code, so long as the proper functionality is provided. For some instruments, a specific instrument class is not needed, as the :ref:`apiref-generic_scpi` classes can be used to expose functionality of these instruments. If you don't see your specific instrument listed, then, please check in the instrument's manual whether it uses a standard set of SCPI commands. Connecting to Instruments ------------------------- Each instrument class in InstrumentKit is constructed using a *communicator* class that wraps a file-like object with additional information about newlines, terminators and other useful details. Most of the time, it is easiest to not worry with creating communicators directly, as convienence methods are provided to quickly connect to instruments over a wide range of common communication protocols and physical connections. For instance, to connect to a generic SCPI-compliant multimeter using a `Galvant Industries GPIB-USB adapter`_, the `~instruments.Instrument.open_gpibusb` method can be used:: >>> import instruments as ik >>> inst = ik.generic_scpi.SCPIMultimeter.open_gpibusb("/dev/ttyUSB0", 1) Similarly, many instruments connected by USB use an FTDI or similar chip to emulate serial ports, and can be connected using the `~instruments.Instrument.open_serial` method by specifying the serial port device file (on Linux) or name (on Windows) along with the baud rate of the emulated port:: >>> inst = ik.generic_scpi.SCPIMultimeter.open_serial("COM10", 115200) As a convienence, an instrument connection can also be specified using a uniform resource identifier (URI) string:: >>> inst = ik.generic_scpi.SCPIMultimeter.open_from_uri("tcpip://192.168.0.10:4100:) Instrument connection URIs of this kind are useful for storing in configuration files, as the same method, `~instruments.Instrument.open_from_uri`, is used, regardless of the communication protocol and physical connection being used. InstrumentKit provides special support for this usage, and can load instruments from specifications listed in a YAML-formatted configuration file. See the `~instruments.load_instruments` function for more details. .. _Galvant Industries GPIB-USB adapter: http://galvant.ca/shop/gpibusb/ Using Connected Instruments --------------------------- Once connected, functionality of each instrument is exposed by methods and properties of the instrument object. For instance, the name of an instrument can be queried by getting the ``name`` property:: >>> print(inst.name) For details of how to use each instrument, please see the :ref:`apiref` entry for that instrument's class. If that class does not implement a given command, raw commands and queries can be issued by using the `~instruments.Instrument.sendcmd` and `~instruments.Instrument.query` methods, respectively:: >>> inst.sendcmd("DATA") # Send command with no response >>> resp = inst.query("*IDN?") # Send command and retrieve response OS-Specific Instructions ======================== Linux ----- Raw USB Device Configuration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To enable writing to a USB device in raw or usbtmc mode, the device file must be readable writable by users. As this is not normally the default, you need to add rules to ``/etc/udev/rules.d`` to override the default permissions. 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. ================================================ FILE: license/AUTHOR.TXT ================================================ Original author: Steven Casagrande stevencasagrande@gmail.com twitter.com/stevecasagrande 2012-2025 ================================================ FILE: license/LICENSE.TXT ================================================ GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 Copyright © 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Remote Network Interaction; Use with the GNU General Public License. Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS ================================================ FILE: matlab/matlab-example.ipynb ================================================ { "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Loading Instruments from MATLAB #" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "MATLAB 2016a supports calling into Python, such that we can open instruments and communicate with them from within MATLAB applications. This takes a little bit of work, however, due to bugs in MATLAB's Python interface. Here, we'll demonstrate using the ``open_instrument.m`` MATLAB function to open instruments from their URIs." ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "collapsed": false }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "\n", " File build/bdist.linux-x86_64/egg/serial/serialposix.py, line 294, in open\n", "\n", " File build/bdist.linux-x86_64/egg/serial/serialutil.py, line 180, in __init__\n", "\n", " File build/bdist.linux-x86_64/egg/instruments/abstract_instruments/comm/serial_manager.py, line 64, in new_serial_connection\n", "\n", " File build/bdist.linux-x86_64/egg/instruments/abstract_instruments/instrument.py, line 438, in open_serial\n", "\n", " File build/bdist.linux-x86_64/egg/instruments/abstract_instruments/instrument.py, line 355, in open_from_uri\n", "\n", " File , line 1, in \n", "Python Error: SerialException: [Errno 2] could not open port /dev/ttyUSB0: [Errno 2] No such file or directory: '/dev/ttyUSB0'\n", "\n" ] } ], "source": [ "instrument = open_instrument('phasematrix.PhaseMatrixFSW0020', 'serial:/dev/ttyUSB0')" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "MLegacy Kernel", "language": "python", "name": "kernel_mlegacy" }, "language_info": { "file_extension": ".m", "help_links": [ { "text": "MetaKernel Magics", "url": "https://github.com/calysto/metakernel/blob/master/metakernel/magics/README.md" } ], "mimetype": "text/x-matlab", "name": "matlab" } }, "nbformat": 4, "nbformat_minor": 0 } ================================================ FILE: matlab/open_instrument.m ================================================ function instrument = open_instrument(name, uri) % open_instrument Opens an instrument given its InstrumentKit URI. % % WARNING: this function can execute arbitrary Python code, so do *not* % call for untrusted URIs. % We need to use py.eval, since using py.* directly doesn't work for % @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 ================================================ FILE: pyproject.toml ================================================ [build-system] requires = ["setuptools>=61.2", "setuptools_scm>=9.2"] build-backend = "setuptools.build_meta" [project] name = "instrumentkit" description = "Test and measurement communication library" authors = [{name = "Steven Casagrande", email = "stevencasagrande@gmail.com"}] license = {text = "AGPLv3"} classifiers = [ "Development Status :: 4 - Beta", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Operating System :: OS Independent", "Intended Audience :: Science/Research", "Intended Audience :: Manufacturing", "Topic :: Scientific/Engineering", "Topic :: Scientific/Engineering :: Interface Engine/Protocol Translator", "Topic :: Software Development :: Libraries", ] dependencies = [ "pint>=0.21.0", "pyserial>=3.3", "python-usbtmc", "python-vxi11>=0.8", "pyusb>=1.0", "pyvisa>=1.9", "ruamel.yaml>=0.18", "typing_extensions>=4.0.1", "standard-xdrlib;python_version>='3.13'", ] dynamic = ["version"] [project.readme] file = "README.rst" content-type = "text/x-rst" [project.urls] Homepage = "https://www.github.com/instrumentkit/InstrumentKit" [project.optional-dependencies] numpy = ["numpy"] dev = [ "coverage", "hypothesis~=6.139.2", "mock", "pytest-cov", "pytest-mock", "pytest-xdist", "pytest~=8.4.0", "pyvisa-sim", "six", ] docs = [ "sphinx~=8.2.0", "sphinx_rtd_theme" ] [tool.setuptools] include-package-data = true package-dir = {"" = "src"} [tool.setuptools.packages.find] where = ["src"] namespaces = false [tool.distutils.bdist_wheel] universal = 1 [tool.setuptools_scm] write_to = "src/instruments/_version.py" ================================================ FILE: src/instruments/__init__.py ================================================ #!/usr/bin/env python """ Defines globally-available subpackages and symbols for the instruments package. """ # IMPORTS #################################################################### __all__ = ["units"] from . import abstract_instruments from .abstract_instruments import Instrument from . import agilent from . import aimtti from . import comet from . import dressler from . import delta_elektronika from . import generic_scpi from . import fluke from . import gentec_eo from . import glassman from . import hcp from . import holzworth from . import hp from . import keithley from . import lakeshore from . import mettler_toledo from . import minghe from . import newport from . import oxford from . import phasematrix from . import pfeiffer from . import picowatt from . import qubitekk from . import rigol from . import srs from . import sunpower from . import tektronix from . import teledyne from . import thorlabs from . import toptica from . import yokogawa from .config import load_instruments from .units import ureg as units ================================================ FILE: src/instruments/abstract_instruments/__init__.py ================================================ #!/usr/bin/env python """ Module containing instrument abstract base classes and communication layers """ from .instrument import Instrument from .electrometer import Electrometer from .function_generator import FunctionGenerator from .multimeter import Multimeter from .oscilloscope import Oscilloscope from .optical_spectrum_analyzer import OpticalSpectrumAnalyzer from .power_supply import PowerSupply ================================================ FILE: src/instruments/abstract_instruments/comm/__init__.py ================================================ #!/usr/bin/env python """ Module containing communication layers """ 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 .usbtmc_communicator import USBTMCCommunicator from .visa_communicator import VisaCommunicator from .vxi11_communicator import VXI11Communicator ================================================ FILE: src/instruments/abstract_instruments/comm/abstract_comm.py ================================================ #!/usr/bin/env python """ Provides an abstract base class for file-like communication layer classes """ # IMPORTS #################################################################### import abc import codecs import logging import struct # CLASSES #################################################################### class AbstractCommunicator(metaclass=abc.ABCMeta): """ Abstract base class for electrometer instruments. All applicable concrete instruments should inherit from this ABC to provide a consistent interface to the user. """ # INITIALIZER # def __init__(self, *args, **kwargs): # pylint: disable=unused-argument self._debug = False # Create a new logger for the module containing the concrete # subclass that we're a part of. self._logger = logging.getLogger(type(self).__module__) # Ensure that there's at least something setup to receive logs. self._logger.addHandler(logging.NullHandler()) # FORMATTING METHODS # def __repr__(self): try: addr = repr(self.address) except: # noqa: E722 addr = "unknown" return f"<{type(self).__name__} object at 0x{id(self):X} connected to {addr}>" # CONCRETE PROPERTIES # @property def debug(self): """ Enables or disables debug support. If active, all messages sent to or received from this communicator are logged to the Python logging service, with the logger name given by the module of the current communicator. Generating log messages for each exchanged command is slow, so these log messages are suppressed by default. Note that you must turn on logging to at least the DEBUG level in order to see these messages. For instance: >>> import logging >>> logging.basicConfig(level=logging.DEBUG) """ return self._debug @debug.setter def debug(self, newval): self._debug = bool(newval) # ABSTRACT PROPERTIES # @property @abc.abstractmethod def address(self): """ Reads or changes the current address for this communicator. """ raise NotImplementedError @address.setter @abc.abstractmethod def address(self, newval): raise NotImplementedError @property @abc.abstractmethod def terminator(self): """ Reads or changes the EOS termination. """ raise NotImplementedError @terminator.setter @abc.abstractmethod def terminator(self, newval): raise NotImplementedError @property @abc.abstractmethod def timeout(self): """ Get the connection interface timeout. """ raise NotImplementedError @timeout.setter @abc.abstractmethod def timeout(self, newval): raise NotImplementedError # ABSTRACT METHODS # @abc.abstractmethod def read_raw(self, size=-1): """ Read bytes in from the connection. :param int size: The number of bytes to read in from the connection. :return: The read bytes :rtype: `bytes` """ @abc.abstractmethod def write_raw(self, msg): """ Write bytes to the connection. :param bytes msg: Bytes to be sent to the instrument over the connection. """ @abc.abstractmethod def _sendcmd(self, msg): """ Sends a message to the connected device, handling all proper termination characters and secondary commands as required. Note that this is called by :class:`AbstractCommunicator.sendcmd`, which also handles debug, event and capture support. """ @abc.abstractmethod def _query(self, msg, size=-1): """ Send a string to the connected instrument using sendcmd and read the response. This is an abstract method because there are situations where information contained in the sent command is needed for reading logic. An example of this is the Galvant Industries GPIB adapter where if you are connected to an older instrument and the query command does not contain a `?`, then the command `+read` needs to be send to force the instrument to send its response. Note that this is called by :class:`AbstractCommunicator.query`, which also handles debug, event and capture support. """ @abc.abstractmethod def flush_input(self): """ Instruct the communicator to flush the input buffer, discarding the entirety of its contents. """ raise NotImplementedError # CONCRETE METHODS # def write(self, msg, encoding="utf-8"): """ Write a string to the connection. This string will be converted to `bytes` using the provided encoding method. .. seealso:: To send `bytes` in Python 3, see `write_raw`. :param str msg: String to be sent to the instrument over the connection. :param str encoding: Encoding to apply on msg to convert the message into bytes """ self.write_raw(msg.encode(encoding)) def read(self, size=-1, encoding="utf-8"): """ Read bytes in from the connection, returning a decoded string using the provided encoding method. .. seealso:: To read `bytes` in Python 3, see `read_raw`. :param int size: The number of bytes to read in from the connection. :param str encoding: Encoding that will be applied to the read bytes :return: The read string from the connection :rtype: `str` """ 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): """ Sends the incoming msg down to the wrapped file-like object but appends any other commands or termination characters required by the communication. This differs from the communicator .write method which directly exposes the communication channel without appending other data. """ if self.debug: self._logger.debug(" <- %s", repr(msg)) self._sendcmd(msg) def query(self, msg, size=-1): """ Send a string to the connected instrument using sendcmd and read the response. This is an abstract method because there are situations where information contained in the sent command is needed for reading logic. An example of this is the Galvant Industries GPIB adapter where if you are connected to an older instrument and the query command does not contain a `?`, then the command `+read` needs to be send to force the instrument to send its response. """ if self.debug: self._logger.debug(" <- %s", repr(msg)) resp = self._query(msg, size) if self.debug: self._logger.debug(" -> %s", repr(resp)) return resp ================================================ FILE: src/instruments/abstract_instruments/comm/file_communicator.py ================================================ #!/usr/bin/env python """ Provides a communication layer for an instrument with a file on the filesystem """ # IMPORTS ##################################################################### import errno import io import time import logging from instruments.abstract_instruments.comm import AbstractCommunicator logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) # CLASSES ##################################################################### class FileCommunicator(io.IOBase, AbstractCommunicator): """ Wraps a `file` object, providing ``sendcmd`` and ``query`` methods, while passing everything else through. :param filelike: File or name of a file to be wrapped as a communicator. Any file-like object wrapped by this class **must** support both reading and writing. If using the `open` builtin function, the mode ``rb+`` is recommended, and has been tested to work with character devices under Linux. :type filelike: `str` or `file` """ def __init__(self, filelike): super().__init__(self) if isinstance(filelike, str): # pragma: no cover filelike = open(filelike, "rb+") self._filelike = filelike self._terminator = "\n" self._testing = False # PROPERTIES # @property def address(self): """ Gets the name of the filesystem file that this communicator has been opened against. :type: `str` """ if hasattr(self._filelike, "name"): return self._filelike.name return None @address.setter def address(self, newval): raise NotImplementedError( "Changing addresses of a file communicator" " is not yet supported." ) @property def terminator(self): """ Gets/sets the end-of-line termination character. :type: `str` """ return self._terminator @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 socket communicator must be " "specified as a single character string." ) self._terminator = newval @property def timeout(self): """ Getting and setting the timeout property for `FileCommunicator` is not supported. """ raise NotImplementedError @timeout.setter def timeout(self, newval): raise NotImplementedError # FILE-LIKE METHODS # def close(self): """ Close connection to the filesystem file. """ try: self._filelike.close() except OSError as e: # pragma: no cover logger.warning("Failed to close file, exception: %s", repr(e)) def read_raw(self, size=-1): """ Read bytes in from the file. :param int size: The number of bytes to be read in from the file :rtype: `bytes` """ if size >= 0: return self._filelike.read(size) elif size == -1: result = b"" c = b"" while c != self._terminator.encode("utf-8"): c = self._filelike.read(1) if c == b"": break if c != self._terminator.encode("utf-8"): result += c return result else: raise ValueError("Must read a positive value of characters.") def write_raw(self, msg): """ Write bytes to the file. :param bytes msg: Bytes to be written to file """ self._filelike.write(msg) def seek(self, offset): """ Seek to a specified offset in the file. Useful for when using a static file, but less so when communicating with a physical instrument via a unix socket. :param int offset: The offset to seek to """ self._filelike.seek(offset) def tell(self): """ Gets the file's current position. :rtype: `int` """ return self._filelike.tell() def flush_input(self): """ Flush the internal buffer to make sure everything has actually been written to the file. This can be equivalent to a no-op on some filelike objects. """ self._filelike.flush() # METHODS # def _sendcmd(self, msg): """ This is the implementation of ``sendcmd`` for communicating with files on a unix system. 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 """ msg += self._terminator self.write(msg) try: self.flush() except OSError as e: logger.warning("Exception %s occured during flush().", repr(e)) def _query(self, msg, size=-1): """ This is the implementation of ``query`` for communicating with files on a unix system. This function is in turn wrapped by the concrete method `AbstractCommunicator.query` to provide consistent logging functionality across all communication layers. :param str msg: 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(msg) if not self._testing: time.sleep(0.02) # Give the bus time to respond. resp = b"" try: # FIXME: this is slow, but we do it to avoid unreliable # filelike devices such as some usbtmc-class devices. while True: nextchar = self._filelike.read(1) if not nextchar: break resp += nextchar if nextchar.endswith(self._terminator.encode("utf-8")): resp = resp[: -len(self._terminator)] break 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. if not resp: raise elif ex.errno != errno.EPIPE: raise # Reraise the existing exception. else: # Give a more helpful and specific exception. 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") ================================================ FILE: src/instruments/abstract_instruments/comm/gpib_communicator.py ================================================ #!/usr/bin/env python """ Provides a communication layer for an instrument connected via a Galvant Industries or Prologix GPIB adapter. """ # IMPORTS ##################################################################### from enum import Enum import io import time from instruments.units import ureg as u from instruments.abstract_instruments.comm import AbstractCommunicator from instruments.util_fns import assume_units # CLASSES ##################################################################### class GPIBCommunicator(io.IOBase, AbstractCommunicator): """ Communicates with a SocketCommunicator or SerialCommunicator object for use with Galvant Industries or Prologix GPIBUSB or GPIBETHERNET adapters. It essentially wraps those physical communication layers with the extra overhead required by the GPIB adapters. """ # pylint: disable=too-many-instance-attributes 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" 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 * u.millisecond if self._model == GPIBCommunicator.Model.gi and self._version <= 4: self._eos = 10 else: self._eos = "\n" # ENUMS # class Model(Enum): """ Enum containing the supported GPIB controller models """ #: Galvant Industries gi = "gi" #: Prologix, LLC pl = "pl" # PROPERTIES # @property def address(self): """ Gets/sets the GPIB address and downstream address associated with the instrument. When setting, if specified as an integer, only changes the GPIB address. If specified as a list, the first element changes the GPIB address, while the second is passed downstream. Example: [gpib_address, downstream_address] Where downstream_address needs to be formatted as appropriate for the connection (eg SerialCommunicator, SocketCommunicator, etc). """ return self._gpib_address, self._file.address @address.setter def address(self, newval): if isinstance(newval, int): if (newval < 1) or (newval > 30): raise ValueError("GPIB address must be between 1 and 30.") self._gpib_address = newval elif isinstance(newval, list): self.address = newval[0] # Set GPIB address self._file.address = newval[1] # Send downstream address else: raise TypeError("Not a valid input type for Instrument address.") @property def timeout(self): """ Gets/sets the timeeout of both the GPIB bus and the connection channel between the PC and the GPIB adapter. :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, 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): """ Gets/sets the GPIB termination character. This can be set to ``\n``, ``\r``, ``\r\n``, or ``eoi``. .. seealso:: `eos` and `eoi` for direct manipulation of these parameters. :type: `str` """ if not self._eoi: return self._terminator return "eoi" @terminator.setter def terminator(self, newval): if isinstance(newval, bytes): newval = newval.decode("utf-8") if isinstance(newval, str): newval = newval.lower() 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: newval = ord(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".' ) elif (newval < 0) or (newval > 255): 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) else: if newval != "eoi": self.eos = newval self.eoi = False self._terminator = self.eos elif newval == "eoi": self.eos = None self._terminator = "eoi" self.eoi = True @property def eoi(self): """ Gets/sets the EOI usage status. EOI is a dedicated line on the GPIB bus. When used, it is used by instruments to signal that the current byte being transmitted is the last in the message. This avoids the need to use a dedicated termination character such as ``\n``. Frequently, instruments will use both EOI-signalling and append an end-of-string (EOS) character. Some will only use one or the other. .. seealso:: `terminator`, `eos` for more communication termination related properties. :type: `bool` """ return self._eoi @eoi.setter def eoi(self, newval): if not isinstance(newval, bool): raise TypeError("EOI status must be specified as a boolean") self._eoi = newval 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")) @property def eos(self): """ Gets/sets the end-of-string (EOS) character. Valid EOS settings are ``\n``, ``\r``, ``\r\n`` and `None`. .. seealso:: `terminator`, `eoi` for more communication termination related properties. :type: `str` or `None` """ return self._eos @eos.setter def eos(self, newval): if self._model == GPIBCommunicator.Model.gi and self._version <= 4: if isinstance(newval, (str, bytes)): newval = ord(newval) self._file.sendcmd(f"+eos:{newval}") self._eos = newval else: if isinstance(newval, int): newval = str(chr(newval)) if newval == "\r\n": self._eos = newval newval = 0 elif newval == "\r": self._eos = newval newval = 1 elif newval == "\n": self._eos = newval newval = 2 elif newval is None: self._eos = newval newval = 3 else: raise ValueError("EOS must be CRLF, CR, LF, or None") self._file.sendcmd(f"++eos {newval}") # FILE-LIKE METHODS # def close(self): """ Close connection to the underlying physical connection channel of the GPIB connection. This is typically a serial connection that is then closed. """ self._file.close() def read_raw(self, size=-1): """ Read bytes in from the gpibusb connection. :param int size: The number of bytes to read in from the connection. :return: The read bytes from the connection :rtype: `bytes` """ return self._file.read_raw(size) def read(self, size=-1, encoding="utf-8"): """ Read characters from wrapped class (ie SocketCommunicator or SerialCommunicator). If size = -1, characters will be read until termination character is found. GI GPIB adapters always terminate serial connections with a CR. Function will read until a CR is found. :param int size: Number of bytes to read :param str encoding: Encoding that will be applied to the read bytes :return: Data read from the GPIB adapter :rtype: `str` """ return self._file.read(size, encoding) def write_raw(self, msg): """ Write bytes to the gpibusb connection. :param bytes msg: Bytes to be sent to the instrument over the connection. """ self._file.write_raw(msg) def write(self, msg, encoding="utf-8"): """ Write data string to GPIB connected instrument. :param str msg: String to write to the instrument :param str encoding: Encoding to apply on msg to convert the message into bytes """ self._file.write(msg, encoding) def flush_input(self): """ Instruct the communicator to flush the input buffer, discarding the entirety of its contents. """ self._file.flush_input() # METHODS # def _sendcmd(self, msg): """ This is the implementation of ``sendcmd`` for communicating with 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 == "": return 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) self.timeout = self.timeout time.sleep(sleep_time) self.eos = self.eos time.sleep(sleep_time) self._file.sendcmd(msg) time.sleep(sleep_time) def _query(self, msg, size=-1): """ This is the implementation of ``query`` for communicating with the GPIB adapters. This function is in turn wrapped by the concrete method `AbstractCommunicator.query` to provide consistent logging functionality across all communication layers. 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 response. :return: The instrument response to the query :rtype: `str` """ self.sendcmd(msg) 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() ================================================ FILE: src/instruments/abstract_instruments/comm/loopback_communicator.py ================================================ #!/usr/bin/env python """ Provides a loopback communicator, used for creating unit tests or for opening test connections to explore the InstrumentKit API. """ # IMPORTS ##################################################################### import io import sys from instruments.abstract_instruments.comm import AbstractCommunicator # CLASSES ##################################################################### class LoopbackCommunicator(io.IOBase, AbstractCommunicator): """ Used to provide a loopback connection for an instrument class. The most common use cases for this communicator are writing unit tests, opening test connections to explore the API without having the physical instrument connected, and testing the behaviour of code under development. """ def __init__(self, stdin=None, stdout=None): super().__init__(self) self._terminator = "\n" self._stdout = stdout self._stdin = stdin # PROPERTIES # @property def address(self): """ Gets the name of ``stdin`` :return: `sys.stdin.name` """ return sys.stdin.name @address.setter def address(self, newval): raise NotImplementedError @property def terminator(self): """ Gets/sets the termination character for the loopback communicator. This should be specified as a single character string. :type: `str` :return: The termination character """ return self._terminator @terminator.setter 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." ) self._terminator = newval @property def timeout(self): """ Gets the timeout for the loopback communicator. This will always return 0. :type: `int` """ return 0 @timeout.setter def timeout(self, newval): pass # FILE-LIKE METHODS # def close(self): """ Close connection to stdin """ try: if self._stdin is not None: self._stdin.close() except OSError: pass def read_raw(self, size=-1): """ Gets desired response command from stdin. If ``stdin`` is `None`, then the user will be prompted to enter a mock response in the Python interpreter. :param int size: Number of characters to read. Default value of -1 will read until termination character is found. :rtype: `bytes` """ if self._stdin is not None: if size == -1 or size is None: result = b"" 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) else: raise ValueError("Must read a positive value of characters.") else: input_var = input("Desired Response: ").encode("utf-8") return input_var def write_raw(self, msg): """ Write raw bytes to the loopback communicator's stdout. If ``stdout`` is `None` then it will be simply printed to the Python interpreter console. :param bytes msg: The bytes to be written """ if self._stdout is not None: self._stdout.write(msg) else: print(f" <- {repr(msg)} ") def seek(self, offset): # pylint: disable=unused-argument,no-self-use """ Go to a specific offset for the input data source. Not implemented for loopback communicator. """ raise NotImplementedError def tell(self): # pylint: disable=no-self-use """ Get the current positional offset for the input data source. Not implemented for loopback communicator. """ raise NotImplementedError def flush_input(self): """ Flush the input buffer, discarding all remaining input contents. For the loopback communicator, this will do nothing and just `pass`. """ # METHODS # def _sendcmd(self, msg): """ This is the implementation of ``sendcmd`` for the loopback communicator. 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 """ if msg != "": msg = f"{msg}{self._terminator}" self.write(msg) def _query(self, msg, size=-1): """ This is the implementation of ``query`` for communicating with the loopback communicator. This function is in turn wrapped by the concrete method `AbstractCommunicator.query` to provide consistent logging functionality across all communication layers. :param str msg: 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(msg) resp = self.read(size) return resp ================================================ FILE: src/instruments/abstract_instruments/comm/serial_communicator.py ================================================ #!/usr/bin/env python """ Provides a serial communicator for connecting with instruments over serial connections. """ # IMPORTS ##################################################################### import io import serial from instruments.units import ureg as u from instruments.abstract_instruments.comm import AbstractCommunicator from instruments.util_fns import assume_units # CLASSES ##################################################################### class SerialCommunicator(io.IOBase, AbstractCommunicator): """ Wraps a `pyserial.Serial` object to add a few properties as well as handling of termination characters. """ def __init__(self, conn): 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.") # PROPERTIES # @property def address(self): """ Gets/sets the address port for the serial object. :type: `str` """ return self._conn.port @address.setter def address(self, newval): # TODO: Input checking on Serial port newval # TODO: Add port changing capability to serialmanager # self._conn.port = newval raise NotImplementedError @property def terminator(self): """ Gets/sets the termination character for the serial communication channel. This is apended to the end of commands when writing, and used to detect when transmission is done when receiving. :type: `str` """ return self._terminator @terminator.setter 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." ) self._terminator = newval @property def timeout(self): """ Gets/sets the communication timeout of the serial comm channel. :type: `~pint.Quantity` :units: As specified or assumed to be of units ``seconds`` """ return self._conn.timeout * u.second @timeout.setter def timeout(self, newval): newval = assume_units(newval, u.second).to(u.second).magnitude self._conn.timeout = newval @property def parity(self): """ Gets / sets the communication parity. :type: `str` """ return self._conn.parity @parity.setter def parity(self, newval): self._conn.parity = newval # FILE-LIKE METHODS # def close(self): """ Shutdown and close the `pyserial.Serial` connection. """ try: self._conn.shutdown() finally: self._conn.close() def read_raw(self, size=-1): """ Read bytes in from the serial port. :param int size: The number of bytes to be read in from the serial port :rtype: `bytes` """ if size >= 0: resp = self._conn.read(size) return resp elif size == -1: result = b"" # 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"" and term is not None: raise OSError( "Serial connection timed out before reading " "a termination character." ) result += c return result[: -len(term)] if term is not None else result else: raise ValueError("Must read a positive value of characters.") def write_raw(self, msg): """ Write bytes to the `pyserial.Serial` object. :param bytes msg: Bytes to be written to the serial port """ self._conn.write(msg) def seek(self, offset): # pylint: disable=unused-argument,no-self-use """ Go to a specific offset for the input data source. Not implemented for serial communicator. """ raise NotImplementedError def tell(self): # pylint: disable=no-self-use """ Get the current positional offset for the input data source. Not implemented for serial communicator. """ raise NotImplementedError def flush_input(self): """ Instruct the communicator to flush the input buffer, discarding the entirety of its contents. Calls the pyserial flushInput() method. """ self._conn.flushInput() # METHODS # def _sendcmd(self, msg): """ This is the implementation of ``sendcmd`` for communicating with serial connections. 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 """ msg += self._terminator self.write(msg) def _query(self, msg, size=-1): """ This is the implementation of ``query`` for communicating with serial connections. This function is in turn wrapped by the concrete method `AbstractCommunicator.query` to provide consistent logging functionality across all communication layers. :param str msg: 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(msg) return self.read(size) ================================================ FILE: src/instruments/abstract_instruments/comm/serial_manager.py ================================================ #!/usr/bin/env python """ This module handles creating the serial objects for the instrument classes. This is needed for Windows because only 1 serial object can have an open connection to a serial port at a time. This is not needed on Linux, as multiple pyserial connections can be open at the same time to the same serial port. """ # IMPORTS ##################################################################### import weakref import serial from instruments.abstract_instruments.comm import SerialCommunicator # GLOBALS ##################################################################### # We want to only *weakly* hold references to serial ports, to allow for them # to be deleted and reopened as need be. # # A WeakValueDictionary *will* delete entries when their values # no longer exist. As a consequence, great care must be taken when iterating # over the dictionary in any way. # See http://docs.python.org/2/library/weakref.html#weakref.WeakValueDictionary # for more details about what "great care" implies. serialObjDict = weakref.WeakValueDictionary() # METHODS ##################################################################### def new_serial_connection(port, baud=460800, timeout=3, write_timeout=3, **kwargs): """ Return a `pyserial.Serial` connection object for the specified serial port address. The same object will be returned for identical port addresses. This is done for Windows which doesn't like when you have multiple things opening the same serial port. Typically this isn't a problem because you only have one instrument per serial port, but adapters such as the Galvant Industries GPIBUSB adapter can have multiple instruments on a single virtual serial port. :param str port: Port address for the serial port :param int baud: Baud rate for the serial port connection :param int timeout: Communication timeout for reading from the serial port connection. Units are seconds. :param write_timeout: Communication timeout for writing to the serial port connection. Units are seconds. :return: A :class:`SerialCommunicator` object wrapping the connection :rtype: `SerialCommunicator` """ if not isinstance(port, str): 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, **kwargs ) ) serialObjDict[port] = conn # pylint: disable=protected-access if not serialObjDict[port]._conn.isOpen(): serialObjDict[port]._conn.open() return serialObjDict[port] ================================================ FILE: src/instruments/abstract_instruments/comm/socket_communicator.py ================================================ #!/usr/bin/env python """ Provides a tcpip socket communicator for connecting with instruments over raw ethernet connections. """ # IMPORTS ##################################################################### import io import socket from instruments.units import ureg as u from instruments.abstract_instruments.comm import AbstractCommunicator from instruments.util_fns import assume_units # CLASSES ##################################################################### class SocketCommunicator(io.IOBase, AbstractCommunicator): """ Communicates with a socket and makes it look like a `file`. Note that this is used instead of `socket.makefile`, as that method does not support timeouts. We do not support all features of `file`-like objects here, but enough to make `~instrument.Instrument` happy. """ def __init__(self, conn): 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)) ) # PROPERTIES # @property def address(self): """ Returns the socket peer address information as a tuple. """ return self._conn.getpeername() @address.setter def address(self, newval): raise NotImplementedError("Unable to change address of sockets.") @property def terminator(self): return self._terminator @terminator.setter 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." ) self._terminator = newval @property def timeout(self): """ Gets/sets the connection timeout of the socket comm channel. :type: `~pint.Quantity` :units: As specified or assumed to be of units ``seconds`` """ return self._conn.gettimeout() * u.second @timeout.setter def timeout(self, newval): newval = assume_units(newval, u.second).to(u.second).magnitude self._conn.settimeout(newval) # FILE-LIKE METHODS # def close(self): """ Shutdown and close the `socket.socket` connection. """ try: self._conn.shutdown(socket.SHUT_RDWR) finally: self._conn.close() def read_raw(self, size=-1): """ Read bytes in from the socket connection. :param int size: The number of bytes to read in from the socket connection. :return: The read bytes :rtype: `bytes` """ if size >= 0: return self._conn.recv(size) elif size == -1: result = b"" while result.endswith(self._terminator.encode("utf-8")) is False: c = self._conn.recv(1) if c == b"": raise OSError( "Socket connection timed out before reading " "a termination character." ) result += c return result[: -len(self._terminator)] else: raise ValueError("Must read a positive value of characters.") def write_raw(self, msg): """ Write bytes to the `socket.socket` connection object. :param bytes msg: Bytes to be sent to the instrument over the socket connection. """ self._conn.sendall(msg) def seek(self, offset): # pylint: disable=unused-argument,no-self-use """ Go to a specific offset for the input data source. Not implemented for socket communicator. """ raise NotImplementedError def tell(self): # pylint: disable=no-self-use """ Get the current positional offset for the input data source. Not implemented for socket communicator. """ raise NotImplementedError def flush_input(self): """ Instruct the communicator to flush the input buffer, discarding the entirety of its contents. """ _ = self.read(-1) # Read in everything in the buffer and trash it # METHODS # def _sendcmd(self, msg): """ This is the implementation of ``sendcmd`` for communicating with socket connections. 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 """ msg += self._terminator self.write(msg) def _query(self, msg, size=-1): """ This is the implementation of ``query`` for communicating with socket connections. This function is in turn wrapped by the concrete method `AbstractCommunicator.query` to provide consistent logging functionality across all communication layers. :param str msg: 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(msg) resp = self.read(size) return resp ================================================ FILE: src/instruments/abstract_instruments/comm/usb_communicator.py ================================================ #!/usr/bin/env python """ Provides a USB communicator for connecting with instruments over raw usb connections. """ # IMPORTS ##################################################################### import io 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 ##################################################################### class USBCommunicator(io.IOBase, AbstractCommunicator): """ This communicator is used to wrap a pyusb connection object. This is typically *not* the suggested way to interact with a USB-connected instrument. Most USB instruments can be interfaced through other communicators such as `FileCommunicator` (usbtmc on Linux), `VisaCommunicator`, or `USBTMCCommunicator`. .. warning:: The operational status of this communicator is poorly tested. """ 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 # @property def address(self): raise NotImplementedError @address.setter def address(self, _): raise ValueError("Unable to change USB target address.") @property def terminator(self): """ Gets/sets the termination character used for communicating with raw USB objects. :return: """ return self._terminator @terminator.setter def terminator(self, newval): if not isinstance(newval, str): raise TypeError( "Terminator for USBCommunicator must be specified " "as a character string." ) self._terminator = newval @property def timeout(self): """ 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): newval = assume_units(newval, u.second).to(u.ms).magnitude self._dev.default_timeout = newval # FILE-LIKE METHODS # def close(self): """ Shutdown and close the USB connection """ self._dev.reset() usb.util.dispose_resources(self._dev) def read_raw(self, size=-1): """Read raw string back from device and return. String returned is most likely shorter than the size requested. Will terminate by itself. Read size of -1 will be transformed into 1000 bytes. :param size: Size to read in bytes :type size: int """ 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._ep_out.write(msg) def seek(self, offset): # pylint: disable=unused-argument,no-self-use raise NotImplementedError def tell(self): # pylint: disable=no-self-use raise NotImplementedError def flush_input(self): """ Instruct the communicator to flush the input buffer, discarding the entirety of its contents. """ self._ep_in.read(self._max_packet_size) # METHODS # def _sendcmd(self, msg): """ This is the implementation of ``sendcmd`` for communicating with raw usb connections. 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 """ msg += self._terminator self.write(msg) def _query(self, msg, size=-1): """ This is the implementation of ``query`` for communicating with raw usb connections. This function is in turn wrapped by the concrete method `AbstractCommunicator.query` to provide consistent logging functionality across all communication layers. :param str msg: 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(msg) return self.read(size) ================================================ FILE: src/instruments/abstract_instruments/comm/usbtmc_communicator.py ================================================ #!/usr/bin/env python """ Provides a communicator that uses Python-USBTMC for connecting with TMC instruments. """ # IMPORTS ##################################################################### import io import usbtmc from instruments.abstract_instruments.comm import AbstractCommunicator from instruments.util_fns import assume_units from instruments.units import ureg as u # CLASSES ##################################################################### class USBTMCCommunicator(io.IOBase, AbstractCommunicator): """ Wraps a USBTMC device. Arguments are passed to `usbtmc.Instrument`. """ def __init__(self, *args, **kwargs): if usbtmc is None: raise ImportError("usbtmc is required for TMC instruments.") super().__init__(self) self._filelike = usbtmc.Instrument(*args, **kwargs) self._terminator = "\n" # PROPERTIES # @property def address(self): if hasattr(self._filelike, "name"): return id(self._filelike) # TODO: replace with something more useful. return None @property def terminator(self): """ Gets/sets the termination character used for communicating with the USBTMC instrument. :type: `str` """ 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." ) self._terminator = newval self._filelike.term_char = ord(newval) @property def timeout(self): """ Gets/sets the communication timeout of the usbtmc comm channel. :type: `~pint.Quantity` :units: As specified or assumed to be of units ``seconds`` """ return self._filelike.timeout * u.second @timeout.setter def timeout(self, newval): newval = assume_units(newval, u.second).to(u.s).magnitude self._filelike.timeout = newval # FILE-LIKE METHODS # def close(self): """ Close the USBTMC connection object """ try: self._filelike.close() except OSError: pass def read(self, size=-1, encoding="utf-8"): """ Read bytes in from the usbtmc connection, returning a decoded string using the provided encoding method. :param int size: The number of bytes to read in from the usbtmc connection. :param str encoding: Encoding that will be applied to the read bytes :return: The read string from the connection :rtype: `str` """ return self._filelike.read(num=size, encoding=encoding) def read_raw(self, size=-1): """ Read bytes in from the usbtmc connection. :param int size: The number of bytes to read in from the usbtmc connection. :return: The read bytes :rtype: `bytes` """ return self._filelike.read_raw(num=size) def write(self, msg, encoding="utf-8"): """ Write a string to the usbtmc connection. This string will be converted to `bytes` using the provided encoding method. :param str msg: String to be sent to the instrument over the usbtmc connection. :param str encoding: Encoding to apply on msg to convert the message into bytes """ self._filelike.write(msg, encoding=encoding) def write_raw(self, msg): """ Write bytes to the usbtmc connection. :param bytes msg: Bytes to be sent to the instrument over the usbtmc connection. """ self._filelike.write_raw(msg) def seek(self, offset): raise NotImplementedError def tell(self): raise NotImplementedError def flush_input(self): """ For a USBTMC connection, this function does not actually do anything and simply returns. """ # METHODS # def _sendcmd(self, msg): """ This is the implementation of ``sendcmd`` for communicating with usbtmc connections. 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 """ self.write(msg) def _query(self, msg, size=-1): """ This is the implementation of ``query`` for communicating with usbtmc connections. This function is in turn wrapped by the concrete method `AbstractCommunicator.query` to provide consistent logging functionality across all communication layers. :param str msg: 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` """ return self._filelike.ask(msg, num=size, encoding="utf-8") ================================================ FILE: src/instruments/abstract_instruments/comm/visa_communicator.py ================================================ #!/usr/bin/env python """ Provides a VISA communicator for connecting with instruments via the VISA library. """ # IMPORTS ##################################################################### import io import pyvisa from instruments.abstract_instruments.comm import AbstractCommunicator from instruments.util_fns import assume_units from instruments.units import ureg as u # CLASSES ##################################################################### class VisaCommunicator(io.IOBase, AbstractCommunicator): """ Communicates a connection exposed by the VISA library and exposes it as a file-like object. """ def __init__(self, conn): super().__init__(self) self._terminator = None version = int(pyvisa.__version__.replace(".", "").ljust(3, "0")) # pylint: disable=no-member if (version < 160 and isinstance(conn, pyvisa.Instrument)) or ( version >= 160 and isinstance(conn, pyvisa.Resource) ): self._conn = conn self.terminator = "\n" else: raise TypeError("VisaCommunicator must wrap a VISA Instrument.") # Make a bytearray for holding data read in from the device # so that we can buffer for two-argument read. self._buf = bytearray() # PROPERTIES # @property def address(self): """ Gets the address or "resource name" for the VISA connection :type: `str` """ return self._conn.resource_name @address.setter def address(self, newval): raise NotImplementedError( "Changing addresses of a VISA Instrument " "is not supported." ) @property def read_termination(self): """Get / Set the read termination that is defined in pyvisa.""" return self._conn.read_termination @read_termination.setter def read_termination(self, newval): if not isinstance(newval, str): raise TypeError( "Read terminator for VisaCommunicator must be specified as a string." ) self._conn.read_termination = newval @property def terminator(self): """ Gets/sets the termination character used for VISA connections :type: `str` """ return self._terminator @terminator.setter def terminator(self, newval): if not isinstance(newval, str): raise TypeError( "Terminator for VisaCommunicator must be specified as a string." ) self._terminator = newval self.read_termination = newval self.write_termination = newval @property def timeout(self): return self._conn.timeout * u.second @timeout.setter def timeout(self, newval): newval = assume_units(newval, u.second).to(u.second).magnitude self._conn.timeout = newval @property def write_termination(self): """Get / Set the write termination that is defined in pyvisa.""" return self._conn.write_termination @write_termination.setter def write_termination(self, newval): if not isinstance(newval, str): raise TypeError( "Write terminator for VisaCommunicator must be specified as a string." ) self._conn.write_termination = newval # FILE-LIKE METHODS # def close(self): """ Close the pyVISA connection object """ try: self._conn.close() except OSError: pass def read_raw(self, size=-1): """ Read bytes in from the pyVISA connection. :param int size: The number of bytes to read in from the VISA connection. :return: The read bytes from the VISA connection :rtype: `bytes` """ if size >= 0: 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_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." ) return msg def write_raw(self, msg): """ Write bytes to the VISA connection. :param bytes msg: Bytes to be sent to the instrument over the VISA connection. """ self._conn.write_raw(msg) def seek(self, offset): # pylint: disable=unused-argument,no-self-use raise NotImplementedError def tell(self): # pylint: disable=no-self-use raise NotImplementedError def flush_input(self): """ Instruct the communicator to flush the input buffer, discarding the entirety of its contents. """ # TODO: Find out how to flush with pyvisa # METHODS # def _sendcmd(self, msg): """ This is the implementation of ``sendcmd`` for communicating with VISA connections. This function is in turn wrapped by the concrete method `AbstractCommunicator.sendcmd` to provide consistent logging functionality across all communication layers. Termination characters are automatically added by pyvisa. :param str msg: The command message to send to the instrument """ self.write(msg) def _query(self, msg, size=-1): """ This is the implementation of ``query`` for communicating with VISA connections. This function is in turn wrapped by the concrete method `AbstractCommunicator.query` to provide consistent logging functionality across all communication layers. Termination characters are automatically added by pyvisa. :param str msg: 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` """ return self._conn.query(msg) ================================================ FILE: src/instruments/abstract_instruments/comm/vxi11_communicator.py ================================================ #!/usr/bin/env python """ Provides a communication layer that uses python-vxi11 to interface with VXI11 devices. """ # IMPORTS ##################################################################### import io import logging import vxi11 from instruments.abstract_instruments.comm import AbstractCommunicator logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) # CLASSES ##################################################################### class VXI11Communicator(io.IOBase, AbstractCommunicator): """ Wraps a VXI-11 device. Arguments are all essentially just passed to `vxi11.Instrument`. VXI-11 is an RPC-based communication protocol over ethernet primarily used for connecting test and measurement equipment to controller hardware. VXI-11 allows for improved communication speeds and reduced latency over that of communicating using TCP over a socket connection. VXI-11 is developed and maintained by the IVI Foundation. More information can be found on their website, as well as that of the LXI standard website. VXI-11 has since been superseded by HiSLIP, which features fixes, improved performance, and new features such as IPv6 support. """ def __init__(self, *args, **kwargs): super().__init__(self) if vxi11 is None: raise ImportError( "Package python-vxi11 is required for XVI11 " "connected instruments." ) AbstractCommunicator.__init__(self) self._inst = vxi11.Instrument(*args, **kwargs) # PROPERTIES # @property def address(self): """ Gets the host and name for the vxi11 connection. Returns the ``host`` and ``name`` in a list. :rtype: `list`[`str`] """ return [self._inst.host, self._inst.name] @property def terminator(self): """ Gets/sets the termination character for the VXI11 communication channel. This is apended to the end of commands when writing, and used to detect when transmission is done when receiving. :type: `str` """ return self._inst.term_char @terminator.setter 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." ) if len(newval) > 1: logger.warning( "VXI11 instruments only support termination" "characters of length 1. The first character" "specified will be used." ) self._inst.term_char = newval @property def timeout(self): """ Gets/sets the communication timeout of the vxi11 comm channel. :type: `~pint.Quantity` :units: As specified or assumed to be of units ``seconds`` """ return self._inst.timeout @timeout.setter def timeout(self, newval): self._inst.timeout = newval # In seconds # FILE-LIKE METHODS # def close(self): """ Shutdown and close the vxi11 connection. """ try: self._inst.close() except OSError: pass def read_raw(self, size=-1): """ Read bytes in from the vxi11 connection. :param int size: The number of bytes to be read in from the vxi11 connection :rtype: `bytes` """ return self._inst.read_raw(num=size) def write_raw(self, msg): """ Write bytes to the vxi11 connection. :param bytes msg: Bytes to be written to the vxi11 connection """ self._inst.write_raw(msg) def seek(self, offset): """ Go to a specific offset for the input data source. Not implemented for vxi11 communicator. """ raise NotImplementedError def tell(self): """ Get the current positional offset for the input data source. Not implemented for vxi11 communicator. """ raise NotImplementedError def flush_input(self): """ Instruct the communicator to flush the input buffer, discarding the entirety of its contents. Not implemented for vxi11 communicator. """ raise NotImplementedError # METHODS # def _sendcmd(self, msg): """ This is the implementation of ``sendcmd`` for communicating with vxi11 connections. 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 """ self.write(msg) def _query(self, msg, size=-1): """ This is the implementation of ``query`` for communicating with vxi11 connections. This function is in turn wrapped by the concrete method `AbstractCommunicator.query` to provide consistent logging functionality across all communication layers. :param str msg: 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` """ return self._inst.ask(msg, num=size) ================================================ FILE: src/instruments/abstract_instruments/electrometer.py ================================================ #!/usr/bin/env python """ Provides an abstract base class for electrometer instruments """ # IMPORTS ##################################################################### import abc from instruments.abstract_instruments import Instrument # CLASSES ##################################################################### class Electrometer(Instrument, metaclass=abc.ABCMeta): """ Abstract base class for electrometer 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 measurement mode for the electrometer. This is an abstract method. :type: `~enum.Enum` """ @mode.setter @abc.abstractmethod def mode(self, newval): pass @property @abc.abstractmethod def unit(self): """ Gets/sets the measurement mode for the electrometer. This is an abstract method. :type: `~pint.Unit` """ @property @abc.abstractmethod def trigger_mode(self): """ Gets/sets the trigger mode for the electrometer. This is an abstract method. :type: `~enum.Enum` """ @trigger_mode.setter @abc.abstractmethod def trigger_mode(self, newval): pass @property @abc.abstractmethod def input_range(self): """ Gets/sets the input range setting for the electrometer. This is an abstract method. :type: `~enum.Enum` """ @input_range.setter @abc.abstractmethod def input_range(self, newval): pass @property @abc.abstractmethod def zero_check(self): """ Gets/sets the zero check status for the electrometer. This is an abstract method. :type: `bool` """ @zero_check.setter @abc.abstractmethod def zero_check(self, newval): pass @property @abc.abstractmethod def zero_correct(self): """ Gets/sets the zero correct status for the electrometer. This is an abstract method. :type: `bool` """ @zero_correct.setter @abc.abstractmethod def zero_correct(self, newval): pass # METHODS # @abc.abstractmethod def fetch(self): """ Request the latest post-processed readings using the current mode. (So does not issue a trigger) """ raise NotImplementedError @abc.abstractmethod def read_measurements(self): """ Trigger and acquire readings using the current mode. """ raise NotImplementedError ================================================ FILE: src/instruments/abstract_instruments/function_generator.py ================================================ #!/usr/bin/env python """ Provides an abstract base class for function generator instruments """ # IMPORTS ##################################################################### import abc from enum import Enum from pint.errors import DimensionalityError from instruments.abstract_instruments import Instrument from instruments.units import ureg as u from instruments.util_fns import assume_units, ProxyList # CLASSES ##################################################################### class FunctionGenerator(Instrument, metaclass=abc.ABCMeta): """ Abstract base class for function generator instruments. All applicable concrete instruments should inherit from this ABC to 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 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" 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" @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 accessible 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 def _get_amplitude_(self): raise NotImplementedError() def _set_amplitude_(self, magnitude, units): raise NotImplementedError() @property def frequency(self): """ Gets/sets the output frequency of the function generator. This is an abstract property. :type: `~pint.Quantity` """ if self._channel_count > 1: return self.channel[0].frequency else: raise NotImplementedError() @frequency.setter def frequency(self, newval): if self._channel_count > 1: self.channel[0].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._channel_count > 1: return self.channel[0].function else: raise NotImplementedError() @function.setter def function(self, newval): if self._channel_count > 1: self.channel[0].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._channel_count > 1: return self.channel[0].offset else: raise NotImplementedError() @offset.setter def offset(self, newval): if self._channel_count > 1: self.channel[0].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._channel_count > 1: return self.channel[0].phase else: raise NotImplementedError() @phase.setter def phase(self, newval): if self._channel_count > 1: self.channel[0].phase = newval else: raise NotImplementedError() ================================================ FILE: src/instruments/abstract_instruments/instrument.py ================================================ #!/usr/bin/env python """ Provides the base Instrument class for all instruments. """ # IMPORTS ##################################################################### import os import collections import socket import struct import typing_extensions import urllib.parse as parse from serial import SerialException from serial.tools.list_ports import comports import pyvisa import usb.core from instruments.abstract_instruments.comm import ( 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"}) # CLASSES ##################################################################### class Instrument: """ This is the base instrument class from which all others are derived from. It provides the basic implementation for all communication related tasks. In addition, it also contains several class methods for opening connections via the supported hardware channels. """ def __init__(self, filelike, *args, **kwargs): # Check to make sure filelike is a subclass of AbstractCommunicator if isinstance(filelike, AbstractCommunicator): self._file = filelike else: 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) self._prompt = None self._terminator = "\n" # COMMAND-HANDLING METHODS # def _ack_expected(self, msg=""): # pylint: disable=unused-argument,no-self-use return None def _authenticate(self, auth): """ Authenticate with credentials for establishing the communication with the instrument. :param auth: Credentials for authentication. """ raise NotImplementedError def sendcmd(self, cmd): """ Sends a command without waiting for a response. :param str cmd: String containing the command to be sent. """ self._file.sendcmd(str(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: if ack_expected is None: break ack = self.read() if ack != ack_expected: raise AcknowledgementError( "Incorrect ACK message received: got {} " "expected {}".format(ack, ack_expected) ) 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) ) def query(self, cmd, size=-1): """ Executes the given query. :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` """ 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] if ack_expected_list[0] is None: # Case no ACK value = self._file.query(cmd, size) else: # Case with ACKs _ = self._file.query(cmd, size=0) # Send the cmd, don't read for ack_expected in ack_expected_list: # Read and verify ACKs ack = self.read() if ack != ack_expected: raise AcknowledgementError( 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( f"Incorrect prompt message received: got {prompt} expected {self.prompt}" ) return value def read(self, size=-1, encoding="utf-8"): """ Read the 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(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 # @property def timeout(self): """ Gets/sets the communication timeout for this instrument. Note that setting this value after opening the connection is not supported for all connection types. :type: `int` """ return self._file.timeout @timeout.setter def timeout(self, newval): self._file.timeout = newval @property def address(self): """ Gets/sets the target communication of the instrument. This is useful for situations when running straight from a Python shell and your instrument has enumerated with a different address. An example when this can happen is if you are using a USB to Serial adapter and you disconnect/reconnect it. :type: `int` for GPIB address, `str` for other """ return self._file.address @address.setter def address(self, newval): self._file.address = newval @property def terminator(self): """ Gets/sets the terminator used for communication. For communication options where this is applicable, the value corresponds to the ASCII character used for termination in decimal format. Example: 10 sets the character to NEWLINE. :type: `int`, or `str` for GPIB adapters. """ return self._file.terminator @terminator.setter def terminator(self, newval): self._file.terminator = newval @property def prompt(self): """ Gets/sets the prompt used for communication. The prompt refers to a character that is sent back from the instrument after it has finished processing your last command. Typically this is used to indicate to an end-user that the device is ready for input when connected to a serial-terminal interface. In IK, the prompt is specified that that it (and its associated termination character) are read in. The value read in from the device is also checked against the stored prompt value to make sure that everything is still in sync. :type: `str` """ return self._prompt @prompt.setter def prompt(self, newval): self._prompt = newval # BASIC I/O METHODS # def write(self, msg): """ Write data string to the connected instrument. This will call the write method for the attached filelike object. This will typically bypass attaching any termination characters or other communication channel related work. .. seealso:: `Instrument.sendcmd` if you wish to send a string to the instrument, while still having InstrumentKit handle termination characters and other communication channel related work. :param str msg: String that will be written to the filelike object (`Instrument._file`) attached to this instrument. """ 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. The format is as follows: #{number of following digits:1-F}{num of bytes to be read}{data bytes} :param int data_width: Specify the number of bytes wide each data point is. One of [1,2,4]. :param str fmt: Format string as specified by the :mod:`struct` module, or `None` to choose a format automatically based on the data width. Typically you can just specify `data_width` and leave this default. """ # 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 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), 16) # Read in the num of bytes to be read num_of_bytes = int(self._file.read_raw(digits)) # Make or use the required format string. if fmt is None: fmt = _DEFAULT_FORMATS[data_width] # Read in the data bytes, and pass them to numpy using the specified # data type (format). # This is looped in case a communication timeout occurs midway # through transfer and multiple reads are required tries = 3 data = self._file.read_raw(num_of_bytes) while len(data) < num_of_bytes: old_len = len(data) data += self._file.read_raw(num_of_bytes - old_len) if old_len == len(data): tries -= 1 if tries == 0: 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", "test", ] @classmethod def open_from_uri(cls, uri): # pylint: disable=too-many-return-statements,too-many-branches """ Given an instrument URI, opens the instrument named by that URI. Instrument URIs are formatted with a scheme, such as ``serial://``, followed by a location that is interpreted differently for each scheme. The following examples URIs demonstrate the currently supported schemes and location formats:: serial://COM3 serial:///dev/ttyACM0 tcpip://192.168.0.10:4100 gpib+usb://COM3/15 gpib+serial://COM3/15 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 ``serial://COM9?baud=115200``. If not specified, the baud rate is assumed to be 115200. :param str uri: URI for the instrument to be loaded. :rtype: `Instrument` .. seealso:: `PySerial`_ documentation for serial port URI format .. _PySerial: http://pyserial.sourceforge.net/ """ # Make sure that urlparse knows that we want query strings. for scheme in cls.URI_SCHEMES: if scheme not in parse.uses_query: parse.uses_query.append(scheme) # Break apart the URI using urlparse. This returns a named tuple whose # parts describe the incoming URI. parsed_uri = parse.urlparse(uri) # We always want the query string to provide keyword args to the # class method. # FIXME: This currently won't work, as everything is strings, # but the other class methods expect ints or floats, depending. kwargs = parse.parse_qs(parsed_uri.query) if parsed_uri.scheme == "serial": # Ex: serial:///dev/ttyACM0 # We want to pass just the netloc and the path to PySerial, # sending the query string as kwargs. Thus, we should make the # device name here. dev_name = parsed_uri.netloc if parsed_uri.path: dev_name = os.path.join(dev_name, parsed_uri.path) # We should handle the baud rate separately, however, to ensure # that the default is set correctly and that the type is `int`, # as expected. if "baud" in kwargs: kwargs["baud"] = int(kwargs["baud"][0]) else: kwargs["baud"] = 115200 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": # 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) elif parsed_uri.scheme == "visa": # Ex: visa://USB::{VID}::{PID}::{SERIAL}::0::INSTR # where {VID}, {PID} and {SERIAL} are to be replaced with # the vendor ID, product ID and serial number of the USB-VISA # device. return cls.open_visa(parsed_uri.netloc, **kwargs) elif parsed_uri.scheme == "usbtmc": # TODO: check for other kinds of usbtmc URLs. # 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 ) 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.") @classmethod def open_tcpip(cls, host, port, auth=None): """ Opens an instrument, connecting via TCP/IP to a given host and TCP port. :param str host: Name or IP address of the instrument. :param int port: TCP port on which the insturment is listening. :param auth: Authentication credentials to establish connection. Type depends on instrument and authentication method used. :rtype: `Instrument` :return: Object representing the connected instrument. .. seealso:: `~socket.socket.connect` for description of `host` and `port` parameters in the TCP/IP address family. """ conn = socket.socket() conn.connect((host, port)) if auth is None: ret_cls = cls(SocketCommunicator(conn)) else: ret_cls = cls(SocketCommunicator(conn), auth=auth) return ret_cls # 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, **kwargs, ): """ Opens an instrument, connecting via a physical or emulated serial port. Note that many instruments which connect via USB are exposed to the operating system as serial ports, so this method will very commonly be used for connecting instruments via USB. This method can be called by either supplying a port as a string, or by specifying vendor and product IDs, and an optional serial number (used when more than one device with the same IDs is attached). If both the port and IDs are supplied, the port will default to the supplied port string, else it will search the available com ports for a port matching the defined IDs and serial number. :param str port: Name of the the port or device file to open a connection on. For example, ``"COM10"`` on Windows or ``"/dev/ttyUSB0"`` on Linux. :param int baud: The baud rate at which instrument communicates. :param int vid: the USB port vendor id. :param int pid: the USB port product id. :param str serial_number: The USB port serial_number. :param float timeout: Number of seconds to wait when reading from the instrument before timing out. :param float write_timeout: Number of seconds to wait when writing to the instrument before timing out. :param kwargs: Additional keyword arguments that will be passed on to serial, e.g., `parity`. :rtype: `Instrument` :return: Object representing the connected instrument. .. seealso:: `~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 " ) if port is not None and vid is not None: 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." ) if port is None: match_count = 0 for _port in comports(): # If no match on vid/pid, go to next comport if not _port.pid == pid or not _port.vid == vid: continue # If we specified a serial num, verify then break if serial_number is not None and _port.serial_number == serial_number: port = _port.device break # If no provided serial number, match, but also keep a count if serial_number is None: port = _port.device match_count += 1 # 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" ) # 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}" ) raise ValueError( err_msg.format( vid=vid, pid=pid, 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, **kwargs ) return cls(ser) @classmethod 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`_. :param str port: Name of the the port or device file to open a connection on. Note that because the GI GPIB-USB adapter identifies as a serial port to the operating system, this should be the name of a serial port. :param int gpib_address: Address on the connected GPIB bus assigned to the instrument. :param float timeout: Number of seconds to wait when reading from the 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. .. seealso:: `~serial.Serial` for description of `port` and timeouts. .. _Galvant Industries GPIB-USB adapter: galvant.ca/#!/store/gpibusb """ ser = serial_manager.new_serial_connection( port, baud=460800, timeout=timeout, write_timeout=write_timeout ) return cls(GPIBCommunicator(ser, gpib_address, model)) @classmethod def open_gpibethernet(cls, host, port, gpib_address, model="pl"): """ 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, model)) @classmethod def open_visa(cls, resource_name): """ Opens an instrument, connecting using the VISA library. Note that `PyVISA`_ and a VISA implementation must both be present and installed for this method to function. :param str resource_name: Name of a VISA resource representing the given instrument. :rtype: `Instrument` :return: Object representing the connected instrument. .. seealso:: `National Instruments help page on VISA resource names `_. .. _PyVISA: http://pyvisa.sourceforge.net/ """ version = list(map(int, pyvisa.__version__.split("."))) while len(version) < 3: version += [0] if version[0] >= 1 and version[1] >= 6: ins = pyvisa.ResourceManager().open_resource(resource_name) else: ins = pyvisa.instrument(resource_name) # pylint: disable=no-member return cls(VisaCommunicator(ins)) @classmethod def open_test(cls, stdin=None, stdout=None): """ Opens an instrument using a loopback communicator for a test connection. The primary use case of this is to instantiate a specific instrument class without requiring an actual physical connection of any kind. This is also very useful for creating unit tests through the parameters of this class method. :param stdin: The stream of data coming from the instrument :type stdin: `io.BytesIO` or `None` :param stdout: Empty data stream that will hold data sent from the Python class to the loopback communicator. This can then be checked for the contents. :type stdout: `io.BytesIO` or `None` :return: Object representing the virtually-connected instrument """ return cls(LoopbackCommunicator(stdin, stdout)) @classmethod def open_usbtmc(cls, *args, **kwargs): """ Opens an instrument, connecting to a USB-TMC device using the Python `usbtmc` library. .. warning:: The operational status of this is unknown. It is suggested that you connect via the other provided class methods. For Linux, if you have the ``usbtmc`` kernel module, the `~instruments.Instrument.open_file` class method will work. On Windows, using the `~instruments.Instrument.open_visa` class method along with having the VISA libraries installed will work. :return: Object representing the connected instrument """ usbtmc_comm = USBTMCCommunicator(*args, **kwargs) return cls(usbtmc_comm) @classmethod def open_vxi11(cls, *args, **kwargs): """ Opens a vxi11 enabled instrument, connecting using the python library `python-vxi11`_. This package must be present and installed for this method to function. :rtype: `Instrument` :return: Object representing the connected instrument. .. _python-vxi11: https://github.com/python-ivi/python-vxi11 """ vxi11_comm = VXI11Communicator(*args, **kwargs) return cls(vxi11_comm) @classmethod def open_usb(cls, vid, pid): """ Opens an instrument, connecting via a raw USB stream. .. note:: Note that raw USB a very uncommon of connecting to instruments, even for those that are connected by USB. Most will identify as either serial ports (in which case, `~instruments.Instrument.open_serial` should be used), or as USB-TMC devices. On Linux, USB-TMC devices can be connected using `~instruments.Instrument.open_file`, provided that the ``usbtmc`` kernel module is loaded. On Windows, some such devices can be opened using the VISA library and the `~instruments.Instrument.open_visa` method. :param str vid: Vendor 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. """ dev = usb.core.find(idVendor=vid, idProduct=pid) if dev is None: raise OSError("No such device found.") return cls(USBCommunicator(dev)) @classmethod def open_file(cls, filename): """ Given a file, treats that file as a character device file that can be read from and written to in order to communicate with the instrument. This may be the case, for instance, if the instrument is connected by the Linux ``usbtmc`` kernel driver. :param str filename: Name of the character device to open. :rtype: `Instrument` :return: Object representing the connected instrument. """ return cls(FileCommunicator(filename)) def __enter__(self) -> typing_extensions.Self: return self def __exit__(self, *exc): self._file.__exit__(*exc) ================================================ FILE: src/instruments/abstract_instruments/multimeter.py ================================================ #!/usr/bin/env python """ Provides an abstract base class for multimeter instruments """ # IMPORTS ##################################################################### import abc from instruments.abstract_instruments import Instrument # CLASSES ##################################################################### class Multimeter(Instrument, metaclass=abc.ABCMeta): """ Abstract base class for multimeter 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 measurement mode for the multimeter. This is an abstract method. :type: `~enum.Enum` """ @mode.setter @abc.abstractmethod def mode(self, newval): pass @property @abc.abstractmethod def trigger_mode(self): """ Gets/sets the trigger mode for the multimeter. This is an abstract method. :type: `~enum.Enum` """ @trigger_mode.setter @abc.abstractmethod def trigger_mode(self, newval): pass @property @abc.abstractmethod def relative(self): """ Gets/sets the status of relative measuring mode for the multimeter. This is an abstract method. :type: `bool` """ @relative.setter @abc.abstractmethod def relative(self, newval): pass @property @abc.abstractmethod def input_range(self): """ Gets/sets the current input range setting of the multimeter. This is an abstract method. :type: `~pint.Quantity` or `~enum.Enum` """ @input_range.setter @abc.abstractmethod def input_range(self, newval): pass # METHODS ## @abc.abstractmethod def measure(self, mode): """ Perform a measurement as specified by mode parameter. """ ================================================ FILE: src/instruments/abstract_instruments/optical_spectrum_analyzer.py ================================================ #!/usr/bin/env python """ Provides an abstract base class for optical spectrum analyzer instruments """ # IMPORTS ##################################################################### import abc from instruments.abstract_instruments import Instrument from instruments.util_fns import ProxyList # 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. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._channel_count = 1 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. Optical spectrum analyzers 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 # 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` """ if self._parent._channel_count == 1: return self._parent.wavelength(bin_format=bin_format) else: 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` """ if self._parent._channel_count == 1: return self._parent.data(bin_format=bin_format) else: raise NotImplementedError # PROPERTIES # @property 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. """ return ProxyList(self, self.Channel, range(self._channel_count)) @property 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 def start_wl(self, newval): raise NotImplementedError @property 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 def stop_wl(self, newval): raise NotImplementedError @property def bandwidth(self): """ Gets/sets the the bandwidth of the OSA. This is an abstract property. :type: `~pint.Quantity` """ raise NotImplementedError @bandwidth.setter def bandwidth(self, newval): raise NotImplementedError # METHODS # @abc.abstractmethod def start_sweep(self): """ Forces a start sweep on the attached OSA. """ raise NotImplementedError 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` """ if self._channel_count > 1: return self.channel[0].wavelength(bin_format=bin_format) else: raise NotImplementedError 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` """ if self._channel_count > 1: return self.channel[0].data(bin_format=bin_format) else: raise NotImplementedError ================================================ FILE: src/instruments/abstract_instruments/oscilloscope.py ================================================ #!/usr/bin/env python """ Provides an abstract base class for oscilloscope instruments """ # IMPORTS ##################################################################### import abc from instruments.abstract_instruments import Instrument # CLASSES ##################################################################### class Oscilloscope(Instrument, metaclass=abc.ABCMeta): """ Abstract base class for oscilloscope 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 oscilloscope. All applicable concrete instruments should inherit from this ABC to provide a consistent interface to the user. """ # PROPERTIES # @property @abc.abstractmethod def coupling(self): """ Gets/sets the coupling setting for the oscilloscope. This is an abstract method. :type: `~enum.Enum` """ raise NotImplementedError @coupling.setter @abc.abstractmethod def coupling(self, newval): raise NotImplementedError class DataSource(metaclass=abc.ABCMeta): """ Abstract base class for data sources (physical channels, math, ref) on an oscilloscope. 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 def __exit__(self, type, value, traceback): if self._old_dsrc is not None: self._parent.data_source = self._old_dsrc def __eq__(self, other): if not isinstance(other, type(self)): return NotImplemented return other.name == self.name __hash__ = None # PROPERTIES # @property @abc.abstractmethod def name(self): """ Gets the name of the channel. This is an abstract property. :type: `str` """ raise NotImplementedError # METHODS # @abc.abstractmethod def read_waveform(self, bin_format=True): """ Gets the waveform 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 waveform with both x and y components. :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 oscilloscope instrument. Typically generated by the `~instruments.util_fns.ProxyList` helper. """ raise NotImplementedError @property @abc.abstractmethod def ref(self): """ Gets an iterator or list for easy Pythonic access to the various ref data sources objects on the oscilloscope instrument. Typically generated by the `~instruments.util_fns.ProxyList` helper. """ raise NotImplementedError @property @abc.abstractmethod def math(self): """ Gets an iterator or list for easy Pythonic access to the various math data sources objects on the oscilloscope instrument. Typically generated by the `~instruments.util_fns.ProxyList` helper. """ raise NotImplementedError # METHODS # @abc.abstractmethod def force_trigger(self): """ Forces a trigger event to occur on the attached oscilloscope. """ raise NotImplementedError ================================================ FILE: src/instruments/abstract_instruments/power_supply.py ================================================ #!/usr/bin/env python """ Provides an abstract base class for power supply instruments """ # IMPORTS ##################################################################### import abc from instruments.abstract_instruments import Instrument # CLASSES ##################################################################### class PowerSupply(Instrument, metaclass=abc.ABCMeta): """ Abstract base class for power supply 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 power supply output channels. 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 # @property @abc.abstractmethod def channel(self): """ Gets a channel object for the power supply. This should use `~instruments.util_fns.ProxyList` to achieve this. This is an abstract method. :rtype: `PowerSupply.Channel` """ raise NotImplementedError @property @abc.abstractmethod def voltage(self): """ Gets/sets the output voltage for all channel on the power supply. 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 all channel on the power supply. This is an abstract method. :type: `~pint.Quantity` """ @current.setter @abc.abstractmethod def current(self, newval): pass ================================================ FILE: src/instruments/abstract_instruments/signal_generator/__init__.py ================================================ #!/usr/bin/env python """ Module containing signal generator abstract base classes """ from .signal_generator import SignalGenerator from .single_channel_sg import SingleChannelSG from .channel import SGChannel ================================================ FILE: src/instruments/abstract_instruments/signal_generator/channel.py ================================================ #!/usr/bin/env python """ Provides an abstract base class for signal generator output channels """ # IMPORTS ##################################################################### import abc # CLASSES ##################################################################### class SGChannel(metaclass=abc.ABCMeta): """ Python abstract base class representing a single channel for a signal generator. .. warning:: This class should NOT be manually created by the user. It is designed to be initialized by the `~instruments.SignalGenerator` class. """ # PROPERTIES # @property @abc.abstractmethod def frequency(self): """ Gets/sets the output frequency of the signal generator channel :type: `~pint.Quantity` """ @frequency.setter @abc.abstractmethod def frequency(self, newval): pass @property @abc.abstractmethod def power(self): """ Gets/sets the output power of the signal generator channel :type: `~pint.Quantity` """ @power.setter @abc.abstractmethod def power(self, newval): pass @property @abc.abstractmethod def phase(self): """ Gets/sets the output phase of the signal generator channel :type: `~pint.Quantity` """ @phase.setter @abc.abstractmethod def phase(self, newval): pass @property @abc.abstractmethod def output(self): """ Gets/sets the output status of the signal generator channel :type: `bool` """ @output.setter @abc.abstractmethod def output(self, newval): pass ================================================ FILE: src/instruments/abstract_instruments/signal_generator/signal_generator.py ================================================ #!/usr/bin/env python """ Provides an abstract base class for signal generator instruments """ # IMPORTS ##################################################################### import abc from instruments.abstract_instruments import Instrument # CLASSES ##################################################################### class SignalGenerator(Instrument, metaclass=abc.ABCMeta): """ Python abstract base class for signal generators (eg microwave sources). This ABC is not for function generators, which have their own separate ABC. .. seealso:: `~instruments.FunctionGenerator` """ # PROPERTIES # @property @abc.abstractmethod def channel(self): """ Gets a specific channel object for the SignalGenerator. :rtype: A class inherited from `~instruments.SGChannel` """ raise NotImplementedError ================================================ FILE: src/instruments/abstract_instruments/signal_generator/single_channel_sg.py ================================================ #!/usr/bin/env python """ Provides an abstract base class for signal generators with only a single output channel. """ # IMPORTS ##################################################################### from instruments.abstract_instruments.signal_generator import SignalGenerator from instruments.abstract_instruments.signal_generator.channel import SGChannel # CLASSES ##################################################################### # pylint: disable=abstract-method class SingleChannelSG(SignalGenerator, SGChannel): """ Class for representing a Signal Generator that only has a single output channel. The sole property in this class allows for the user to use the API for SGs with multiple channels and a more compact form since it only has one output. For example, both of the following calls would work the same: >>> print sg.channel[0].freq # Multi-channel style >>> print sg.freq # Compact style """ # PROPERTIES # @property def channel(self): return (self,) ================================================ FILE: src/instruments/agilent/__init__.py ================================================ #!/usr/bin/env python """ Module containing Agilent instruments """ from instruments.agilent.agilent33220a import Agilent33220a from instruments.agilent.agilent34410a import Agilent34410a ================================================ FILE: src/instruments/agilent/agilent33220a.py ================================================ #!/usr/bin/env python """ Provides support for the Agilent 33220a function generator. """ # IMPORTS ##################################################################### from enum import Enum 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, ) # CLASSES ##################################################################### class Agilent33220a(SCPIFunctionGenerator): """ The `Agilent/Keysight 33220a`_ is a 20MHz function/arbitrary waveform generator. This model has been replaced by the Keysight 33500 series waveform generators. This class may or may not work with these newer models. Example usage: >>> import instruments as ik >>> import instruments.units as u >>> inst = ik.agilent.Agilent33220a.open_gpibusb('/dev/ttyUSB0', 1) >>> inst.function = inst.Function.sinusoid >>> inst.frequency = 1 * u.kHz >>> inst.output = True .. _Agilent/Keysight 33220a: http://www.keysight.com/en/pd-127539-pn-33220A """ # ENUMS # class Function(Enum): """ Enum containing valid functions for the Agilent/Keysight 33220a """ sinusoid = "SIN" square = "SQU" ramp = "RAMP" pulse = "PULS" noise = "NOIS" dc = "DC" user = "USER" class LoadResistance(Enum): """ Enum containing valid load resistance for the Agilent/Keysight 33220a """ minimum = "MIN" maximum = "MAX" high_impedance = "INF" class OutputPolarity(Enum): """ Enum containg valid output polarity modes for the Agilent/Keysight 33220a """ normal = "NORM" inverted = "INV" # PROPERTIES # function = enum_property( command="FUNC", enum=Function, doc=""" Gets/sets the output function of the function generator :type: `Agilent33220a.Function` """, set_fmt="{}:{}", ) duty_cycle = int_property( command="FUNC:SQU:DCYC", doc=""" Gets/sets the duty cycle of a square wave. Duty cycle represents the amount of time that the square wave is at a high level. :type: `int` """, valid_set=range(101), ) ramp_symmetry = int_property( command="FUNC:RAMP:SYMM", doc=""" Gets/sets the ramp symmetry for ramp waves. Symmetry represents the amount of time per cycle that the ramp wave is rising (unless polarity is inverted). :type: `int` """, valid_set=range(101), ) output = bool_property( command="OUTP", inst_true="ON", inst_false="OFF", doc=""" Gets/sets the output enable status of the front panel output connector. The value `True` corresponds to the output being on, while `False` is the output being off. :type: `bool` """, ) output_sync = bool_property( 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( command="OUTP:POL", enum=OutputPolarity, doc=""" Gets/sets the polarity of the waveform relative to the offset voltage. :type: `~Agilent33220a.OutputPolarity` """, ) @property def load_resistance(self): """ Gets/sets the desired output termination load (ie, the impedance of the load attached to the front panel output connector). The instrument has a fixed series output impedance of 50ohms. This function allows the instrument to compensate of the voltage divider and accurately report the voltage across the attached load. :units: As specified (if a `~pint.Quantity`) or assumed to be of units :math:`\\Omega` (ohm). :type: `~pint.Quantity` or `Agilent33220a.LoadResistance` """ value = self.query("OUTP:LOAD?") try: return int(value) * u.ohm except ValueError: return self.LoadResistance(value.strip()) @load_resistance.setter def load_resistance(self, newval): if isinstance(newval, self.LoadResistance): newval = newval.value else: 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): raise NotImplementedError @phase.setter def phase(self, newval): raise NotImplementedError ================================================ FILE: src/instruments/agilent/agilent34410a.py ================================================ #!/usr/bin/env python """ Provides support for the Agilent 34410a digital multimeter. """ # IMPORTS ##################################################################### from instruments.generic_scpi import SCPIMultimeter from instruments.optional_dep_finder import numpy from instruments.units import ureg as u # CLASSES ##################################################################### class Agilent34410a(SCPIMultimeter): # pylint: disable=abstract-method """ The Agilent 34410a is a very popular 6.5 digit DMM. This class should also cover the Agilent 34401a, 34411a, as well as the backwards compatability mode in the newer Agilent/Keysight 34460a/34461a. You can find the full specifications for these instruments on the `Keysight website`_. Example usage: >>> import instruments as ik >>> 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/ """ # PROPERTIES # @property def data_point_count(self): """ Gets the total number of readings that are located in reading memory (RGD_STORE). :rtype: `int` """ return int(self.query("DATA:POIN?")) # STATE MANAGEMENT METHODS # def init(self): """ Switch device from "idle" state to "wait-for-trigger state". Measurements will begin when specified triggering conditions are met, following the receipt of the INIT command. Note that this command will also clear the previous set of readings from memory. """ self.sendcmd("INIT") def abort(self): """ Abort all measurements currently in progress. """ self.sendcmd("ABOR") # MEMORY MANAGEMENT METHODS # def clear_memory(self): """ Clears the non-volatile memory of the Agilent 34410a. """ self.sendcmd("DATA:DEL NVMEM") def r(self, count): """ Have the multimeter perform a specified number of measurements and then transfer them using a binary transfer method. Data will be cleared from instrument memory after transfer is complete. Data is transfered from the instrument in 64-bit double floating point precision format. :param int count: Number of samples to take. :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?" else: msg = "R? " + str(count) self.sendcmd("FORM:DATA REAL,64") self.sendcmd(msg) data = self.binblockread(8, fmt=">d") if numpy: return data * units return tuple(val * units for val in data) # DATA READING METHODS # def fetch(self): """ Transfer readings from instrument memory to the output buffer, and thus to the computer. If currently taking a reading, the instrument will wait until it is complete before executing this command. Readings are NOT erased from memory when using fetch. Use the R? command to read and erase data. Note that the data is transfered as ASCII, and thus it is not recommended to transfer a large number of data points using this method. :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("FETC?").split(","))) if numpy: return data * units return tuple(val * units for val in data) def read_data(self, sample_count): """ Transfer specified number of data points from reading memory (RGD_STORE) to output buffer. First data point sent to output buffer is the oldest. Data is erased after being sent to output buffer. :param int sample_count: Number of data points to be transfered to output buffer. If set to -1, all points in memory will be transfered. :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.') if sample_count == -1: sample_count = self.data_point_count units = UNITS[self.mode] 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: `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(","))) if numpy: return data * units return tuple(val * units for val in data) def read_last_data(self): """ Retrieve the last measurement taken. This can be executed at any time, including when the instrument is currently taking measurements. If there are no data points available, the value ``9.91000000E+37`` is returned. :units: As specified by the data returned by the instrument. :rtype: `~pint.Quantity` """ data = self.query("DATA:LAST?") unit_map = { "VDC": "V", "VAC": "V", } 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 u.Quantity(*data) def read_meter(self): """ Switch device from "idle" state to "wait-for-trigger" state. Immediately after the trigger conditions are met, the data will be sent to the output buffer of the instrument. This is similar to calling `~Agilent34410a.init` and then immediately following `~Agilent34410a.fetch`. :rtype: `~pint.Quantity` """ mode = self.mode units = UNITS[mode] return float(self.query("READ?")) * units # UNITS ####################################################################### UNITS = { 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, } ================================================ FILE: src/instruments/aimtti/__init__.py ================================================ #!/usr/bin/env python """ Module containing Aim-TTi power supplies """ from .aimttiel302p import AimTTiEL302P ================================================ FILE: src/instruments/aimtti/aimttiel302p.py ================================================ #!/usr/bin/env python """ Provides support for the Aim-TTI EL302P power supply """ # IMPORTS ##################################################################### from enum import Enum from instruments.abstract_instruments import PowerSupply from instruments.units import ureg as u from instruments.util_fns import ( bounded_unitful_property, enum_property, unitful_property, ) # CLASSES ##################################################################### class AimTTiEL302P(PowerSupply, PowerSupply.Channel): """ The Aim-TTI EL302P is a single output power supply. Because it is a single channel output, this object inherits from both PowerSupply and PowerSupply.Channel. Before this power supply can be remotely operated, remote communication must be enabled and the unit must be on. Please refer to the manual. Example usage: >>> import instruments as ik >>> psu = ik.aimtti.AimTTiEL302P.open_serial('/dev/ttyUSB0', 9600) >>> psu.voltage = 10 # Sets output voltage to 10V. >>> psu.voltage array(10.0) * V >>> psu.output = True # Turns on the power supply """ # ENUMS ## class Mode(Enum): """ Enum containing the possible modes of operations of the instrument. """ #: Constant voltage mode voltage = "M CV" #: Constant current mode current = "M CC" class Error(Enum): """ Enum containing the possible error codes returned by the instrument. """ #: No errors error_none = "ERR 0" #: Command not recognized error_not_recognized = "ERR 1" #: Command value outside instrument limits error_outside_limits = "ERR 2" # PROPERTIES ## voltage, voltage_min, voltage_max = bounded_unitful_property( "V", u.volt, valid_range=(0.0 * u.volt, 30.0 * u.volt), doc=""" Gets/sets the output voltage of the source. Value must be between 0V and 30V. :units: As specified, or assumed to be :math:`\\text{V}` otherwise. :type: `float` or `~pint.Quantity` """, input_decoration=lambda x: float(x[2:]), format_code="{}", ) voltage_sense = unitful_property( command="VO", units=u.volt, doc=""" Gets the actual output voltage measured by the power supply. :units: :math:`\\text{V}` :rtype: `~pint.Quantity` """, input_decoration=lambda x: float(x[2:]), readonly=True, ) current, current_min, current_max = bounded_unitful_property( "I", u.amp, valid_range=(0.01 * u.amp, 2.0 * u.amp), doc=""" Gets/sets the output current of the source. Value must be between 0.01A and 2A. :units: As specified, or assumed to be :math:`\\text{A}` otherwise. :type: `float` or `~pint.Quantity` """, input_decoration=lambda x: float(x[2:]), format_code="{}", ) current_sense = unitful_property( command="IO", units=u.amp, doc=""" Gets the actual output current measured by the power supply. :units: :math:`\\text{A}` :rtype: `~pint.Quantity` """, input_decoration=lambda x: float(x[2:]), readonly=True, ) @property def output(self): return self.query("OUT?") == "OUT ON" @output.setter def output(self, newval): value = "ON" if newval is True else "OFF" self.sendcmd(f"{value}") mode = enum_property( "M", Mode, doc=""" Gets output mode status. """, readonly=True, ) error = enum_property( "ERR", Error, doc=""" Gets the value in the error register. """, readonly=True, ) @property def name(self): """ Gets the name of the connected instrument. :rtype: `str` """ idn_string = self.query("*IDN?") idn_list = idn_string.split(",") return " ".join(idn_list[:2]) def reset(self): """ Resets the instrument to the default power-up settings (1.00V, 1.00A, output off). """ self.sendcmd("*RST") @property def channel(self): """ Return the channel (which in this case is the entire instrument, since there is only 1 channel on the EL302P.) :rtype: 'tuple' of length 1 containing a reference back to the parent EL302P object. """ return (self,) ================================================ FILE: src/instruments/comet/__init__.py ================================================ """ Module containing Comet instruments. """ from .cito_plus_1310 import CitoPlus1310 ================================================ FILE: src/instruments/comet/cito_plus_1310.py ================================================ #!/usr/bin/env python """Support for Comet Cito Plus RF generator.""" # IMPORTS ##################################################################### from enum import IntEnum from typing import Union from instruments.abstract_instruments import Instrument from instruments.units import ureg as u from instruments.util_fns import assume_units # CLASSES ##################################################################### class CitoPlus1310(Instrument): """Communicate with the Comet Cito Plus 1310 RF generator. Various connection options are available for different models. Note that this driver is only tested with the RS-232 interface and that, according to the manual, communication over TCP/IP is different. Important: Make sure that the correct parity is set in the instrument and when calling the instrument. The default seems to be even parity. Below example used even parity for the communication. In general, all communication parameters (baud rate, parity, etc.) can be set in the instrument itself. Below example just shows one possible configuration. Example: >>> import serial >>> import instruments as ik >>> port = '/dev/ttyUSB0' >>> baud = 115200 >>> inst = ik.comet.CitoPlus1310.open_serial(port, baud, parity=serial.PARITY_EVEN) >>> inst.rf # query RF state False >>> inst.rf = True # turn on RF """ class RegulationMode(IntEnum): """Regulation modes that are available on the Cito Plus 1310.""" ForwardPower = 0 LoadPower = 1 ProcessControl = 2 def __init__(self, filelike): super().__init__(filelike) self._address = 0x0A self._exception_codes = { 0x01: "Unknown parameter or illegal function code", 0x04: "Value invalid", 0x05: "Parameter not writable", 0x06: "Parameter not readable", 0x07: "Stop", 0x08: "Not allowed", 0x09: "Wrong data type", 0x0A: "Internal error", 0x0B: "Value too high", 0x0C: "Value too low", } self._byte_order = "big" # byte order for command and data self._byte_order_crc = "little" # byte order for CRC-16 checksum @property def name(self) -> str: """Get the name of the instrument.""" data = self.query(self._make_pkg(10)) return data.decode("utf-8") @property def forward_power(self) -> u.Quantity: """Get the actual forward power of the generator in W. :return: Forward power. :rtype: Quantity """ data = self.query(self._make_pkg(8021)) data = int.from_bytes(data, byteorder=self._byte_order) return assume_units(data, u.mW).to(u.W) @property def load_power(self) -> u.Quantity: """Get the actual load power of the generator in W. :return: Load power. :rtype: Quantity """ data = self.query(self._make_pkg(8023)) data = int.from_bytes(data, byteorder=self._byte_order) return assume_units(data, u.mW).to(u.W) @property def output_power(self) -> u.Quantity: """Get/set the set output power of the generator in W. :return: Output power. :rtype: Quantity """ data = self.query(self._make_pkg(1206)) data = int.from_bytes(data, byteorder=self._byte_order) return assume_units(data, u.mW).to(u.W) @output_power.setter def output_power(self, value: u.Quantity) -> None: value = assume_units(value, u.W).to(u.mW) if value < 1 * u.W: value = 0 * u.W # instrument can't set anything lower value = int(value.magnitude) self.sendcmd(self._make_pkg(1206, value)) @property def reflected_power(self) -> u.Quantity: """Get the actual reflected power of the generator in W. :return: Reflected power. :rtype: Quantity """ data = self.query(self._make_pkg(8022)) data = int.from_bytes(data, byteorder=self._byte_order) return assume_units(data, u.mW).to(u.W) @property def regulation_mode(self) -> RegulationMode: """Get/set the regulation mode of the generator. :return: Regulation mode. :rtype: RegulationMode """ data = self.query(self._make_pkg(1201)) return self.RegulationMode(int.from_bytes(data, byteorder=self._byte_order)) @regulation_mode.setter def regulation_mode(self, value) -> None: self.sendcmd(self._make_pkg(1201, value.value)) @property def rf(self) -> bool: """Get/set the RF state. :return: The RF state. :rtype: bool """ data = self.query(self._make_pkg(8000)) return int.from_bytes(data, byteorder=self._byte_order) != 1 @rf.setter def rf(self, value: bool) -> None: data = 1 if value else 0 self.sendcmd(self._make_pkg(1001, data)) def sendcmd(self, pkg: bytes) -> None: """Write a command to the instrument. Uses the query command to check return, i.e., that everything is fine, but does not return data. :param bytes pkg: The package to send to the instrument. """ self.query(pkg, write_cmd=True) def query(self, pkg: bytes, write_cmd=False) -> Union[None, bytes]: """Query instrument. This will check if the command is accepted by the instrument and if not, raise an OSError with the appropriate return code that came back. :param bytes pkg: The package to send to the instrument. :param boolwrite_cmd: If True, this is a write command and will only check if received package the same as sent one. """ self._file.write_raw(pkg) hdr = self._file.read_raw(2) fn_code = hdr[1] if fn_code != 0x41 and fn_code != 0x42: exc_code = self._file.read_raw(1)[0] self._check_exception(fn_code, exc_code) if write_cmd: # read the rest, make sure the packages agree and if not raise OSError. len_to_read = len(pkg) - 2 rest = self._file.read_raw(len_to_read) pkg_return = hdr + rest if pkg_return != pkg: raise OSError("Received package does not match sent package.") return # so it is a query and we expect data data_length = self._file.read_raw(1) data = self._file.read_raw( int.from_bytes(data_length, byteorder=self._byte_order) ) crc = self._file.read_raw(2) crc_exp = _crc16(hdr + data_length + data).to_bytes( 2, byteorder=self._byte_order_crc ) if crc != crc_exp: raise OSError("CRC-16 checksum of returned package does not match.") return data def _check_exception(self, fn_code: int, exc_code: int) -> None: """Checks if the function code is an exception and raises an OSError if so. :param int fn_code: The function code. :param int exc_code: The exception code. :raises OSError: If the function code is an exception. """ if fn_code != 0x41 or fn_code != 0x42: raise OSError( f"Exception code: {hex(exc_code)}: {self._exception_codes.get(exc_code, 'Unknown')}" ) def _make_hdr(self, fn_code: int) -> bytes: """Make the header according to our init settings. :param int fn_code: The function code to use. :return: The header bytes. :rtype: bytes """ hdr = bytes([self._address, fn_code]) return hdr def _make_pkg(self, cmd_code, data=None, data_length=4): """Create a package to send to the instrument. :param int cmd_code: The command code. :param data: The data to send. If None, this is a read command. Defaults to None. :param int data_length: The length of the data in bytes. Only used when writing. :return: Properly packed data to send to the instrument. :rtype: bytes """ if data is None: fn_code = 0x41 else: fn_code = 0x42 hdr = self._make_hdr(fn_code) cmd = cmd_code.to_bytes(length=2, byteorder=self._byte_order) if data is not None: dat = data.to_bytes(length=data_length, byteorder=self._byte_order) else: dat = (0x01).to_bytes(length=2, byteorder=self._byte_order) pkg = hdr + cmd + dat crc = _crc16(pkg) crc_bytes = crc.to_bytes(2, byteorder=self._byte_order_crc) return pkg + crc_bytes def _crc16(data: bytes): """Create the CRC-16 checksum for the given data. :param bytes data: The data for which to create the checksum. :return: The CRC-16 checksum. :rtype: int """ crc16tab = [ 0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241, 0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440, 0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40, 0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841, 0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40, 0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41, 0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641, 0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040, 0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240, 0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441, 0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41, 0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840, 0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41, 0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40, 0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640, 0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041, 0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240, 0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441, 0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41, 0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840, 0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41, 0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40, 0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640, 0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041, 0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241, 0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440, 0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40, 0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841, 0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40, 0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41, 0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641, 0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040, ] crc = 0 for dat in data: tmp = (0xFF & crc) ^ dat # only last 16 bits of `crc`! crc = (crc >> 8) ^ crc16tab[tmp] return crc ================================================ FILE: src/instruments/config.py ================================================ #!/usr/bin/env python """ Module containing support for loading instruments from configuration files. """ # IMPORTS ##################################################################### import warnings from ruamel.yaml import YAML from instruments.units import ureg as u from instruments.util_fns import setattr_expression, split_unit_str # FUNCTIONS ################################################################### def walk_dict(d, path): """ Given a "path" in a dictionary, returns the element specified by that path. For instance, given ``{'a': {'b': 42, 'c': {'d': ['foo']}}}`, the path ``"/"`` returns the whole dictionary, ``"/a"`` returns ``{'b': 42, 'c': {'d': ['foo']}}`` and ``/a/c/d"`` returns ``['foo']``. If ``path`` is a list, it is treated identically to ``"/" + "/".join(path)``. :param dict d: The dictionary to walk through :param path: The walking path through the dictionary :type path: `str` or `list` """ # Treat as a base case that the path is empty. if not path: return d 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:]) # 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)) yaml = YAML(typ="unsafe") # We avoid having to register !Q every time by doing as soon as the # relevant constructor is defined. yaml.Constructor.add_constructor("!Q", quantity_constructor) def load_instruments(conf_file_name, conf_path="/"): """ Given the path to a YAML-formatted configuration file and a path within that file, loads the instruments described in that configuration file. The subsection of the configuration file is expected to look like a map from names to YAML nodes giving the class and instrument URI for each instrument. For example:: ddg: class: !!python/name:instruments.srs.SRSDG645 uri: gpib+usb://COM7/15 Loading instruments from this configuration will result in a dictionary of 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:: instruments: ddg: class: !!python/name:instruments.srs.SRSDG645 uri: gpib+usb://COM7/15 prefs: ... Then, specifying ``"/instruments"`` as the configuration path will cause this function to load the instruments named in that block, and ignore all other keys in the YAML file. :param str conf_file_name: Name of the configuration file to load 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. :rtype: `dict` .. warning:: The configuration file must be trusted, as the class name references allow for executing arbitrary code. Do not load instruments from configuration files sent over network connections. Note that keys in sections excluded by the ``conf_path`` argument are still processed, such that any side effects that may occur due to such processing will occur independently of the value of ``conf_path``. """ if isinstance(conf_file_name, str): with open(conf_file_name) as f: conf_dict = yaml.load(f) else: conf_dict = yaml.load(conf_file_name) conf_dict = walk_dict(conf_dict, conf_path) inst_dict = {} for name, value in conf_dict.items(): try: inst_dict[name] = value["class"].open_from_uri(value["uri"]) 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 with URI " "{}:\n\t{}.".format(value["uri"], ex), RuntimeWarning, ) inst_dict[name] = None return inst_dict ================================================ FILE: src/instruments/delta_elektronika/__init__.py ================================================ #!/usr/bin/env python """ Module containing Delta Elektronika instruments """ from instruments.delta_elektronika.psc_eth import PscEth ================================================ FILE: src/instruments/delta_elektronika/psc_eth.py ================================================ """Support for Delta Elektronika DC power supplies with PSC-ETH-2 interface.""" # IMPORTS ##################################################################### from enum import IntEnum from typing import Tuple, Union from instruments.abstract_instruments import Instrument from instruments.units import ureg as u from instruments.util_fns import assume_units, unitful_property # CLASSES ##################################################################### class PscEth(Instrument): """Communicate with a Delta Elektronica one channel power supply via the PSC-ETH-2 ethernet interface. For communication, make sure the device is set to "ethernet" mode. Example: >>> import instruments as ik >>> from instruments import units as u >>> i = ik.delta_elektronika.PscEth.open_tcpip("192.168.127.100", port=8462) >>> print(i.name) """ def __init__(self, filelike): super().__init__(filelike) class LimitStatus(IntEnum): """Enum class for the limit status.""" OFF = 0 ON = 1 # CLASS PROPERTIES # @property def name(self) -> str: return self.query("*IDN?") @property def current_limit(self) -> tuple["PscEth.LimitStatus", u.Quantity]: """Get the current limit status. :return: A tuple of the current limit status and the current limit value. :rtype: `tuple` of (`PscEth.LimitStatus`, `~pint.Quantity`) """ resp = self.query("SYST:LIM:CUR?") val, status = resp.split(",") ls = self.LimitStatus.OFF if "off" in status.lower() else self.LimitStatus.ON return ls, assume_units(float(val), u.A) @property def voltage_limit(self) -> tuple["PscEth.LimitStatus", u.Quantity]: """Get the voltage limit status. :return: A tuple of the voltage limit status and the voltage limit value. :rtype: `tuple` of (`PscEth.LimitStatus`, `~pint.Quantity`) """ resp = self.query("SYST:LIM:VOL?") val, status = resp.split(",") ls = self.LimitStatus.OFF if "off" in status.lower() else self.LimitStatus.ON return ls, assume_units(float(val), u.V) current = unitful_property( "SOUR:CURR", u.A, format_code="{:.15f}", doc=""" Set/get the output current. Note: There is no bound checking of the value specified. :newval: The output current to set. :uval: `float` (assumes milliamps) or `~pint.Quantity` """, ) current_max = unitful_property( "SOUR:CURR:MAX", u.A, format_code="{:.15f}", doc=""" Set/get the maximum output current. Note: This value should generally not be used. It sets the maximum capable current of the power supply, which is fixed by the hardware. If you set this to other values, you will get strange measurement results. :newval: The maximum output current to set. :uval: `float` (assumes milliamps) or `~pint.Quantity` """, ) current_measure = unitful_property( "MEAS:CURR", u.A, format_code="{:.15f}", readonly=True, doc=""" Get the measured output current. :rtype: `~pint.Quantity` """, ) current_stepsize = unitful_property( "SOUR:CUR:STE", u.A, format_code="{:.15f}", readonly=True, doc=""" Get the output current step size. :rtype: `~pint.Quantity` """, ) voltage = unitful_property( "SOUR:VOL", u.V, format_code="{:.15f}", doc=""" Set/get the output voltage. Note: There is no bound checking of the value specified. :newval: The output voltage to set. :uval: `float` (assumes volts) or `~pint.Quantity` """, ) voltage_max = unitful_property( "SOUR:VOLT:MAX", u.V, format_code="{:.15f}", doc=""" Set/get the maximum output voltage. Note: This value should generally not be used. It sets the maximum capable voltage of the power supply, which is fixed by the hardware. If you set this to other values, you will get strange measurement results. :newval: The maximum output voltage to set. :uval: `float` (assumes volts) or `~pint.Quantity` """, ) voltage_measure = unitful_property( "MEAS:VOLT", u.V, format_code="{:.15f}", readonly=True, doc=""" Get the measured output voltage. :rtype: `~pint.Quantity` """, ) voltage_stepsize = unitful_property( "SOUR:VOL:STE", u.V, format_code="{:.15f}", readonly=True, doc=""" Get the output voltage step size. :rtype: `~pint.Quantity` """, ) def recall(self) -> None: """Recall the settings from non-volatile memory.""" self.sendcmd("*RCL") def reset(self) -> None: """Reset the instrument to default settings.""" self.sendcmd("*RST") def save(self) -> None: """Save the current settings to non-volatile memory.""" self.sendcmd("*SAV") def set_current_limit( self, stat: "PscEth.LimitStatus", val: Union[float, u.Quantity] = 0 ) -> None: """Set the current limit. :param stat: The limit status to set. :type stat: `PscEth.LimitStatus` :param val: The current limit value to set. Only requiered when turning it on. :type val: `float` (assumes milliamps) or `~pint.Quantity` """ if not isinstance(stat, PscEth.LimitStatus): raise TypeError("stat must be of type PscEth.LimitStatus") val = assume_units(val, u.A).to(u.A).magnitude cmd = f"SYST:LIM:CUR {val:.15f},{stat.name}" self.sendcmd(cmd) def set_voltage_limit( self, stat: "PscEth.LimitStatus", val: Union[float, u.Quantity] = 0 ) -> None: """Set the voltage limit. :param stat: The limit status to set. :type stat: `PscEth.LimitStatus` :param val: The voltage limit value to set. Only requiered when turning it on. :type val: `float` (assumes volts) or `~pint.Quantity` """ if not isinstance(stat, PscEth.LimitStatus): raise TypeError("stat must be of type PscEth.LimitStatus") val = assume_units(val, u.V).to(u.V).magnitude cmd = f"SYST:LIM:VOL {val:.15f},{stat.name}" self.sendcmd(cmd) ================================================ FILE: src/instruments/dressler/__init__.py ================================================ #!/usr/bin/env python """ Module containing Dressler instruments """ from instruments.dressler.cesar_1312 import Cesar1312 ================================================ FILE: src/instruments/dressler/cesar_1312.py ================================================ """Support for Dressler Cesar 1312 RF generator.""" # IMPORTS ##################################################################### from enum import IntEnum from typing import List, Tuple, Union from instruments.abstract_instruments import Instrument from instruments.units import ureg as u from instruments.util_fns import assume_units # CLASSES ##################################################################### class Cesar1312(Instrument): """Communicate with the Dressler Cesar 1312 RF generator. Various connection options are available for different models. This driver has been tested using the RS-232 option. The instrument for which this driver was tested required odd parity mode. You must provide the correct parity for your device when opening the serial connection. Note that you must set the control mode to `ControlMode.Host` in order to send any commands from the computer to the device. Example: >>> import serial >>> import instruments as ik >>> port = '/dev/ttyUSB0' >>> baud = 115200 >>> inst = ik.dressler.Cesar1312.open_serial(port, baud, parity=serial.PARITY_ODD) >>> inst.control_mode = inst.ControlMode.Host >>> inst.rf # query RF state False >>> inst.rf = True # turn on RF """ class ControlMode(IntEnum): """Control modes of the Cesar 1312 RF generator.""" Host = 2 UserPort = 4 FrontPanel = 6 class RegulationMode(IntEnum): """Regulation modes of the Cesar 1312 RF generator.""" ForwardPower = 6 LoadPower = 7 ExternalPower = 8 def __init__(self, filelike): super().__init__(filelike) self._retries = 3 self._csr_codes = { 0: "OK", 1: "Command rejected because unit is in wrong control mode.", 2: "Command rejected because RF output is on.", 4: "Command rejected because data sent is out of range.", 5: "Command rejected because User Port RF signal is off.", 7: "Command rejected because active fault(s) exist in generator.", 9: "Command rejected because the data byte count is incorrect.", 19: "Command rejected because the recipe mode is active", 50: "Command rejected because the frequency is out of range.", 51: "Command rejected because the duty cycle is out of range.", 99: "Command not implemented.", } self._address = 0x01 self._ack = bytes([0x06]) self._nak = bytes([0x15]) # CLASS PROPERTIES # @property def address(self) -> int: """Set/get the address of the device. Note that an address of 0 is the broadcast address. Most likely, you can leave this at the default of 1. :return: The set address. :rtype: int """ return self._address @address.setter def address(self, value: int) -> None: if value < 0 or value > 31: raise ValueError("Address must be in the range 0-31.") self._address = value @property def retries(self) -> int: """Set/get the number of retries if a command fails. :return: The number of retries as an integer. :rtype: int """ return self._retries @retries.setter def retries(self, value: int) -> tuple[int, int, bytes]: if value < 0: raise ValueError("Retries must be greater than or equal to 0.") self._retries = value # INSTRUMENT PROPERTIES # @property def control_mode(self) -> ControlMode: """Set/get the active control of the RF generator. Possible values are given in the `ControlMode` enum. For computer control, you likely want to set this to `ControlMode.Host`. ..note:: If you set the control mode at any time back to `ControlMode.FrontPanel`, the RF will turn off. :return: The current control mode. :rtype: ControlMode Example: >>> inst.control_mode = ik.dressler.Cesar1312.ControlMode.Host >>> inst.control_mode """ data = self.query(self._make_pkg(155)) return self.ControlMode(data[0]) @control_mode.setter def control_mode(self, value: ControlMode) -> None: self.sendcmd(self._make_pkg(14, self._make_data(1, value.value))) @property def name(self) -> str: """Get the supply type and size of the RF generator. :return: The supply type and size. :rtype: str Example: >>> inst.name 'CESAR_1312' """ name_type = self.query(self._make_pkg(128)).decode("utf-8") name_size = self.query(self._make_pkg(129)).decode("utf-8") return f"{name_type}{name_size}" @property def output_power(self) -> u.Quantity: """Set/get the output power of the device in W. :return: The output power in W for the defined mode. :rtype: u.Quantity Example: >>> inst.output_power = 10 * u.W >>> inst.output_power """ ret_data = self.query(self._make_pkg(164))[:2] return u.Quantity(int.from_bytes(ret_data, "little"), u.W) @output_power.setter def output_power(self, value: u.Quantity) -> None: value = assume_units(value, u.W).to(u.W) data = self._make_data(2, int(value.magnitude)) self.sendcmd(self._make_pkg(8, data)) @property def reflected_power(self) -> u.Quantity: """Get the reflected power in W. :return: The reflected power in W. :rtype: u.Quantity Example: >>> inst.reflected_power """ ret_data = self.query(self._make_pkg(166)) return u.Quantity(int.from_bytes(ret_data, "little"), u.W) @property def regulation_mode(self) -> RegulationMode: """Set/get the regulation mode. Possible values are given in the `RegulationMode` enum. :return: The current regulation mode. :rtype: RegulationMode Example: >>> inst.regulation_mode = ik.dressler.Cesar1312.RegulationMode.ForwardPower >>> inst.regulation_mode """ data = self.query(self._make_pkg(154)) return self.RegulationMode(data[0]) @regulation_mode.setter def regulation_mode(self, value: RegulationMode) -> None: self.sendcmd(self._make_pkg(3, self._make_data(1, value.value))) @property def rf(self) -> bool: """Set/get the RF output state of the device. RF on will be `True` while RF off will be `False`. :return: The current RF state. :rtype: bool Example: >>> inst.rf = True >>> inst.rf True """ data = self.query(self._make_pkg(162)) return bool(data[0] & 0b00100000) @rf.setter def rf(self, value: bool) -> None: cmd = 2 if value else 1 self.sendcmd(self._make_pkg(cmd)) # METHODS # def query(self, package: bytes, len_data=1) -> bytes: """Send a package to the instrument, assert it's all good, and return answer. This sends the package and checks the response. If the response is NAK, it retries until an ACK is received or the number of retries is reached. Once an ACK is received, it listens for the response of the instrument parsed the header, command, and optinally the data length (if > 6), then listens to the data and checksum and ensures that the overallc hecksum is zero. If not, it will send a NAK and retry reading until the checksum is zero. Then it will send an ACK to finish the communication. :param bytes package: The package to send. :return: The data received from the device. :rtype: bytes """ tries = 0 got_ack = False while tries < self.retries + 1: self._file.write_raw(package) response = self.read_raw(1) if response == self._ack: got_ack = True break else: tries += 1 if not got_ack: raise OSError("Failed to get ACK from device after sending the command.") tries = 0 got_pkg = False while tries < self.retries: header = self.read_raw(1) cmd = self.read_raw(1) adr, dlength = self._unpack_header(header) optional_data_length = None if dlength == 0b111: optional_data_length = self.read_raw(1) dlength = int(optional_data_length.hex(), 16) data = self.read_raw(dlength) if dlength > 0 else None checksum = self.read_raw(1) pkg = header + cmd if optional_data_length: pkg += optional_data_length if data: pkg += data pkg += checksum if self._calculate_checksum(pkg) == bytes([0x0]): self._file.write_raw(self._ack) got_pkg = True break else: tries += 1 self._file.write_raw(self._nak) if not got_pkg: raise OSError("Failed to get a valid package from the device.") return data def sendcmd(self, pkg: bytes) -> None: """Send a package to the instrument and assert it's all good. Uses the query routine and interprets the data, which should be one byte, as a CSR. If the CSR is not OK (0), will print a warning with the message. :param bytes pkg: The package to send. """ data = self.query(pkg) if data: csr = int(data.hex(), 16) if csr != 0: raise OSError( f"{self._csr_codes.get(csr, 'Unknown error')} (CSR={csr})" ) else: raise ValueError("No data received from the device.") def _make_data( self, length: Union[int, list[int]], data: Union[int, list[int]] ) -> bytes: """Create the data bytes for the package. If only one number is given, provide the length and the actual value as integers (or list). If more than one number is given, provide both as lists. :param Union[int, list[int]] length: The length of the data. :param Union[int, list[int]] data: The data to send. :return: Data in appropriate order. :rtype: bytes """ if isinstance(length, int): length = [length] if isinstance(data, int): data = [data] data_bytes = b"" for ll, dd in zip(length, data): data_bytes += dd.to_bytes(ll, byteorder="little", signed=False) return data_bytes def _make_pkg(self, cmd_number: int, data: Union[None, bytes] = None) -> bytes: """Make a package and return it packed as bytes. :param int cmd_number: The command number. :param bytes data: The data to send, already in proper order as bytes, or None. Defaults to None, which makes it a query command. """ data_length = len(data) if data else 0 header = self._pack_header(data_length) if data_length > 255: raise ValueError("Data length too long, must be <= 255.") if cmd_number > 255: raise ValueError("Command number too long, must be <= 255.") if data_length > 6: pkg = [header, cmd_number, data_length] else: pkg = [header, cmd_number] pkg = bytes(pkg) if data is not None: pkg += data pkg = pkg + self._calculate_checksum(pkg) return pkg @staticmethod def _calculate_checksum(data: bytes) -> bytes: """Calculate the checksum of the data. :param bytes data: The data to calculate the checksum for. :return: Checksum. :rtype: bytes """ checksum = data[0] for it, bt in enumerate(data): if it > 0: checksum ^= bt return bytes([checksum]) def _pack_header(self, data_length: int): """Make the header of the package. :param int data_length: The length of the data. If > 6, will be set to 7. :return: The header as an integer. """ if data_length > 6: data_length = 7 # need an extra byte for data length return (self._address << 3) + data_length @staticmethod def _unpack_header(hdr: bytes) -> tuple[int]: """Parse the header and return address and data length. :param bytes hdr: The header byte. :return: The address and data length as integers. :rtype: tuple[int] """ addr = hdr[0] >> 3 data_length = hdr[0] & 0b00000111 return addr, data_length ================================================ FILE: src/instruments/errors.py ================================================ #!/usr/bin/env python """ Module containing custom exception errors used by various instruments. """ # IMPORTS ##################################################################### # CLASSES ##################################################################### class AcknowledgementError(IOError): """ This error is raised when an instrument fails to send the expected acknowledgement string. """ class PromptError(IOError): """ This error is raised when an instrument fails to send a "prompt" character when one is expected. Typically most instruments do not send these characters, but some do in a misguided attempt to be more "user friendly". """ ================================================ FILE: src/instruments/fluke/__init__.py ================================================ #!/usr/bin/env python """ Module containing Fluke instruments """ from .fluke3000 import Fluke3000 ================================================ FILE: src/instruments/fluke/fluke3000.py ================================================ #!/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 } ================================================ FILE: src/instruments/generic_scpi/__init__.py ================================================ #!/usr/bin/env python """ Module containing generic SCPI instruments """ from .scpi_instrument import SCPIInstrument from .scpi_multimeter import SCPIMultimeter from .scpi_function_generator import SCPIFunctionGenerator ================================================ FILE: src/instruments/generic_scpi/scpi_function_generator.py ================================================ #!/usr/bin/env python """ Provides support for SCPI compliant function generators """ # IMPORTS ##################################################################### from instruments.units import ureg as u from instruments.abstract_instruments import FunctionGenerator from instruments.generic_scpi import SCPIInstrument from instruments.util_fns import enum_property, unitful_property # CLASSES ##################################################################### class SCPIFunctionGenerator(FunctionGenerator, SCPIInstrument): """ This class is used for communicating with generic SCPI-compliant function generators. Example usage: >>> import instruments as ik >>> import instruments.units as u >>> inst = ik.generic_scpi.SCPIFunctionGenerator.open_tcpip("192.168.1.1") >>> inst.frequency = 1 * u.kHz """ # CONSTANTS # _UNIT_MNEMONICS = { FunctionGenerator.VoltageMode.peak_to_peak: "VPP", FunctionGenerator.VoltageMode.rms: "VRMS", FunctionGenerator.VoltageMode.dBm: "DBM", } _MNEMONIC_UNITS = {mnem: unit for unit, mnem in _UNIT_MNEMONICS.items()} # FunctionGenerator CONTRACT # def _get_amplitude_(self): """ Gets the amplitude for a generic SCPI function generator :type: `tuple` containing `float` for value, and `FunctionGenerator.VoltageMode` for the type of measurement (eg VPP, VRMS, DBM). """ units = self.query("VOLT:UNIT?").strip() return (float(self.query("VOLT?").strip()), self._MNEMONIC_UNITS[units]) def _set_amplitude_(self, magnitude, units): """ Sets the amplitude for a generic SCPI function generator :param magnitude: Desired amplitude magnitude :type magnitude: `float` :param units: The type of voltage measurements units :type units: `FunctionGenerator.VoltageMode` """ self.sendcmd(f"VOLT:UNIT {self._UNIT_MNEMONICS[units]}") self.sendcmd(f"VOLT {magnitude}") # PROPERTIES # frequency = unitful_property( 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 `~pint.Quantity` """, ) function = enum_property( command="FUNC", enum=FunctionGenerator.Function, doc=""" Gets/sets the output function of the function generator :type: `SCPIFunctionGenerator.Function` """, ) offset = unitful_property( 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 `~pint.Quantity`) or assumed to be of units volts. :type: `~pint.Quantity` with units volts. """, ) @property def phase(self): raise NotImplementedError @phase.setter def phase(self, newval): raise NotImplementedError ================================================ FILE: src/instruments/generic_scpi/scpi_instrument.py ================================================ #!/usr/bin/env python """ Provides support for SCPI compliant instruments """ # 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 # CLASSES ##################################################################### class SCPIInstrument(Instrument): r""" Base class for all SCPI-compliant instruments. Inherits from from `~instruments.Instrument`. This class does not implement any instrument-specific communication commands. What it does add is several of the generic SCPI star commands. This includes commands such as ``*IDN?``, ``*OPC?``, and ``*RST``. Example usage: >>> import instruments as ik >>> inst = ik.generic_scpi.SCPIInstrument.open_tcpip('192.168.0.2', 8888) >>> print(inst.name) """ # PROPERTIES # @property def name(self): """ The name of the connected instrument, as reported by the standard SCPI command ``*IDN?``. :rtype: `str` """ return self.query("*IDN?") @property def scpi_version(self): """ Returns the version of the SCPI protocol supported by this instrument, as specified by the ``SYST:VERS?`` command described in section 21.21 of the SCPI 1999 standard. """ return self.query("SYST:VERS?") @property def op_complete(self): """ Check if all operations sent to the instrument have been completed. :rtype: `bool` """ result = self.query("*OPC?") return bool(int(result)) @property def power_on_status(self): """ Gets/sets the power on status for the instrument. :type: `bool` """ 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] if isinstance(newval, str): newval = newval.lower() if newval in on: self.sendcmd("*PSC 1") elif newval in off: self.sendcmd("*PSC 0") else: raise ValueError @property def self_test_ok(self): """ Gets the results of the instrument's self test. This lets you check if the self test was sucessful or not. :rtype: `bool` """ result = self.query("*TST?") try: result = int(result) return result == 0 except ValueError: return False # BASIC SCPI COMMANDS ## def reset(self): """ Reset instrument. On many instruments this is a factory reset and will revert all settings to default. """ self.sendcmd("*RST") def clear(self): """ Clear instrument. Consult manual for specifics related to that instrument. """ self.sendcmd("*CLS") def trigger(self): """ Send a software trigger event to the instrument. On most instruments this will cause some sort of hardware event to start. For example, a multimeter might take a measurement. This software trigger usually performs the same action as a hardware trigger to your instrument. """ 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") # SYSTEM COMMANDS ## @property def line_frequency(self): """ Gets/sets the power line frequency setting for the instrument. :return: The power line frequency :units: Hertz :type: `~pint.Quantity` """ 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").to("Hz").magnitude) ) # ERROR QUEUE HANDLING ## # NOTE: This functionality is still quite incomplete, and could be fleshed # out significantly still. One good thing would be to add handling # for SCPI-defined error codes. # # Another good use of this functionality would be to allow users to # automatically check errors after each command or query. 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 # error codes from the SCPI base, they can be added in a natural # way. no_error = 0 # -100 BLOCK: COMMAND ERRORS ## command_error = -100 invalid_character = -101 syntax_error = -102 invalid_separator = -103 data_type_error = -104 get_not_allowed = -105 # -106 and -107 not specified. parameter_not_allowed = -108 missing_parameter = -109 command_header_error = -110 header_separator_error = -111 program_mnemonic_too_long = -112 undefined_header = -113 header_suffix_out_of_range = -114 unexpected_number_of_parameters = -115 numeric_data_error = -120 invalid_character_in_number = -121 exponent_too_large = -123 too_many_digits = -124 numeric_data_not_allowed = -128 suffix_error = -130 invalid_suffix = -131 suffix_too_long = -134 suffix_not_allowed = -138 character_data_error = -140 invalid_character_data = -141 character_data_too_long = -144 character_data_not_allowed = -148 string_data_error = -150 invalid_string_data = -151 string_data_not_allowed = -158 block_data_error = -160 invalid_block_data = -161 block_data_not_allowed = -168 expression_error = -170 invalid_expression = -171 expression_not_allowed = -178 macro_error = -180 invalid_outside_macro_definition = -181 invalid_inside_macro_definition = -183 macro_parameter_error = -184 # pylint: disable=fixme # TODO: copy over other blocks. # -200 BLOCK: EXECUTION ERRORS ## # -300 BLOCK: DEVICE-SPECIFIC ERRORS ## # Note that device-specific errors also include all positive numbers. # -400 BLOCK: QUERY ERRORS ## # OTHER ERRORS ## #: Raised when the instrument detects that it has been turned from #: off to on. power_on = -500 # Yes, SCPI 1999 defines the instrument turning on as # an error. Yes, this makes my brain hurt. user_request_event = -600 request_control_event = -700 operation_complete = -800 def check_error_queue(self): """ Checks and clears the error queue for this device, returning a list of :class:`SCPIInstrument.ErrorCodes` or `int` elements for each error reported by the connected instrument. """ # pylint: disable=fixme # TODO: use SYST:ERR:ALL instead of SYST:ERR:CODE:ALL to get # messages as well. Should be just a bit more parsing, but the # SCPI standard isn't clear on how the pairs are represented, # so it'd be helpful to have an example first. err_list = map(int, self.query("SYST:ERR:CODE:ALL?").split(",")) return [ self.ErrorCodes[err] if isinstance(err, self.ErrorCodes) else err for err in err_list if err != self.ErrorCodes.no_error ] # DISPLAY COMMANDS ## @property def display_brightness(self): """ Brightness of the display on the connected instrument, represented as a float ranging from 0 (dark) to 1 (full brightness). :type: `float` """ return float(self.query("DISP:BRIG?")) @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(f"DISP:BRIG {newval}") @property def display_contrast(self): """ Contrast of the display on the connected instrument, represented as a float ranging from 0 (no contrast) to 1 (full contrast). :type: `float` """ return float(self.query("DISP:CONT?")) @display_contrast.setter def display_contrast(self, newval): if newval < 0 or newval > 1: raise ValueError("Display contrast must be a number between 0" " and 1.") self.sendcmd(f"DISP:CONT {newval}") ================================================ FILE: src/instruments/generic_scpi/scpi_multimeter.py ================================================ #!/usr/bin/env python """ Provides support for SCPI compliant multimeters """ # IMPORTS ##################################################################### from enum import Enum from instruments.units import ureg as u from instruments.abstract_instruments import Multimeter from instruments.generic_scpi import SCPIInstrument from instruments.util_fns import assume_units, enum_property, unitful_property # CONSTANTS ################################################################### 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"] # CLASSES ##################################################################### class SCPIMultimeter(SCPIInstrument, Multimeter): """ This class is used for communicating with generic SCPI-compliant multimeters. Example usage: >>> import instruments as ik >>> inst = ik.generic_scpi.SCPIMultimeter.open_tcpip("192.168.1.1") >>> print(inst.measure(inst.Mode.resistance)) """ # ENUMS ## class Mode(Enum): """ Enum of valid measurement modes for (most) SCPI compliant multimeters """ capacitance = "CAP" continuity = "CONT" current_ac = "CURR:AC" current_dc = "CURR:DC" diode = "DIOD" frequency = "FREQ" fourpt_resistance = "FRES" period = "PER" resistance = "RES" temperature = "TEMP" voltage_ac = "VOLT:AC" voltage_dc = "VOLT:DC" class TriggerMode(Enum): """ Valid trigger sources for most SCPI Multimeters. "Immediate": This is a continuous trigger. This means the trigger signal is always present. "External": External TTL pulse on the back of the instrument. It is active low. "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" class InputRange(Enum): """ Valid device range parameters outside of directly specifying the range. """ minimum = "MIN" maximum = "MAX" default = "DEF" automatic = "AUTO" class Resolution(Enum): """ Valid measurement resolution parameters outside of directly the resolution. """ minimum = "MIN" maximum = "MAX" default = "DEF" class TriggerCount(Enum): """ Valid trigger count parameters outside of directly the value. """ minimum = "MIN" maximum = "MAX" default = "DEF" infinity = "INF" class SampleCount(Enum): """ Valid sample count parameters outside of directly the value. """ minimum = "MIN" maximum = "MAX" default = "DEF" class SampleSource(Enum): """ Valid sample source parameters. #. "immediate": The trigger delay time is inserted between successive samples. After the first measurement is completed, the instrument waits the time specified by the trigger delay and then performs the next sample. #. "timer": Successive samples start one sample interval after the START of the previous sample. """ immediate = "IMM" timer = "TIM" # PROPERTIES ## # pylint: disable=unnecessary-lambda,undefined-variable mode = enum_property( command="CONF", enum=Mode, doc=""" Gets/sets the current measurement mode for the multimeter. Example usage: >>> dmm.mode = dmm.Mode.voltage_dc :type: `~SCPIMultimeter.Mode` """, input_decoration=lambda x: SCPIMultimeter._mode_parse(x), set_fmt="{}:{}", ) trigger_mode = enum_property( command="TRIG:SOUR", enum=TriggerMode, doc=""" Gets/sets the SCPI Multimeter trigger mode. Example usage: >>> dmm.trigger_mode = dmm.TriggerMode.external :type: `~SCPIMultimeter.TriggerMode` """, ) @property def input_range(self): """ Gets/sets the device input range for the device range for the currently set multimeter mode. Example usages: >>> dmm.input_range = dmm.InputRange.automatic >>> dmm.input_range = 1 * u.millivolt :units: As appropriate for the current mode setting. :type: `~pint.Quantity`, or `~SCPIMultimeter.InputRange` """ value = self.query("CONF?") mode = self.Mode(self._mode_parse(value)) value = value.split(" ")[1].split(",")[0] # Extract device range try: return float(value) * UNITS[mode] except ValueError: return self.InputRange(value.strip()) @input_range.setter def input_range(self, newval): current = self.query("CONF?") mode = self.Mode(self._mode_parse(current)) units = UNITS[mode] if isinstance(newval, self.InputRange): newval = newval.value else: newval = assume_units(newval, units).to(units).magnitude self.sendcmd(f"CONF:{mode.value} {newval}") @property def resolution(self): """ Gets/sets the measurement resolution for the multimeter. When specified as a float it is assumed that the user is providing an appropriate value. Example usage: >>> dmm.resolution = 3e-06 >>> dmm.resolution = dmm.Resolution.maximum :type: `int`, `float` or `~SCPIMultimeter.Resolution` """ value = self.query("CONF?") value = value.split(" ")[1].split(",")[1] # Extract resolution try: return float(value) except ValueError: return self.Resolution(value.strip()) @resolution.setter def resolution(self, newval): current = self.query("CONF?") mode = self.Mode(self._mode_parse(current)) input_range = current.split(" ")[1].split(",")[0] 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(f"CONF:{mode.value} {input_range},{newval}") @property def trigger_count(self): """ Gets/sets the number of triggers that the multimeter will accept before returning to an "idle" trigger state. Note that if the sample_count propery has been changed, the number of readings taken total will be a multiplication of sample count and trigger count (see property `SCPIMulimeter.sample_count`). If specified as a `~SCPIMultimeter.TriggerCount` value, the following options apply: #. "minimum": 1 trigger #. "maximum": Maximum value as per instrument manual #. "default": Instrument default as per instrument manual #. "infinity": Continuous. Typically when the buffer is filled in this case, the older data points are overwritten. Note that when using triggered measurements, it is recommended that you disable autorange by either explicitly disabling it or specifying your desired range. :type: `int` or `~SCPIMultimeter.TriggerCount` """ value = self.query("TRIG:COUN?") try: return int(value) except ValueError: return self.TriggerCount(value.strip()) @trigger_count.setter 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(f"TRIG:COUN {newval}") @property def sample_count(self): """ Gets/sets the number of readings (samples) that the multimeter will take per trigger event. The time between each measurement is defined with the sample_timer property. Note that if the trigger_count propery has been changed, the number of readings taken total will be a multiplication of sample count and trigger count (see property `SCPIMulimeter.trigger_count`). If specified as a `~SCPIMultimeter.SampleCount` value, the following options apply: #. "minimum": 1 sample per trigger #. "maximum": Maximum value as per instrument manual #. "default": Instrument default as per instrument manual Note that when using triggered measurements, it is recommended that you disable autorange by either explicitly disabling it or specifying your desired range. :type: `int` or `~SCPIMultimeter.SampleCount` """ value = self.query("SAMP:COUN?") try: return int(value) except ValueError: return self.SampleCount(value.strip()) @sample_count.setter 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(f"SAMP:COUN {newval}") trigger_delay = unitful_property( 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: `~pint.Quantity` """, ) sample_source = enum_property( command="SAMP:SOUR", enum=SampleSource, doc=""" Gets/sets the multimeter sample source. This determines whether the trigger delay or the sample timer is used to dtermine sample timing when the sample count is greater than 1. In both cases, the first sample is taken one trigger delay time period after the trigger event. After that, it depends on which mode is used. :type: `SCPIMultimeter.SampleSource` """, ) sample_timer = unitful_property( 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 `SCPIMultimeter.sample_source`). This command does not effect the delay between the trigger occuring and the start of the first sample. This trigger delay is set with the `~SCPIMultimeter.trigger_delay` property. :units: As specified, or assumed to be of units seconds otherwise. :type: `~pint.Quantity` """, ) @property def relative(self): raise NotImplementedError @relative.setter def relative(self, newval): raise NotImplementedError # METHODS ## def measure(self, mode=None): """ Instruct the multimeter to perform a one time measurement. The instrument will use default parameters for the requested measurement. The measurement will immediately take place, and the results are directly sent to the instrument's output buffer. Method returns a Python quantity consisting of a numpy array with the instrument value and appropriate units. If no appropriate units exist, (for example, continuity), then return type is `float`. :param mode: Desired measurement mode. If set to `None`, will default to the current mode. :type mode: `~SCPIMultimeter.Mode` """ 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)) ) # pylint: disable=no-member value = float(self.query(f"MEAS:{mode.value}?")) return value * UNITS[mode] # INTERNAL FUNCTIONS ## @staticmethod def _mode_parse(val): """ When given a string of the form "VOLT +1.00000000E+01,+3.00000000E-06" this function will return just the first component representing the mode the multimeter is currently in. :param str val: Input string to be parsed. :rtype: `str` """ val = val.split(" ")[0] if val == "VOLT": val = "VOLT:DC" return val # UNITS ####################################################################### UNITS = { 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, } ================================================ FILE: src/instruments/gentec_eo/__init__.py ================================================ """Module containing Gentec-eo instruments.""" from .blu import Blu ================================================ FILE: src/instruments/gentec_eo/blu.py ================================================ """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 abs(value) < 1e-99: return "0".zfill(8) for p in range(8, 1, -1): val = f"{value:.{p}g}".zfill(8) if len(val) == 8: return val ================================================ FILE: src/instruments/glassman/__init__.py ================================================ #!/usr/bin/env python """ Module containing Glassman power supplies """ from .glassmanfr import GlassmanFR ================================================ FILE: src/instruments/glassman/glassmanfr.py ================================================ #!/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") ================================================ FILE: src/instruments/hcp/__init__.py ================================================ #!/usr/bin/env python """ Module containing HC Photonics instruments """ from .tc038 import TC038 from .tc038d import TC038D ================================================ FILE: src/instruments/hcp/tc038.py ================================================ #!/usr/bin/env python """ Provides support for the TC038 AC crystal oven by HC Photonics. """ # IMPORTS ##################################################################### from instruments.units import ureg as u from instruments.abstract_instruments.instrument import Instrument from instruments.util_fns import assume_units # CLASSES ##################################################################### class TC038(Instrument): """ Communication with the HCP TC038 oven. This is the older version with an AC power supply and AC heater. It has parity or framing errors from time to time. Handle them in your application. """ _registers = { "temperature": "D0002", "setpoint": "D0120", } def __init__(self, *args, **kwargs): """ Initialize the TC038 is a crystal oven. Example usage: >>> import instruments as ik >>> import instruments.units as u >>> inst = ik.hcp.TC038.open_serial('COM10') >>> inst.setpoint = 45.3 >>> print(inst.temperature) """ super().__init__(*args, **kwargs) self.terminator = "\r" self.addr = 1 self._monitored_quantity = None self._file.parity = "E" # serial.PARITY_EVEN def sendcmd(self, command): """ Send "command" to the oven with "commandData". Parameters ---------- command : string, optional Command to be sent. Three chars indicating the type, and data for the command, if necessary. """ # 010 is CPU (01) and time to wait (0), which are fix super().sendcmd(chr(2) + f"{self.addr:02}" + "010" + command + chr(3)) def query(self, command): """ Send a command to the oven and read its response. Parameters ---------- command : string, optional Command to be sent. Three chars indicating the type, and data for the command, if necessary. Returns ------- string response of the system. """ return super().query(chr(2) + f"{self.addr:02}" + "010" + command + chr(3)) @property def monitored_quantity(self): """The monitored quantity.""" return self._monitored_quantity @monitored_quantity.setter def monitored_quantity(self, quantity="temperature"): """ Configure the oven to monitor a certain `quantity`. `quantity` may be any key of `_registers`. Default is the current temperature in °C. """ assert quantity in self._registers.keys(), f"Quantity {quantity} is unknown." # WRS in order to setup to monitor a word # monitor 1 to 16 words # monitor the word in the given register # Additional registers are added with a separating space or comma. self.query(command="WRS" + "01" + self._registers[quantity]) self._monitored_quantity = quantity @property def setpoint(self): """Read and return the current setpoint in °C.""" got = self.query(command="WRD" + "D0120" + ",01") # WRD: read words # start with register D0003 # read a single word, separated by space or comma return self._data_to_temp(got) @setpoint.setter def setpoint(self, value): """Set the setpoint to a temperature in °C.""" number = assume_units(value, u.degC).to(u.degC).magnitude commandData = f"D0120,01,{int(round(number * 10)):04X}" # Temperature without decimal sign in hex representation got = self.query(command="WWR" + commandData) assert got[5:7] == "OK", "A communication error occurred." @property def temperature(self): """Read and return the current temperature in °C.""" got = self.query(command="WRD" + "D0002" + ",01") return self._data_to_temp(got) @property def monitored_value(self): """ Read and return the monitored value. Per default it's the current temperature in °C. """ # returns the monitored words got = self.query(command="WRM") return self._data_to_temp(got) @property def information(self): """Read the device information.""" return self.query("INF6")[7:-1] @staticmethod def _data_to_temp(data): """Convert the returned hex value "data" to a temperature in °C.""" return u.Quantity(int(data[7:11], 16) / 10, u.degC) # get the hex number, convert to int and shift the decimal sign ================================================ FILE: src/instruments/hcp/tc038d.py ================================================ #!/usr/bin/env python """ Provides support for the TC038 AC crystal oven by HC Photonics. """ # IMPORTS ##################################################################### from instruments.units import ureg as u from instruments.abstract_instruments.instrument import Instrument from instruments.util_fns import assume_units # CLASSES ##################################################################### class TC038D(Instrument): """ Communication with the HCP TC038D oven. This is the newer version with DC heating. The temperature controller is on default set to modbus communication. The oven expects raw bytes written, no ascii code, and sends raw bytes. For the variables are two or four-byte modes available. We use the four-byte mode addresses, so do we. In that case element count has to be double the variables read. """ functions = {"read": 0x03, "writeMultiple": 0x10, "writeSingle": 0x06, "echo": 0x08} byteMode = 4 def __init__(self, *args, **kwargs): """ The TC038 is a crystal oven. Example usage: >>> import instruments as ik >>> import instruments.units as u >>> inst = ik.hcp.TC038.open_serial('COM10') >>> inst.setpoint = 45.3 >>> print(inst.temperature) """ super().__init__(*args, **kwargs) self.addr = 1 @staticmethod def CRC16(data): """Calculate the CRC16 checksum for the data byte array.""" CRC = 0xFFFF for octet in data: CRC ^= octet for j in range(8): lsb = CRC & 0x1 # least significant bit CRC = CRC >> 1 if lsb: CRC ^= 0xA001 return [CRC & 0xFF, CRC >> 8] def readRegister(self, address, count=1): """Read count variables from start address on.""" # Count has to be double the number of elements in 4-byte-mode. count *= self.byteMode // 2 data = [self.addr] data.append(self.functions["read"]) # function code data += [address >> 8, address & 0xFF] # 2B address data += [count >> 8, count & 0xFF] # 2B number of elements data += self.CRC16(data) self._file.write_raw(bytes(data)) # Slave address, function, length got = self.read_raw(3) if got[1] == self.functions["read"]: length = got[2] # data length, 2 Byte CRC read = self.read_raw(length + 2) if read[-2:] != bytes(self.CRC16(got + read[:-2])): raise ConnectionError("Response CRC does not match.") return read[:-2] else: # an error occurred end = self.read_raw(2) # empty the buffer if got[2] == 0x02: raise ValueError("The read start address is incorrect.") if got[2] == 0x03: raise ValueError("The number of elements exceeds the allowed range") raise ConnectionError(f"Unknown read error. Received: {got} {end}") def writeMultiple(self, address, values): """Write multiple variables.""" data = [self.addr] data.append(self.functions["writeMultiple"]) # function code data += [address >> 8, address & 0xFF] # 2B address if isinstance(values, int): data += [0x0, self.byteMode // 2] # 2B number of elements data.append(self.byteMode) # 1B number of write data for i in range(self.byteMode - 1, -1, -1): data.append(values >> i * 8 & 0xFF) elif hasattr(values, "__iter__"): elements = len(values) * self.byteMode // 2 data += [elements >> 8, elements & 0xFF] # 2B number of elements data.append(len(values) * self.byteMode) # 1B number of write data for element in values: for i in range(self.byteMode - 1, -1, -1): data.append(element >> i * 8 & 0xFF) else: raise ValueError( "Values has to be an integer or an iterable of " f"integers. values: {values}" ) data += self.CRC16(data) self._file.write_raw(bytes(data)) got = self.read_raw(2) # slave address, function if got[1] == self.functions["writeMultiple"]: # start address, number elements, CRC; each 2 Bytes long got += self.read_raw(2 + 2 + 2) if got[-2:] != bytes(self.CRC16(got[:-2])): raise ConnectionError("Response CRC does not match.") else: end = self.read_raw(3) # error code and CRC errors = { 0x02: "Wrong start address", 0x03: "Variable data error", 0x04: "Operation error", } raise ValueError(errors[end[0]]) @property def setpoint(self): """Get the current setpoint in °C.""" value = int.from_bytes(self.readRegister(0x106), byteorder="big") / 10 return u.Quantity(value, u.degC) @setpoint.setter def setpoint(self, value): """Set the setpoint in °C.""" value = assume_units(value, u.degC).to(u.degC) value = int(round(value.to("degC").magnitude * 10, 0)) self.writeMultiple(0x106, int(round(value))) @property def temperature(self): """Get the current temperature in °C.""" value = int.from_bytes(self.readRegister(0x0), byteorder="big") / 10 return u.Quantity(value, u.degC) ================================================ FILE: src/instruments/holzworth/__init__.py ================================================ #!/usr/bin/env python """ Module containing Holzworth instruments """ from .holzworth_hs9000 import HS9000 ================================================ FILE: src/instruments/holzworth/holzworth_hs9000.py ================================================ #!/usr/bin/env python """ Provides support for the Holzworth HS9000 """ # IMPORTS ##################################################################### from instruments.units import ureg as u from instruments.abstract_instruments.signal_generator import SignalGenerator, SGChannel from instruments.util_fns import ( ProxyList, split_unit_str, bounded_unitful_property, bool_property, ) # CLASSES ##################################################################### class HS9000(SignalGenerator): """ Communicates with a `Holzworth HS-9000 series`_ multi-channel frequency synthesizer. .. _Holzworth HS-9000 series: http://www.holzworth.com/synthesizers-multi.htm """ # INNER CLASSES # class Channel(SGChannel): """ Class representing a physical channel on the Holzworth HS9000 .. warning:: This class should NOT be manually created by the user. It is designed to be initialized by the `HS9000` class. """ def __init__(self, hs, idx_chan): self._hs = hs self._idx = idx_chan # We unpacked the channel index from the string of the form "CH1", # in order to make the API more Pythonic, but now we need to put # it back. # Some channel names, like "REF", are special and are preserved # as strs. self._ch_name = ( idx_chan if isinstance(idx_chan, str) else f"CH{idx_chan + 1}" ) # PRIVATE METHODS # def sendcmd(self, cmd): """ Function used to send a command to the instrument while wrapping the command with the neccessary identifier for the channel. :param str cmd: Command that will be sent to the instrument after being prefixed with the channel identifier """ self._hs.sendcmd(f":{self._ch_name}:{cmd}") def query(self, cmd): """ Function used to send a command to the instrument while wrapping the command with the neccessary identifier for the channel. :param str cmd: Command that will be sent to the instrument after being prefixed with the channel identifier :return: The result from the query :rtype: `str` """ return self._hs.query(f":{self._ch_name}:{cmd}") # STATE METHODS # def reset(self): """ Resets the setting of the specified channel Example usage: >>> import instruments as ik >>> hs = ik.holzworth.HS9000.open_tcpip("192.168.0.2", 8080) >>> hs.channel[0].reset() """ self.sendcmd("*RST") def recall_state(self): """ Recalls the state of the specified channel from memory. Example usage: >>> import instruments as ik >>> hs = ik.holzworth.HS9000.open_tcpip("192.168.0.2", 8080) >>> hs.channel[0].recall_state() """ self.sendcmd("*RCL") def save_state(self): """ Saves the current state of the specified channel. Example usage: >>> import instruments as ik >>> hs = ik.holzworth.HS9000.open_tcpip("192.168.0.2", 8080) >>> hs.channel[0].save_state() """ self.sendcmd("*SAV") # PROPERTIES # @property def temperature(self): """ Gets the current temperature of the specified channel. :units: As specified by the instrument. :rtype: `~pint.Quantity` """ val, units = split_unit_str(self.query("TEMP?")) units = f"deg{units}" return u.Quantity(val, units) frequency, frequency_min, frequency_max = bounded_unitful_property( "FREQ", units=u.GHz, doc=""" Gets/sets the frequency of the specified channel. When setting, values are bounded between what is returned by `frequency_min` and `frequency_max`. Example usage: >>> import instruments as ik >>> hs = ik.holzworth.HS9000.open_tcpip("192.168.0.2", 8080) >>> print(hs.channel[0].frequency) >>> print(hs.channel[0].frequency_min) >>> print(hs.channel[0].frequency_max) :type: `~pint.Quantity` :units: As specified or assumed to be of units GHz """, ) power, power_min, power_max = bounded_unitful_property( "PWR", 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` and `power_max`. Example usage: >>> import instruments as ik >>> hs = ik.holzworth.HS9000.open_tcpip("192.168.0.2", 8080) >>> print(hs.channel[0].power) >>> print(hs.channel[0].power_min) >>> print(hs.channel[0].power_max) :type: `~pint.Quantity` :units: `instruments.units.dBm` """, ) phase, phase_min, phase_max = bounded_unitful_property( "PHASE", 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` and `phase_max`. Example usage: >>> import instruments as ik >>> hs = ik.holzworth.HS9000.open_tcpip("192.168.0.2", 8080) >>> print(hs.channel[0].phase) >>> print(hs.channel[0].phase_min) >>> print(hs.channel[0].phase_max) :type: `~pint.Quantity` :units: As specified or assumed to be of units degrees """, ) output = bool_property( "PWR:RF", inst_true="ON", inst_false="OFF", set_fmt="{}:{}", doc=""" Gets/sets the output status of the channel. Setting to `True` will turn the channel's output stage on, while a value of `False` will turn it off. Example usage: >>> import instruments as ik >>> hs = ik.holzworth.HS9000.open_tcpip("192.168.0.2", 8080) >>> print(hs.channel[0].output) >>> hs.channel[0].output = True :type: `bool` """, ) # PROXY LIST ## def _channel_idxs(self): """ Internal function used to get the list of valid channel names to be used by `HS9000.channel` :return: A list of valid channel indicies :rtype: `list` of `int` and `str` """ # The command :ATTACH? returns a string of the form ":CH1:CH2" to # indicate what channels are attached to the internal USB bus. # We convert what channel names we can to integers, and leave the # rest as strings. return [ ( int(ch_name.replace("CH", "")) - 1 if ch_name.startswith("CH") else ch_name.strip() ) for ch_name in self.query(":ATTACH?").split(":") if ch_name ] @property def channel(self): """ Gets a specific channel on the HS9000. The desired channel is accessed like one would access a list. Example usage: >>> import instruments as ik >>> hs = ik.holzworth.HS9000.open_tcpip("192.168.0.2", 8080) >>> print(hs.channel[0].frequency) :return: A channel object for the HS9000 :rtype: `~HS9000.Channel` """ return ProxyList(self, self.Channel, self._channel_idxs()) # OTHER PROPERTIES # @property def name(self): """ Gets identification string of the HS9000 :return: The string as usually returned by ``*IDN?`` on SCPI instruments :rtype: `str` """ # This is a weird one; the HS-9000 associates the :IDN? command # with each individual channel, though we want it to be a synthesizer- # wide property. To solve this, we assume that CH1 is always a channel # and ask its name. return self.channel[0].query("IDN?") @property def ready(self): """ Gets the ready status of the HS9000. :return: If the instrument is ready for operation :rtype: `bool` """ return "Ready" in self.query(":COMM:READY?") ================================================ FILE: src/instruments/hp/__init__.py ================================================ #!/usr/bin/env python """ Module containing HP instruments """ from .hp3325a import HP3325a from .hp3456a import HP3456a from .hp6624a import HP6624a from .hp6632b import HP6632b from .hp6652a import HP6652a from .hpe3631a import HPe3631a ================================================ FILE: src/instruments/hp/hp3325a.py ================================================ #!/usr/bin/env python # # hp3325a.py: Driver for the HP3235a/b Synthesizer/Function Generator. # # © 2023 Scott Phillips (polygonguru@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 HP3325a Synthesizer/Function Generator Originally contributed and copyright held by Scott Phillips (polygonguru@gmail.com) An unrestricted license has been provided to the maintainers of the Instrument Kit project. """ # IMPORTS ##################################################################### import math from enum import Enum, IntEnum from instruments import Instrument from instruments.abstract_instruments import FunctionGenerator from instruments.units import ureg as u from instruments.util_fns import enum_property, unitful_property, bool_property # CLASSES ##################################################################### def amplitude_parse(am_resp: str) -> float: am_units = am_resp[-2:] am_num = am_resp[:-2].replace("AM", "").strip() return float(am_num * HP3325a.ampl_scale[am_units]) def frequency_parse(fr_resp: str) -> float: freq_units = fr_resp[-2:] freq_num = fr_resp[:-2].replace("FR", "").strip() return float(freq_num * HP3325a.freq_scale[freq_units]) def offset_parse(of_resp: str) -> float: of_resp = of_resp.replace("OF", "") of_units = of_resp[-2:] return float(of_resp[:-2]) * (1 if of_units == "VO" else 1000) class HP3325a(FunctionGenerator): """The `HP3325a` is a 20Mhz Synthesizer / Function Generator. It supports sine-, square-, triangle-, ramp-waves across a wide range of frequencies. It also supports amplitude and phase modulation, as well as DC-offset. `HP3325a` is a HPIB / pre-448.2 instrument. """ def __init__(self, filelike): """ Initialise the instrument, and set the required eos, eoi needed for communication. """ super().__init__(filelike) self._channel_count = 1 self.terminator = "\r\n" class Waveform(IntEnum): """ Enum with the supported math modes """ dc_only = 0 sine = 1 square = 2 triangle = 3 positive_ramp = 4 negative_ramp = 5 class FrequencyScale(Enum): """ Enum with the supported frequency scales """ hertz = 1 kilohertz = 1e3 megahertz = 1e6 class AmplitudeScale(Enum): """ Enum with the supported amplitude scales """ Volts = 1 Millivolts = 1e-3 Volts_RMS = math.sqrt(2.0) Millivolts_RMS = 1e-3 * math.sqrt(2.0) freq_scale = {"HZ": 1, "KH": 1e3, "MH": 1e6} ampl_scale = { "VO": 1, "MV": 1e-3, "VR": math.sqrt(2.0), "MR": 1e-3 * math.sqrt(2.0), } # PROPERTIES ## function = enum_property( command="IFU", enum=Waveform, set_cmd="FU", doc=""" Gets/sets the output function of the function generator type: `HP3325a.Waveform` """, input_decoration=int, set_fmt="{}{}", ) amplitude = unitful_property( command="IAM", units=u.volts, set_cmd="AM", format_code="{}", doc=""" Gets/sets the amplitude of the output waveform :type: `float` """, input_decoration=amplitude_parse, set_fmt="{}{}VO", ) frequency = unitful_property( command="IFR", units=u.hertz, set_cmd="FR", format_code="{}", doc=""" Gets/sets the frequency of the output waveform :type: `float` """, input_decoration=frequency_parse, set_fmt="{}{}HZ", ) offset = unitful_property( command="IOF", units=u.volts, set_cmd="OF", format_code="{}", doc=""" Gets/sets the offset of the output waveform :type: `float` """, input_decoration=offset_parse, set_fmt="{}{}VO", ) phase = unitful_property( command="IPH", units=u.degrees, set_cmd="PH", format_code="{}", doc=""" Gets/sets the phase of the output waveform :type: `float` """, input_decoration=lambda x: float(x.replace("PH", "").replace("DE", "").strip()), set_fmt="{}{}DE", ) high_voltage = bool_property( command="IHV", set_cmd="HV", inst_true="HV1", inst_false="HV0", doc=""" Gets/sets the high voltage mode of the output waveform :type: `bool` """, set_fmt="{}{}", ) amplitude_modulation = bool_property( command="IMA", set_cmd="MA", inst_true="1", inst_false="0", doc=""" Gets/sets the amplitude modulation mode of the output waveform :type: `bool` """, set_fmt="{}{}", ) marker_frequency = bool_property( command="IMA", set_cmd="MA", inst_true="1", inst_false="0", doc=""" Gets/sets the marker frequency mode of the output waveform :type: `bool` """, set_fmt="{}{}", ) def query(self, cmd, size=-1): """ Query the instrument with the given command and return the response """ # strip the question mark because HP3325A is too old for that cmd = cmd.replace("?", "") return Instrument.query(self, cmd, size) def amplitude_calibration(self): self.sendcmd("AC") def assign_zero_phase(self): self.sendcmd("AP") # TODO - Support CALM which only works on 3325B # TODO - Support DCLR which only works on 3325B # TODO - Support DISP which only works on 3325B # TODO - Support DRCL,DSTO which only works on 3325B # TODO - Support DSP which only works on 3325B # TODO - Support ECHO which only works on 3325B # TODO - Support ENH which only works on 3325B # TODO - Support ESTB which only works on 3325B # TODO - Support EXTR which only works on 3325B # TODO - Support HEAD which only works on 3325B # TODO - Support *IDN? which only works on 3325B # TODO - Support LCL which only works on 3325B def query_error(self) -> int: # TODO - Support ERR? on HP3325B which is more specific err_resp = self.query("IER") return int(err_resp.replace("E", "").replace("R", "").strip()) ================================================ FILE: src/instruments/hp/hp3456a.py ================================================ #!/usr/bin/env python # # hp3456a.py: Driver for the HP3456a Digital Voltmeter. # # © 2014 Willem Dijkstra (wpd@xs4all.nl). # # 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 HP3456a Digital Voltmeter Originally contributed and copyright held by Willem Dijkstra (wpd@xs4all.nl) An unrestricted license has been provided to the maintainers of the Instrument Kit project. """ # IMPORTS ##################################################################### import time from enum import Enum, IntEnum 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 ##################################################################### class HP3456a(Multimeter): """The `HP3456a` is a 6 1/2 digit bench multimeter. It supports DCV, ACV, ACV + DCV, 2 wire Ohms, 4 wire Ohms, DCV/DCV Ratio, ACV/DCV Ratio, Offset compensated 2 wire Ohms and Offset compensated 4 wire Ohms measurements. Measurements can be further extended using a system math mode that allows for pass/fail, statistics, dB/dBm, null, scale and percentage readings. `HP3456a` is a HPIB / pre-448.2 instrument. """ def __init__(self, filelike): """ Initialise the instrument, and set the required eos, eoi needed for communication. """ super().__init__(filelike) self.timeout = 15 * u.second self.terminator = "\r" self.sendcmd("HO0T4SO1") self._null = False # ENUMS ## class MathMode(IntEnum): """ Enum with the supported math modes """ off = 0 pass_fail = 1 statistic = 2 null = 3 dbm = 4 thermistor_f = 5 thermistor_c = 6 scale = 7 percent = 8 db = 9 class Mode(Enum): """ Enum containing the supported mode codes """ #: DC voltage dcv = "S0F1" #: AC voltage acv = "S0F2" #: RMS of DC + AC voltage acvdcv = "S0F3" #: 2 wire resistance resistance_2wire = "S0F4" #: 4 wire resistance resistance_4wire = "S0F5" #: ratio DC / DC voltage ratio_dcv_dcv = "S1F1" #: ratio AC / DC voltage ratio_acv_dcv = "S1F2" #: ratio (AC + DC) / DC voltage ratio_acvdcv_dcv = "S1F3" #: offset compensated 2 wire resistance oc_resistence_2wire = "S1F4" #: offset compensated 4 wire resistance oc_resistence_4wire = "S1F5" class Register(Enum): """ Enum with the register names for all `HP3456a` internal registers. """ number_of_readings = "N" number_of_digits = "G" nplc = "I" delay = "D" mean = "M" variance = "V" count = "C" lower = "L" r = "R" upper = "U" y = "Y" z = "Z" class TriggerMode(IntEnum): """ Enum with valid trigger modes. """ internal = 1 external = 2 single = 3 hold = 4 class ValidRange(Enum): """ Enum with the valid ranges for voltage, resistance, and number of 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) # PROPERTIES ## mode = enum_property( "", Mode, doc="""Set the measurement mode. :type: `HP3456a.Mode` """, writeonly=True, set_fmt="{}{}", ) autozero = bool_property( "Z", inst_true="1", inst_false="0", doc="""Set the autozero mode. This is used to compensate for offsets in the dc input amplifier circuit of the multimeter. If set, the amplifier"s input circuit is shorted to ground prior to actual measurement in order to take an offset reading. This offset is then used to compensate for drift in the next measurement. When disabled, one offset reading is taken immediately and stored into memory to be used for all successive measurements onwards. Disabling autozero increases the `HP3456a`"s measurement speed, and also makes the instrument more suitable for high impendance measurements since no input switching is done.""", writeonly=True, set_fmt="{}{}", ) filter = bool_property( "FL", inst_true="1", inst_false="0", doc="""Set the analog filter mode. The `HP3456a` has a 3 pole active filter with greater than 60dB attenuation at frequencies of 50Hz and higher. The filter is applied between the input terminals and input amplifier. When in ACV or ACV+DCV functions the filter is applied to the output of the ac converter and input amplifier. In these modes select the filter for measurements below 400Hz.""", writeonly=True, set_fmt="{}{}", ) math_mode = enum_property( "M", MathMode, doc="""Set the math mode. The `HP3456a` has a number of different math modes that can change measurement output, or can provide additional statistics. Interaction with these modes is done via the `HP3456a.Register`. :type: `HP3456a.MathMode` """, writeonly=True, set_fmt="{}{}", ) trigger_mode = enum_property( "T", TriggerMode, doc="""Set the trigger mode. Note that using `HP3456a.measure()` will override the `trigger_mode` to `HP3456a.TriggerMode.single`. :type: `HP3456a.TriggerMode` """, writeonly=True, set_fmt="{}{}", ) @property def number_of_readings(self): """Get/set the number of readings done per trigger/measurement cycle using `HP3456a.Register.number_of_readings`. :type: `float` :rtype: `float` """ return self._register_read(HP3456a.Register.number_of_readings) @number_of_readings.setter def number_of_readings(self, value): self._register_write(HP3456a.Register.number_of_readings, value) @property def number_of_digits(self): """Get/set the number of digits used in measurements using `HP3456a.Register.number_of_digits`. Set to higher values to increase accuracy at the cost of measurement speed. :type: `int` """ return int(self._register_read(HP3456a.Register.number_of_digits)) @number_of_digits.setter 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))) ) self._register_write(HP3456a.Register.number_of_digits, newval) @property def nplc(self): """Get/set the number of powerline cycles to integrate per measurement using `HP3456a.Register.nplc`. Setting higher values increases accuracy at the cost of a longer measurement time. The implicit assumption is that the input reading is stable over the number of powerline cycles to integrate. :type: `int` """ return int(self._register_read(HP3456a.Register.nplc)) @nplc.setter def nplc(self, newval): newval = int(newval) valid = HP3456a.ValidRange["nplc"].value if newval in valid: self._register_write(HP3456a.Register.nplc, newval) else: raise ValueError("Valid nplc settings are: " "{}".format(valid)) @property def delay(self): """Get/set the delay that is waited after a trigger for the input to settle using `HP3456a.Register.delay`. :type: As specified, assumed to be `~quantaties.Quantity.s` otherwise :rtype: `~quantaties.Quantity.s` """ return self._register_read(HP3456a.Register.delay) * u.s @delay.setter def delay(self, value): delay = assume_units(value, u.s).to(u.s).magnitude self._register_write(HP3456a.Register.delay, delay) @property def mean(self): """ Get the mean over `HP3456a.Register.count` measurements from `HP3456a.Register.mean` when in `HP3456a.MathMode.statistic`. :rtype: `float` """ return self._register_read(HP3456a.Register.mean) @property def variance(self): """ Get the variance over `HP3456a.Register.count` measurements from `HP3456a.Register.variance` when in `HP3456a.MathMode.statistic`. :rtype: `float` """ return self._register_read(HP3456a.Register.variance) @property def count(self): """ Get the number of measurements taken from `HP3456a.Register.count` when in `HP3456a.MathMode.statistic`. :rtype: `int` """ return int(self._register_read(HP3456a.Register.count)) @property def lower(self): """ Get/set the value in `HP3456a.Register.lower`, which indicates the lowest value measurement made while in `HP3456a.MathMode.statistic`, or the lowest value preset for `HP3456a.MathMode.pass_fail`. :type: `float` """ return self._register_read(HP3456a.Register.lower) @lower.setter def lower(self, value): self._register_write(HP3456a.Register.lower, value) @property def upper(self): """ Get/set the value in `HP3456a.Register.upper`, which indicates the highest value measurement made while in `HP3456a.MathMode.statistic`, or the highest value preset for `HP3456a.MathMode.pass_fail`. :type: `float` :rtype: `float` """ return self._register_read(HP3456a.Register.upper) @upper.setter def upper(self, value): return self._register_write(HP3456a.Register.upper, value) @property def r(self): """ Get/set the value in `HP3456a.Register.r`, which indicates the resistor value used while in `HP3456a.MathMode.dbm` or the number of recalled readings in reading storage mode. :type: `float` :rtype: `float` """ return self._register_read(HP3456a.Register.r) @r.setter def r(self, value): self._register_write(HP3456a.Register.r, value) @property def y(self): """ Get/set the value in `HP3456a.Register.y` to be used in calculations when in `HP3456a.MathMode.scale` or `HP3456a.MathMode.percent`. :type: `float` :rtype: `float` """ return self._register_read(HP3456a.Register.y) @y.setter def y(self, value): self._register_write(HP3456a.Register.y, value) @property def z(self): """ Get/set the value in `HP3456a.Register.z` to be used in calculations when in `HP3456a.MathMode.scale` or the first reading when in `HP3456a.MathMode.statistic`. :type: `float` :rtype: `float` """ return self._register_read(HP3456a.Register.z) @z.setter def z(self, value): self._register_write(HP3456a.Register.z, value) @property def input_range(self): """Set the input range to be used. 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: `~pint.Quantity` """ raise NotImplementedError @input_range.setter def input_range(self, value): if isinstance(value, str): if value.lower() == "auto": self.sendcmd("R1W") else: raise ValueError( "Only 'auto' is acceptable when specifying " "the input range as a string." ) elif isinstance(value, u.Quantity): if value.units == u.volt: valid = HP3456a.ValidRange.voltage.value value = value.to(u.volt) elif value.units == u.ohm: valid = HP3456a.ValidRange.resistance.value value = value.to(u.ohm) else: raise ValueError( "Value {} not quantity.volt or quantity.ohm" "".format(value) ) value = float(value.magnitude) if value not in valid: raise ValueError( "Value {} outside valid ranges " "{}".format(value, valid) ) value = valid.index(value) + 2 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)) ) @property def relative(self): """ Enable or disable `HP3456a.MathMode.Null` on the instrument. :type: `bool` """ return self._null @relative.setter def relative(self, value): if value is True: self._null = True self.sendcmd(f"M{HP3456a.MathMode.null.value}") elif value is False: self._null = False self.sendcmd(f"M{HP3456a.MathMode.off.value}") else: raise TypeError( "Relative setting must be specified as a bool, " "got {}".format(type(value)) ) # METHODS ## def auto_range(self): """ Set input range to auto. The `HP3456a` should upscale when a reading is at 120% and downscale when it below 11% full scale. Note that auto ranging can increase the measurement time. """ self.input_range = "auto" def fetch(self, mode=None): """Retrieve n measurements after the HP3456a has been instructed to perform a series of similar measurements. Typically the mode, range, nplc, analog filter, autozero is set along with the number of measurements to take. The series is then started at the trigger command. Example usage: >>> dmm.number_of_digits = 6 >>> dmm.auto_range() >>> dmm.nplc = 1 >>> dmm.mode = dmm.Mode.resistance_2wire >>> n = 100 >>> dmm.number_of_readings = n >>> dmm.trigger() >>> time.sleep(n * 0.04) >>> v = dmm.fetch(dmm.Mode.resistance_2wire) >>> print len(v) 10 :param mode: Desired measurement mode. If not specified, the previous set mode will be used, but no measurement unit will be returned. :type mode: `HP3456a.Mode` :return: A series of measurements from the multimeter. :rtype: `~pint.Quantity` """ if mode is not None: units = UNITS[mode] else: units = 1 value = self.query("", size=-1) values = [float(x) * units for x in value.split(",")] return values def measure(self, mode=None): """Instruct the HP3456a to perform a one time measurement. The measurement will use the current set registers for the measurement (number_of_readings, number_of_digits, nplc, delay, mean, lower, upper, y and z) and will immediately take place. Note that using `HP3456a.measure()` will override the `trigger_mode` to `HP3456a.TriggerMode.single` Example usage: >>> dmm = ik.hp.HP3456a.open_gpibusb("/dev/ttyUSB0", 22) >>> dmm.number_of_digits = 6 >>> dmm.nplc = 1 >>> print dmm.measure(dmm.Mode.resistance_2wire) :param mode: Desired measurement mode. If not specified, the previous set mode will be used, but no measurement unit will be returned. :type mode: `HP3456a.Mode` :return: A measurement from the multimeter. :rtype: `~pint.Quantity` """ if mode is not None: modevalue = mode.value units = UNITS[mode] else: modevalue = "" units = 1 self.sendcmd(f"{modevalue}W1STNT3") value = self.query("", size=-1) return float(value) * units def _register_read(self, name): """ Read a register on the HP3456a. :param name: The name of the register to read from :type name: `HP3456a.Register` :rtype: `float` """ try: name = HP3456a.Register[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(f"RE{name.value}") time.sleep(0.1) return float(self.query("", size=-1)) def _register_write(self, name, value): """ Write a register on the HP3456a. :param name: The name of the register to write to :type name: `HP3456a.Register` :type value: `float` """ try: name = HP3456a.Register[name] except KeyError: pass if not isinstance(name, HP3456a.Register): 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, ]: raise ValueError(f"register {name} is read only") self.sendcmd(f"W{value}ST{name.value}") time.sleep(0.1) def trigger(self): """ Signal a single manual trigger event to the `HP3456a`. """ self.sendcmd("T3") # UNITS ####################################################################### UNITS = { None: 1, 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: u.ohm, HP3456a.Mode.oc_resistence_4wire: u.ohm, } ================================================ FILE: src/instruments/hp/hp6624a.py ================================================ #!/usr/bin/env python """ Provides support for the HP6624a power supply """ # IMPORTS ##################################################################### from enum import Enum from instruments.abstract_instruments import PowerSupply from instruments.units import ureg as u from instruments.util_fns import ProxyList, unitful_property, bool_property # CLASSES ##################################################################### class HP6624a(PowerSupply): """ The HP6624a is a multi-output power supply. This class can also be used for HP662xa, where x=1,2,3,4,7. Note that some models have fewer channels than the HP6624, and it is up to the user to take this into account. This can be changed with the `~HP6624a.channel_count` property. Example usage: >>> import instruments as ik >>> psu = ik.hp.HP6624a.open_gpibusb('/dev/ttyUSB0', 1) >>> psu.channel[0].voltage = 10 # Sets channel 1 voltage to 10V. """ def __init__(self, filelike): super().__init__(filelike) self._channel_count = 4 # INNER CLASSES # class Channel(PowerSupply.Channel): """ Class representing a power output channel on the HP6624a. .. warning:: This class should NOT be manually created by the user. It is designed to be initialized by the `HP6624a` class. """ def __init__(self, hp, idx): self._hp = hp self._idx = idx + 1 # COMMUNICATION METHODS # def _format_cmd(self, cmd): cmd = cmd.split(" ") if len(cmd) == 1: cmd = f"{cmd[0]} {self._idx}" else: cmd = "{cmd} {idx},{value}".format( cmd=cmd[0], idx=self._idx, value=cmd[1] ) return cmd def sendcmd(self, cmd): """ Function used to send a command to the instrument while wrapping the command with the neccessary identifier for the channel. :param str cmd: Command that will be sent to the instrument after being prefixed with the channel identifier """ cmd = self._format_cmd(cmd) self._hp.sendcmd(cmd) def query(self, cmd): """ Function used to send a command to the instrument while wrapping the command with the neccessary identifier for the channel. :param str cmd: Command that will be sent to the instrument after being prefixed with the channel identifier :return: The result from the query :rtype: `str` """ cmd = self._format_cmd(cmd) return self._hp.query(cmd) # PROPERTIES # @property def mode(self): """ Gets/sets the mode for the specified channel. """ raise NotImplementedError @mode.setter def mode(self, newval): raise NotImplementedError voltage = unitful_property( "VSET", u.volt, set_fmt="{} {:.1f}", output_decoration=float, doc=""" Gets/sets the voltage of the specified channel. If the device is in constant current mode, this sets the voltage limit. Note there is no bounds checking on the value specified. :units: As specified, or assumed to be :math:`\\text{V}` otherwise. :type: `float` or `~pint.Quantity` """, ) current = unitful_property( "ISET", u.amp, set_fmt="{} {:.1f}", output_decoration=float, doc=""" Gets/sets the current of the specified channel. If the device is in constant voltage mode, this sets the current limit. Note there is no bounds checking on the value specified. :units: As specified, or assumed to be :math:`\\text{A}` otherwise. :type: `float` or `~pint.Quantity` """, ) voltage_sense = unitful_property( "VOUT", 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: `~pint.Quantity` """, ) current_sense = unitful_property( "IOUT", 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: `~pint.Quantity` """, ) overvoltage = unitful_property( "OVSET", u.volt, set_fmt="{} {:.1f}", output_decoration=float, doc=""" Gets/sets the overvoltage protection setting for the specified channel. Note there is no bounds checking on the value specified. :units: As specified, or assumed to be :math:`\\text{V}` otherwise. :type: `float` or `~pint.Quantity` """, ) overcurrent = bool_property( "OVP", inst_true="1", inst_false="0", doc=""" Gets/sets the overcurrent protection setting for the specified channel. This is a toggle setting. It is either on or off. :type: `bool` """, ) output = bool_property( "OUT", inst_true="1", inst_false="0", doc=""" Gets/sets the outputting status of the specified channel. This is a toggle setting. True will turn on the channel output while False will turn it off. :type: `bool` """, ) # METHODS ## def reset(self): """ Reset overvoltage and overcurrent errors to resume operation. """ self.sendcmd("OVRST") self.sendcmd("OCRST") # ENUMS # class Mode(Enum): """ Enum holding typical valid output modes for a power supply. However, for the HP6624a I believe that it is only capable of constant-voltage output, so this class current does not do anything and is just a placeholder. """ voltage = 0 current = 0 # PROPERTIES ## @property def channel(self): """ Gets a specific channel object. The desired channel is specified like one would access a list. :rtype: `HP6624a.Channel` .. seealso:: `HP6624a` for example using this property. """ return ProxyList(self, HP6624a.Channel, range(self.channel_count)) @property def voltage(self): """ Gets/sets the voltage for all four channels. :units: As specified (if a `~pint.Quantity`) or assumed to be of units Volts. :type: `tuple`[`~pint.Quantity`, ...] with units Volt """ 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) ) for i in range(self.channel_count): self.channel[i].voltage = newval[i] else: for i in range(self.channel_count): self.channel[i].voltage = newval @property def current(self): """ Gets/sets the current for all four channels. :units: As specified (if a `~pint.Quantity`) or assumed to be of units Amps. :type: `tuple`[`~pint.Quantity`, ...] with units Amp """ 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) ) for i in range(self.channel_count): self.channel[i].current = newval[i] else: for i in range(self.channel_count): self.channel[i].current = newval @property def voltage_sense(self): """ Gets the actual voltage as measured by the sense wires for all channels. :units: :math:`\\text{V}` (volts) :rtype: `tuple`[`~pint.Quantity`, ...] """ return tuple(self.channel[i].voltage_sense for i in range(self.channel_count)) @property def current_sense(self): """ Gets the actual current as measured by the instrument for all channels. :units: :math:`\\text{A}` (amps) :rtype: `tuple`[`~pint.Quantity`, ...] """ return tuple(self.channel[i].current_sense for i in range(self.channel_count)) @property def channel_count(self): """ Gets/sets the number of output channels available for the connected power supply. :type: `int` """ return self._channel_count @channel_count.setter def channel_count(self, newval): if not isinstance(newval, int): raise TypeError("Channel count must be specified as an integer.") if newval < 1: raise ValueError("Channel count must be >=1") self._channel_count = newval # METHODS ## def clear(self): """ Taken from the manual: Return the power supply to its power-on state and all parameters are returned to their initial power-on values except the following: #) The store/recall registers are not cleared. #) The power supply remains addressed to listen. #) The PON bit in the serial poll register is cleared. """ self.sendcmd("CLR") ================================================ FILE: src/instruments/hp/hp6632b.py ================================================ #!/usr/bin/env python # # hp6632b.py: Python class for the HP6632b power supply # # © 2014 Willem Dijkstra (wpd@xs4all.nl). # # 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 HP6632b DC power supply Originally contributed and copyright held by Willem Dijkstra (wpd@xs4all.nl) An unrestricted license has been provided to the maintainers of the Instrument Kit project. """ # IMPORTS ##################################################################### from enum import Enum, IntEnum from instruments.generic_scpi.scpi_instrument import SCPIInstrument from instruments.hp.hp6652a import HP6652a from instruments.units import ureg as u from instruments.util_fns import ( unitful_property, unitless_property, bool_property, enum_property, int_property, ) # CLASSES ##################################################################### class HP6632b(SCPIInstrument, HP6652a): """ The HP6632b is a system dc power supply with an output rating of 0-20V/0-5A, precision low current measurement and low output noise. According to the manual this class MIGHT be usable for any HP power supply with a model number - HP663Xb with X in {1, 2, 3, 4}, - HP661Xc with X in {1,2, 3, 4} and - HP663X2A for X in {1, 3}, without the additional measurement capabilities. HOWEVER, it has only been tested by the author with HP6632b supplies. Example usage: >>> import instruments as ik >>> psu = ik.hp.HP6632b.open_gpibusb('/dev/ttyUSB0', 6) >>> psu.voltage = 10 # Sets voltage to 10V. >>> psu.output = True # Enable output >>> psu.voltage array(10.0) * V >>> psu.voltage_trigger = 20 # Set transient trigger voltage >>> psu.init_output_trigger() # Prime instrument to initiated state, ready for trigger >>> psu.trigger() # Send trigger >>> psu.voltage array(10.0) * V """ # ENUMS ## class ALCBandwidth(IntEnum): """ Enum containing valid ALC bandwidth modes for the hp6632b """ normal = 1.5e4 fast = 6e4 class DigitalFunction(Enum): """ Enum containing valid digital function modes for the hp6632b """ 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" class ErrorCodes(IntEnum): """ Enum containing generic-SCPI error codes along with codes specific to the HP6632b. """ no_error = 0 # -100 BLOCK: COMMAND ERRORS ## command_error = -100 invalid_character = -101 syntax_error = -102 invalid_separator = -103 data_type_error = -104 get_not_allowed = -105 # -106 and -107 not specified. parameter_not_allowed = -108 missing_parameter = -109 command_header_error = -110 header_separator_error = -111 program_mnemonic_too_long = -112 undefined_header = -113 header_suffix_out_of_range = -114 unexpected_number_of_parameters = -115 numeric_data_error = -120 invalid_character_in_number = -121 exponent_too_large = -123 too_many_digits = -124 numeric_data_not_allowed = -128 suffix_error = -130 invalid_suffix = -131 suffix_too_long = -134 suffix_not_allowed = -138 character_data_error = -140 invalid_character_data = -141 character_data_too_long = -144 character_data_not_allowed = -148 string_data_error = -150 invalid_string_data = -151 string_data_not_allowed = -158 block_data_error = -160 invalid_block_data = -161 block_data_not_allowed = -168 expression_error = -170 invalid_expression = -171 expression_not_allowed = -178 macro_error_180 = -180 invalid_outside_macro_definition = -181 invalid_inside_macro_definition = -183 macro_parameter_error = -184 # -200 BLOCK: EXECUTION ERRORS ## # -300 BLOCK: DEVICE-SPECIFIC ERRORS ## # Note that device-specific errors also include all positive numbers. # -400 BLOCK: QUERY ERRORS ## # OTHER ERRORS ## #: Raised when the instrument detects that it has been turned from #: off to on. power_on = -500 # Yes, SCPI 1999 defines the instrument turning on as # an error. Yes, this makes my brain hurt. user_request_event = -600 request_control_event = -700 operation_complete = -800 # -200 BLOCK: EXECUTION ERRORS execution_error = -200 data_out_of_range = -222 too_much_data = -223 illegal_parameter_value = -224 out_of_memory = -225 macro_error_270 = -270 macro_execution_error = -272 illegal_macro_label = -273 macro_recursion_error = -276 macro_redefinition_not_allowed = -277 # -300 BLOCK: DEVICE-SPECIFIC ERRORS system_error = -310 too_many_errors = -350 # -400 BLOCK: QUERY ERRORS query_error = -400 query_interrupted = -410 query_unterminated = -420 query_deadlocked = -430 query_unterminated_after_indefinite_response = -440 # DEVICE ERRORS ram_rd0_checksum_failed = 1 ram_config_checksum_failed = 2 ram_cal_checksum_failed = 3 ram_state_checksum_failed = 4 ram_rst_checksum_failed = 5 ram_selftest = 10 vdac_idac_selftest1 = 11 vdac_idac_selftest2 = 12 vdac_idac_selftest3 = 13 vdac_idac_selftest4 = 14 ovdac_selftest = 15 digital_io_selftest = 80 ingrd_recv_buffer_overrun = 213 rs232_recv_framing_error = 216 rs232_recv_parity_error = 217 rs232_recv_overrun_error = 218 front_panel_uart_overrun = 220 front_panel_uart_framing = 221 front_panel_uart_parity = 222 front_panel_uart_buffer_overrun = 223 front_panel_uart_timeout = 224 cal_switch_prevents_cal = 401 cal_password_incorrect = 402 cal_not_enabled = 403 computed_readback_cal_const_incorrect = 404 computed_prog_cal_constants_incorrect = 405 incorrect_seq_cal_commands = 406 cv_or_cc_status_incorrect = 407 output_mode_must_be_normal = 408 too_many_sweep_points = 601 command_only_applic_rs232 = 602 curr_or_volt_fetch_incompat_with_last_acq = 603 measurement_overrange = 604 class RemoteInhibit(Enum): """ Enum containing vlaid remote inhibit modes for the hp6632b. """ latching = "LATC" live = "LIVE" off = "OFF" class SenseWindow(Enum): """ Enum containing valid sense window modes for the hp6632b. """ hanning = "HANN" rectangular = "RECT" # PROPERTIES ## voltage_alc_bandwidth = enum_property( "VOLT:ALC:BAND", ALCBandwidth, input_decoration=lambda x: int(float(x)), readonly=True, doc=""" Get the "automatic level control bandwidth" which for the HP66332A and HP6631-6634 determines if the output capacitor is in circuit. `Normal` denotes that it is, and `Fast` denotes that it is not. :type: `~HP6632b.ALCBandwidth` """, ) voltage_trigger = unitful_property( "VOLT:TRIG", 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 `~pint.Quantity` """, ) current_trigger = unitful_property( "CURR:TRIG", 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 `~pint.Quantity` """, ) init_output_continuous = bool_property( "INIT:CONT:SEQ1", "1", "0", doc=""" Get/set the continuous output trigger. In this state, the power supply will remain in the initiated state, and respond continuously on new incoming triggers by applying the set voltage and current trigger levels. :type: `bool` """, ) current_sense_range = unitful_property( "SENS:CURR:RANGE", u.ampere, doc=""" Get/set the sense current range by the current max value. A current of 20mA or less selects the low-current range, a current value higher than that selects the high-current range. The low current range increases the low current measurement sensitivity and accuracy. :units: As specified, or assumed to be :math:`\\text{A}` otherwise. :type: `float` or `~pint.Quantity` """, ) output_dfi = bool_property( "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 panel FLT connection, that can be used to signal external devices when a fault is detected. :type: `bool` """, ) output_dfi_source = enum_property( "OUTP:DFI:SOUR", DFISource, doc=""" Get/set the source for discrete fault indicator (DFI) events. :type: `~HP6632b.DFISource` """, ) output_remote_inhibit = enum_property( "OUTP:RI:MODE", RemoteInhibit, doc=""" Get/set the remote inhibit signal. Remote inhibit is an external, chassis-referenced logic signal routed through the rear panel INH connection, which allows an external device to signal a fault. :type: `~HP6632b.RemoteInhibit` """, ) digital_function = enum_property( "DIG:FUNC", DigitalFunction, doc=""" Get/set the inhibit+fault port to digital in+out or vice-versa. :type: `~HP6632b.DigitalFunction` """, ) digital_data = int_property( "DIG:DATA", valid_set=range(0, 8), doc=""" Get/set digital in+out port to data. Data can be an integer from 0-7. :type: `int` """, ) sense_sweep_points = unitless_property( "SENS:SWE:POIN", doc=""" Get/set the number of points in a measurement sweep. :type: `int` """, ) sense_sweep_interval = unitful_property( "SENS:SWE:TINT", 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 `~pint.Quantity` """, ) sense_window = enum_property( "SENS:WIND", SenseWindow, doc=""" Get/set the measurement window function. :type: `~HP6632b.SenseWindow` """, ) output_protection_delay = unitful_property( "OUTP:PROT:DEL", 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 the Operation Status Condition register. This command also delays over current protection, but not overvoltage protection. :units: As specified, or assumed to be :math:`\\text{s}` otherwise. :type: `float` or `~pint.Quantity` """, ) # FUNCTIONS ## 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") def abort_output_trigger(self): """ Set the output trigger system to the idle state. """ self.sendcmd("ABORT") # SCPIInstrument commands that need local overrides @property def line_frequency(self): raise NotImplementedError @line_frequency.setter def line_frequency(self, newval): raise NotImplementedError @property def display_brightness(self): raise NotImplementedError @display_brightness.setter def display_brightness(self, newval): raise NotImplementedError @property def display_contrast(self): raise NotImplementedError @display_contrast.setter def display_contrast(self, newval): raise NotImplementedError def check_error_queue(self): """ Checks and clears the error queue for this device, returning a list of :class:`~SCPIInstrument.ErrorCodes` or `int` elements for each error reported by the connected instrument. """ done = False result = [] while not done: err = int(self.query("SYST:ERR?").split(",")[0]) if err == self.ErrorCodes.no_error: done = True else: result.append( self.ErrorCodes(err) if any(err == item.value for item in self.ErrorCodes) else err ) return result ================================================ FILE: src/instruments/hp/hp6652a.py ================================================ #!/usr/bin/env python """ Driver for the HP6652a single output power supply Originally contributed by Wil Langford (wil.langford+instrumentkit@gmail.com) """ # IMPORTS ##################################################################### from instruments.units import ureg as u from instruments.abstract_instruments import PowerSupply from instruments.util_fns import unitful_property, bool_property # CLASSES ##################################################################### 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 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(?). (e.g. HP6652A and HP6671A) HOWEVER, it has only been tested by the author with an HP6652A power supply. Example usage: >>> import time >>> import instruments as ik >>> psu = ik.hp.HP6652a.open_serial('/dev/ttyUSB0', 57600) >>> psu.voltage = 3 # Sets output voltage to 3V. >>> psu.output = True >>> psu.voltage array(3.0) * V >>> psu.voltage_sense < 5 True >>> psu.output = False >>> psu.voltage_sense < 1 True >>> psu.display_textmode=True >>> psu.display_text("test GOOD") 'TEST GOOD' >>> time.sleep(5) >>> psu.display_textmode=False """ # ENUMS ## # I don't know of any possible enumerations supported # by this instrument. # PROPERTIES ## voltage = unitful_property( "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 `~pint.Quantity` """, ) current = unitful_property( "CURR", 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 `~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: :math:`\\text{V}` (volts) :rtype: `~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: :math:`\\text{A}` (amps) :rtype: `~pint.Quantity` """, ) overvoltage = unitful_property( "VOLT:PROT", 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 `~pint.Quantity` """, ) overcurrent = bool_property( "CURR:PROT:STAT", inst_true="1", inst_false="0", doc=""" Gets/sets the overcurrent protection setting. This is a toggle setting. It is either on or off. :type: `bool` """, ) output = bool_property( "OUTP", inst_true="1", inst_false="0", doc=""" 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` """, ) display_textmode = bool_property( "DISP:MODE", inst_true="TEXT", inst_false="NORM", doc=""" Gets/sets the display mode. This is a toggle setting. True will allow text to be sent to the front-panel LCD with the display_text() method. False returns to the normal display mode. .. seealso:: `~HP6652a.display_text()` :type: `bool` """, ) @property def name(self): """ The name of the connected instrument, as reported by the standard SCPI command ``*IDN?``. :rtype: `str` """ idn_string = self.query("*IDN?") idn_list = idn_string.split(",") return " ".join(idn_list[:2]) @property def mode(self): """ Unimplemented. """ raise NotImplementedError("Setting the mode is not implemented.") @mode.setter def mode(self, newval): """ Unimplemented. """ raise NotImplementedError("Setting the mode is not implemented.") # METHODS ## def reset(self): """ Reset overvoltage and overcurrent errors to resume operation. """ self.sendcmd("OUTP:PROT:CLE") def display_text(self, text_to_display): """ Sends up to 12 (uppercase) alphanumerics to be sent to the front-panel LCD display. Some punctuation is allowed, and can affect the number of characters allowed. See the programming manual for the HP6652A for more details. Because the maximum valid number of possible characters is 15 (counting the possible use of punctuation), the text will be truncated to 15 characters before the command is sent to the instrument. If an invalid string is sent, the command will fail silently. Any lowercase letters in the text_to_display will be converted to uppercase before the command is sent to the instrument. No attempt to validate punctuation is currently made. Because the string cannot be read back from the instrument, this method returns the actual string value sent. :param text_to_display: The text that you wish to have displayed on the front-panel LCD :type text_to_display: 'str' :return: Returns the version of the provided string that will be send to the instrument. This means it will be truncated to a maximum of 15 characters and changed to all upper case. :rtype: `str` """ if len(text_to_display) > 15: text_to_display = text_to_display[:15] text_to_display = text_to_display.upper() self.sendcmd(f'DISP:TEXT "{text_to_display}"') return text_to_display @property def channel(self): """ Return the channel (which in this case is the entire instrument, since there is only 1 channel on the HP6652a.) :rtype: 'tuple' of length 1 containing a reference back to the parent HP6652a object. """ return (self,) ================================================ FILE: src/instruments/hp/hpe3631a.py ================================================ #!/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` """, ) ================================================ FILE: src/instruments/keithley/__init__.py ================================================ #!/usr/bin/env python """ Module containing Keithley instruments """ from .keithley195 import Keithley195 from .keithley485 import Keithley485 from .keithley580 import Keithley580 from .keithley2182 import Keithley2182 from .keithley6220 import Keithley6220 from .keithley6514 import Keithley6514 ================================================ FILE: src/instruments/keithley/keithley195.py ================================================ #!/usr/bin/env python """ Driver for the Keithley 195 digital multimeter """ # IMPORTS ##################################################################### import time import struct from enum import Enum, IntEnum from instruments.abstract_instruments import Multimeter from instruments.units import ureg as u # CLASSES ##################################################################### class Keithley195(Multimeter): """ The Keithley 195 is a 5 1/2 digit auto-ranging digital multimeter. You can find the full specifications list in the `Keithley 195 user's guide`_. Example usage: >>> import instruments as ik >>> import instruments.units as u >>> dmm = ik.keithley.Keithley195.open_gpibusb('/dev/ttyUSB0', 12) >>> print dmm.measure(dmm.Mode.resistance) .. _Keithley 195 user's guide: http://www.keithley.com/data?asset=803 """ def __init__(self, filelike): super().__init__(filelike) self.sendcmd("YX") # Removes the termination CRLF self.sendcmd("G1DX") # Disable returning prefix and suffix # ENUMS ## class Mode(IntEnum): """ Enum containing valid measurement modes for the Keithley 195 """ voltage_dc = 0 voltage_ac = 1 resistance = 2 current_dc = 3 current_ac = 4 class TriggerMode(IntEnum): """ Enum containing valid trigger modes for the Keithley 195 """ talk_continuous = 0 talk_one_shot = 1 get_continuous = 2 get_one_shot = 3 x_continuous = 4 x_one_shot = 5 ext_continuous = 6 ext_one_shot = 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) current_ac = (20e-6, 200e-6, 2e-3, 20e-3, 200e-3, 2, 2) resistance = (20, 200, 2000, 20e3, 200e3, 2e6, 20e6) # PROPERTIES # @property def mode(self): """ Gets/sets the measurement mode for the Keithley 195. The base model only has DC voltage and resistance measurements. In order to use AC voltage, DC current, and AC current measurements your unit must be equiped with option 1950. Example use: >>> import instruments as ik >>> dmm = ik.keithley.Keithley195.open_gpibusb('/dev/ttyUSB0', 12) >>> dmm.mode = dmm.Mode.resistance :type: `Keithley195.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"F{newval.value}DX") @property def trigger_mode(self): """ Gets/sets the trigger mode of the Keithley 195. There are two different trigger settings for four different sources. This means there are eight different settings for the trigger mode. The two types are continuous and one-shot. Continuous has the instrument continuously sample the resistance. One-shot performs a single resistance measurement. 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". On "X" is when one sends the ASCII character "X" to the instrument. This character is used as a general execute to confirm commands send to the instrument. In InstrumentKit, "X" is sent after each command so it is not suggested that one uses on "X" triggering. Last, is external triggering. This is the port on the rear of the instrument. Refer to the manual for electrical characteristics of this port. :type: `Keithley195.TriggerMode` """ 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(f"T{newval.value}X") @property def relative(self): """ Gets/sets the zero command (relative measurement) mode of the Keithley 195. As stated in the manual: The zero mode serves as a means for a baseline suppression. When the correct zero command is send over the bus, the instrument will enter the zero mode, as indicated by the front panel ZERO indicator light. All reading displayed or send over the bus while zero is enabled are the difference between the stored baseline adn the actual voltage level. For example, if a 100mV baseline is stored, 100mV will be subtracted from all subsequent readings as long as the zero mode is enabled. The value of the stored baseline can be as little as a few microvolts or as large as the selected range will permit. See the manual for more information. :type: `bool` """ 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(f"Z{int(newval)}DX") @property def input_range(self): """ Gets/sets the range of the Keithley 195 input terminals. The valid range settings depends on the current mode of the instrument. They are listed as follows: #) 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)`` #) current_ac = ``(20e-6, 200e-6, 2e-3, 20e-3, 200e-3, 2)`` #) resistance = ``(20, 200, 2000, 20e3, 200e3, 2e6, 20e6)`` All modes will also accept the string ``auto`` which will set the 195 into auto ranging mode. :rtype: `~pint.Quantity` or `str` """ index = self.parse_status_word(self.get_status_word())["range"] if index == 0: 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") return else: 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 if isinstance(newval, (float, int)): if newval in valid: newval = valid.index(newval) + 1 else: 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(f"R{newval}DX") # METHODS # def measure(self, mode=None): """ Instruct the Keithley 195 to perform a one time measurement. The instrument will use default parameters for the requested measurement. The measurement will immediately take place, and the results are directly sent to the instrument's output buffer. Method returns a Python quantity consisting of a numpy array with the instrument value and appropriate units. With the 195, it is HIGHLY recommended that you seperately set the mode and let the instrument settle into the new mode. This can sometimes take longer than the 2 second delay added in this method. In our testing the 2 seconds seems to be sufficient but we offer no guarentee. Example usage: >>> import instruments as ik >>> import instruments.units as u >>> dmm = ik.keithley.Keithley195.open_gpibusb('/dev/ttyUSB0', 12) >>> print(dmm.measure(dmm.Mode.resistance)) :param mode: Desired measurement mode. This must always be specified in order to provide the correct return units. :type mode: `Keithley195.Mode` :return: A measurement from the multimeter. :rtype: `~pint.Quantity` """ if mode is not None: current_mode = self.mode if mode != current_mode: self.mode = mode time.sleep(2) # Gives the instrument a moment to settle else: mode = self.mode value = self.query("") return float(value) * UNITS2[mode] def get_status_word(self): """ Retreive the status word from the instrument. This contains information regarding the various settings of the instrument. The function `~Keithley195.parse_status_word` is designed to parse the return string from this function. :return: String containing setting information of the instrument :rtype: `str` """ self.sendcmd("U0DX") return self._file.read_raw() @staticmethod def parse_status_word(statusword): # pylint: disable=too-many-locals """ Parse the status word returned by the function `~Keithley195.get_status_word`. Returns a `dict` with the following keys: ``{trigger,mode,range,eoi,buffer,rate,srqmode,relative,delay,multiplex, selftest,dataformat,datacontrol,filter,terminator}`` :param statusword: Byte string to be unpacked and parsed :type: `str` :return: A parsed version of the status word as a Python dictionary :rtype: `dict` """ 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): """ Tell the Keithley 195 to execute all commands that it has received. Do note that this is different from the standard SCPI ``*TRG`` command (which is not supported by the 195 anyways). """ self.sendcmd("X") def auto_range(self): """ Turn on auto range for the Keithley 195. This is the same as calling ``Keithley195.input_range = 'auto'`` """ self.input_range = "auto" # UNITS ####################################################################### UNITS = { "DCV": u.volt, "ACV": u.volt, "ACA": u.amp, "DCA": u.amp, "OHM": u.ohm, } UNITS2 = { 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, } ================================================ FILE: src/instruments/keithley/keithley2182.py ================================================ #!/usr/bin/env python """ Driver for the Keithley 2182 nano-voltmeter """ # IMPORTS ##################################################################### from enum import Enum 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 ##################################################################### class Keithley2182(SCPIMultimeter): """ The Keithley 2182 is a nano-voltmeter. You can find the full specifications list in the `user's guide`_. Example usage: >>> import instruments as ik >>> meter = ik.keithley.Keithley2182.open_gpibusb("/dev/ttyUSB0", 10) >>> print(meter.measure(meter.Mode.voltage_dc)) """ # INNER CLASSES # class Channel(Multimeter): """ Class representing a channel on the Keithley 2182 nano-voltmeter. .. warning:: This class should NOT be manually created by the user. It is designed to be initialized by the `Keithley2182` class. """ # pylint: disable=super-init-not-called def __init__(self, parent, idx): self._parent = parent self._idx = idx + 1 # PROPERTIES # @property def mode(self): return Keithley2182.Mode(self._parent.query("SENS:FUNC?")) @mode.setter def mode(self, newval): raise NotImplementedError @property def trigger_mode(self): raise NotImplementedError @trigger_mode.setter def trigger_mode(self, newval): raise NotImplementedError @property def relative(self): raise NotImplementedError @relative.setter def relative(self, newval): raise NotImplementedError @property def input_range(self): raise NotImplementedError @input_range.setter def input_range(self, newval): raise NotImplementedError # METHODS # def measure(self, mode=None): """ Performs a measurement of the specified channel. If no mode parameter is specified then the current mode is used. :param mode: Mode that the measurement will be performed in :type mode: Keithley2182.Mode :return: The value of the measurement :rtype: `~pint.Quantity` """ if mode is not None: # self.mode = mode raise NotImplementedError self._parent.sendcmd(f"SENS:CHAN {self._idx}") value = float(self._parent.query("SENS:DATA:FRES?")) unit = self._parent.units return u.Quantity(value, unit) # ENUMS # class Mode(Enum): """ Enum containing valid measurement modes for the Keithley 2182 """ voltage_dc = "VOLT" temperature = "TEMP" class TriggerMode(Enum): """ Enum containing valid trigger modes for the Keithley 2182 """ immediate = "IMM" external = "EXT" bus = "BUS" timer = "TIM" manual = "MAN" # PROPERTIES # @property def channel(self): """ Gets a specific Keithley 2182 channel object. The desired channel is specified like one would access a list. Although not default, the 2182 has up to two channels. For example, the following would print the measurement from channel 1: >>> meter = ik.keithley.Keithley2182.open_gpibusb("/dev/ttyUSB0", 10) >>> print meter.channel[0].measure() :rtype: `Keithley2182.Channel` """ return ProxyList(self, Keithley2182.Channel, range(2)) @property def relative(self): """ Gets/sets the relative measurement function of the Keithley 2182. This is used to enable or disable the relative function for the currently set mode. When enabling, the current reading is used as a baseline which is subtracted from future measurements. If relative is already on, the stored value is refreshed with the currently read value. See the manual for more information. :type: `bool` """ mode = self.channel[0].mode return self.query(f"SENS:{mode.value}:CHAN1:REF:STAT?") == "ON" @relative.setter def relative(self, newval): if not isinstance(newval, bool): raise TypeError("Relative mode must be a boolean.") mode = self.channel[0].mode if self.relative: self.sendcmd(f"SENS:{mode.value}:CHAN1:REF:ACQ") else: newval = "ON" if newval is True else "OFF" self.sendcmd(f"SENS:{mode.value}:CHAN1:REF:STAT {newval}") @property def input_range(self): raise NotImplementedError @input_range.setter def input_range(self, newval): raise NotImplementedError @property def units(self): """ Gets the current measurement units of the instrument. :rtype: `~pint.Unit` """ mode = self.channel[0].mode if mode == Keithley2182.Mode.voltage_dc: return u.volt unit = self.query("UNIT:TEMP?") if unit == "C": unit = u.celsius elif unit == "K": unit = u.kelvin elif unit == "F": unit = u.fahrenheit else: raise ValueError("Unknown temperature units.") return unit # METHODS # def fetch(self): """ Transfer readings from instrument memory to the output buffer, and thus to the computer. If currently taking a reading, the instrument will wait until it is complete before executing this command. Readings are NOT erased from memory when using fetch. Use the ``R?`` command to read and erase data. Note that the data is transfered as ASCII, and thus it is not recommended to transfer a large number of data points using GPIB. :return: Measurement readings from the instrument output buffer. :rtype: `tuple`[`~pint.Quantity`, ...] or if numpy is installed, `~pint.Quantity` with `numpy.array` data """ 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): """ Perform and transfer a measurement of the desired type. :param mode: Desired measurement mode. If left at default the measurement will occur with the current mode. :type: `Keithley2182.Mode` :return: Returns a single shot measurement of the specified mode. :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(f"MEAS:{mode.value}?")) unit = self.units return value * unit ================================================ FILE: src/instruments/keithley/keithley485.py ================================================ #!/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 ================================================ FILE: src/instruments/keithley/keithley580.py ================================================ #!/usr/bin/env python # # keithley580.py: Driver for the Keithley 580 micro-ohmmeter. # # © 2013 Willem Dijkstra (wpd@xs4all.nl). # 2014 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 . # """ Driver for the HP6632b DC power supply Originally contributed and copyright held by Willem Dijkstra (wpd@xs4all.nl) An unrestricted license has been provided to the maintainers of the Instrument Kit project. """ # IMPORTS ##################################################################### import time import struct from enum import IntEnum from instruments.units import ureg as u from instruments.abstract_instruments import Instrument # CLASSES ##################################################################### class Keithley580(Instrument): """ The Keithley Model 580 is a 4 1/2 digit resolution autoranging micro-ohmmeter with a +- 20,000 count LCD. It is designed for low resistance measurement requirements from 10uΩ to 200kΩ. The device needs some processing time (manual reports 300-500ms) after a command has been transmitted. """ def __init__(self, filelike): """ Initialise the instrument and remove CRLF line termination """ super().__init__(filelike) self.sendcmd("Y:X") # Removes the termination CRLF characters # ENUMS # class Polarity(IntEnum): """ Enum containing valid polarity modes for the Keithley 580 """ positive = 0 negative = 1 class Drive(IntEnum): """ Enum containing valid drive modes for the Keithley 580 """ pulsed = 0 dc = 1 class TriggerMode(IntEnum): """ Enum containing valid trigger modes for the Keithley 580 """ talk_continuous = 0 talk_one_shot = 1 get_continuous = 2 get_one_shot = 3 trigger_continuous = 4 trigger_one_shot = 5 # PROPERTIES # @property def polarity(self): """ Gets/sets instrument polarity. Example use: >>> import instruments as ik >>> keithley = ik.keithley.Keithley580.open_gpibusb('/dev/ttyUSB0', 1) >>> keithley.polarity = keithley.Polarity.positive :type: `Keithley580.Polarity` """ value = self.parse_status_word(self.get_status_word())["polarity"] if value == "+": return Keithley580.Polarity.positive else: 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) ) self.sendcmd(f"P{newval.value}X") @property def drive(self): """ Gets/sets the instrument drive to either pulsed or DC. Example use: >>> import instruments as ik >>> keithley = ik.keithley.Keithley580.open_gpibusb('/dev/ttyUSB0', 1) >>> keithley.drive = keithley.Drive.pulsed :type: `Keithley580.Drive` """ value = self.parse_status_word(self.get_status_word())["drive"] return Keithley580.Drive[value] @drive.setter 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) ) self.sendcmd(f"D{newval.value}X") @property def dry_circuit_test(self): """ Gets/sets the 'dry circuit test' mode of the Keithley 580. This mode is used to minimize any physical and electrical changes in the contact junction by limiting the maximum source voltage to 20mV. By limiting the voltage, the measuring circuit will leave the resistive surface films built up on the contacts undisturbed. This allows for measurement of the resistance of these films. See the Keithley 580 manual for more information. :type: `bool` """ 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(f"C{int(newval)}X") @property def operate(self): """ Gets/sets the operating mode of the Keithley 580. If set to true, the instrument will be in operate mode, while false sets the instruments into standby mode. :type: `bool` """ 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(f"O{int(newval)}X") @property def relative(self): """ Gets/sets the relative measurement mode of the Keithley 580. 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 resistances 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.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(f"Z{int(newval)}X") @property def trigger_mode(self): """ Gets/sets the trigger mode of the Keithley 580. 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 resistance. One-shot performs a single resistance measurement. 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. This character is used as a general execute to confirm commands send to the instrument. In InstrumentKit, "X" is sent after each command so it is not suggested that one uses on "X" triggering. :type: `Keithley580.TriggerMode` """ raise NotImplementedError @trigger_mode.setter def trigger_mode(self, newval): if isinstance(newval, str): newval = Keithley580.TriggerMode[newval] 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): """ 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: `~pint.Quantity` or `str` """ 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) if isinstance(newval, str): newval = newval.lower() 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, 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") # METHODS # def trigger(self): """ Tell the Keithley 580 to execute all commands that it has received. Do note that this is different from the standard SCPI ``*TRG`` command (which is not supported by the 580 anyways). """ self.sendcmd("X") def auto_range(self): """ Turn on auto range for the Keithley 580. This is the same as calling the `Keithley580.set_resistance_range` method and setting the parameter to "AUTO". """ self.sendcmd("R0X") def set_calibration_value(self, value): """ Sets the calibration value. This is not currently implemented. :param value: Calibration value to write """ # self.write('V+n.nnnnE+nn') raise NotImplementedError("setCalibrationValue not implemented") def store_calibration_constants(self): """ Instructs the instrument to store the calibration constants. This is not currently implemented. """ # self.write('L0X') raise NotImplementedError("storeCalibrationConstants not implemented") def get_status_word(self): """ The keithley will not always respond with the statusword when asked. We use a simple heuristic here: request it up to 5 times, using a 1s delay to allow the keithley some thinking time. :rtype: `str` """ tries = 5 statusword = "" while statusword[:3] != b"580" and tries != 0: tries -= 1 self.sendcmd("U0X") time.sleep(1) self.sendcmd("") statusword = self._file.read_raw() 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 `~Keithley580.get_status_word`. Returns a `dict` with the following keys: ``{drive,polarity,drycircuit,operate,range,relative,eoi,trigger, sqrondata,sqronerror,linefreq,terminator}`` :param statusword: Byte string to be unpacked and parsed :type: `str` :rtype: `dict` """ 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] except: 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): """ Perform a measurement with the Keithley 580. The usual mode parameter is ignored for the Keithley 580 as the only valid mode is resistance. :rtype: `~pint.Quantity` """ self.trigger() self.sendcmd("") return self.parse_measurement(self._file.read_raw()[:-1])["resistance"] @staticmethod def parse_measurement(measurement): """ Parse the measurement string returned by the instrument. Returns a dict with the following keys: ``{status,polarity,drycircuit,drive,resistance}`` :param measurement: String to be unpacked and parsed :type: `str` :rtype: `dict` """ 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) * u.ohm except: raise Exception(f"Cannot parse measurement: {measurement}") return { "status": status, "polarity": polarity, "drycircuit": drycircuit, "drive": drive, "resistance": resistance, } # COMMUNICATOR METHODS # def sendcmd(self, cmd): super().sendcmd(cmd + ":") def query(self, cmd, size=-1): return super().query(cmd + ":", size)[:-1] ================================================ FILE: src/instruments/keithley/keithley6220.py ================================================ #!/usr/bin/env python """ Provides support for the Keithley 6220 constant current supply """ # IMPORTS ##################################################################### from instruments.units import ureg as u from instruments.abstract_instruments import PowerSupply from instruments.generic_scpi import SCPIInstrument from instruments.util_fns import bounded_unitful_property # CLASSES ##################################################################### class Keithley6220(SCPIInstrument, PowerSupply): """ The Keithley 6220 is a single channel constant current supply. Because this is a constant current supply, most features that a regular power supply have are not present on the 6220. Example usage: >>> import instruments.units as u >>> import instruments as ik >>> ccs = ik.keithley.Keithley6220.open_gpibusb("/dev/ttyUSB0", 10) >>> ccs.current = 10 * u.milliamp # Sets current to 10mA >>> ccs.disable() # Turns off the output and sets the current to 0A """ # PROPERTIES ## @property def channel(self): """ For most power supplies, this would return a channel specific object. However, the 6220 only has a single channel, so this function simply returns a tuple containing itself. This is for compatibility reasons if a multichannel supply is replaced with the single-channel 6220. For example, the following commands are the same and both set the current to 10mA: >>> ccs.channel[0].current = 0.01 >>> ccs.current = 0.01 """ 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." ) @voltage.setter def voltage(self, newval): raise NotImplementedError( "The Keithley 6220 does not support voltage " "settings." ) current, current_min, current_max = bounded_unitful_property( "SOUR:CURR", 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 `~pint.Quantity` """, ) # METHODS # def disable(self): """ Set the output current to zero and disable the output. """ self.sendcmd("SOUR:CLE:IMM") ================================================ FILE: src/instruments/keithley/keithley6514.py ================================================ #!/usr/bin/env python """ Provides support for the Keithley 6514 electrometer """ # IMPORTS ##################################################################### from enum import Enum 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 ##################################################################### class Keithley6514(SCPIInstrument, Electrometer): """ The `Keithley 6514`_ is an electrometer capable of doing sensitive current, charge, voltage and resistance measurements. Example usage: >>> import instruments as ik >>> import instruments.units as u >>> dmm = ik.keithley.Keithley6514.open_gpibusb('/dev/ttyUSB0', 12) """ # ENUMS # class Mode(Enum): """ Enum containing valid measurement modes for the Keithley 6514 """ 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" 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" 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, ) resistance = (2e3, 20e3, 200e3, 2e6, 20e6, 200e6, 2e9, 20e9, 200e9) charge = (20e-9, 200e-9, 2e-6, 20e-6) # CONSTANTS # _MODE_UNITS = { Mode.voltage: u.volt, Mode.current: u.amp, Mode.resistance: u.ohm, Mode.charge: u.coulomb, } # PRIVATE METHODS # def _valid_range(self, mode): if mode == self.Mode.voltage: return self.ValidRange.voltage elif mode == self.Mode.current: return self.ValidRange.current elif mode == self.Mode.resistance: return self.ValidRange.resistance elif mode == self.Mode.charge: return self.ValidRange.charge else: 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(","))) reading = vals[0] * self.unit timestamp = vals[1] status = vals[2] return reading, timestamp, status # PROPERTIES # # The mode values have quotes around them for some annoying reason. mode = enum_property( "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", TriggerMode, doc=""" Gets/sets the trigger mode of the Keithley 6514. """, ) arm_source = enum_property( "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", doc=""" Gets/sets the zero checking status of the Keithley 6514. """, ) zero_correct = bool_property( "SYST:ZCOR", inst_true="ON", inst_false="OFF", doc=""" Gets/sets the zero correcting status of the Keithley 6514. """, ) @property def unit(self): return self._MODE_UNITS[self.mode] @property def auto_range(self): """ Gets/sets the auto range setting :type: `bool` """ # pylint: disable=no-member 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")) @property def input_range(self): """ Gets/sets the upper limit of the current range. :type: `~pint.Quantity` """ # pylint: disable=no-member mode = self.mode 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.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(f"{mode.value}:RANGE:UPPER {val:e}") # METHODS ## def auto_config(self, mode): """ This command causes the device to do the following: - Switch to the specified mode - Reset all related controls to default values - Set trigger and arm to the 'immediate' setting - Set arm and trigger counts to 1 - Set trigger delays to 0 - Place unit in idle state - Disable all math calculations - Disable buffer operation - Enable autozero """ self.sendcmd(f"CONF:{mode.value}") def fetch(self): """ Request the latest post-processed readings using the current mode. (So does not issue a trigger) Returns a tuple of the form (reading, timestamp) """ raw = self.query("FETC?") reading, timestamp, _ = self._parse_measurement(raw) return reading, timestamp def read_measurements(self): """ Trigger and acquire readings using the current mode. Returns a tuple of the form (reading, timestamp) """ raw = self.query("READ?") reading, timestamp, _ = self._parse_measurement(raw) return reading, timestamp ================================================ FILE: src/instruments/lakeshore/__init__.py ================================================ #!/usr/bin/env python """ Module containing Lakeshore instruments """ from instruments.lakeshore.lakeshore336 import Lakeshore336 from instruments.lakeshore.lakeshore340 import Lakeshore340 from instruments.lakeshore.lakeshore370 import Lakeshore370 from instruments.lakeshore.lakeshore475 import Lakeshore475 ================================================ FILE: src/instruments/lakeshore/lakeshore336.py ================================================ #!/usr/bin/env python """ Provides support for the Lakeshore Model 336 cryogenic temperature controller. """ # IMPORTS ##################################################################### from instruments.generic_scpi import SCPIInstrument from instruments.units import ureg as u from instruments.util_fns import ProxyList # CLASSES ##################################################################### class Lakeshore336(SCPIInstrument): """ The Lakeshore Model 336 is a multi-sensor cryogenic temperature controller. Example usage: >>> import instruments as ik >>> import instruments.units as u >>> import serial >>> inst = ik.lakeshore.Lakeshore336.open_serial('/dev/ttyUSB0', baud=57600, bytesize=serial.SEVENBITS, parity=serial.PARITY_ODD, stopbits=serial.STOPBITS_ONE) >>> print(inst.sensor[0].temperature) >>> print(inst.sensor[1].temperature) """ # INNER CLASSES ## class Sensor: """ Class representing a sensor attached to the Lakeshore Model 336. .. warning:: This class should NOT be manually created by the user. It is designed to be initialized by the `Lakeshore336` class. """ def __init__(self, parent, idx): _idx_mapper = {0: "A", 1: "B", 2: "C", 3: "D"} self._parent = parent self._idx = _idx_mapper[idx] # PROPERTIES ## @property def temperature(self): """ Gets the temperature of the specified sensor. :units: Kelvin :type: `~pint.Quantity` """ value = self._parent.query(f"KRDG?{self._idx}") return u.Quantity(float(value), u.kelvin) # PROPERTIES ## @property def sensor(self): """ Gets a specific sensor object. The desired sensor is specified like one would access a list. For instance, after opening the connection as described in the overview, this would query the temperature of the first sensor: >>> print(inst.sensor[0].temperature) The Lakeshore 336 supports up to 4 sensors (index 0-3). :rtype: `~Lakeshore336.Sensor` """ return ProxyList(self, Lakeshore336.Sensor, range(4)) ================================================ FILE: src/instruments/lakeshore/lakeshore340.py ================================================ #!/usr/bin/env python """ Provides support for the Lakeshore 340 cryogenic temperature controller. """ # IMPORTS ##################################################################### from instruments.generic_scpi import SCPIInstrument from instruments.units import ureg as u from instruments.util_fns import ProxyList # CLASSES ##################################################################### class Lakeshore340(SCPIInstrument): """ The Lakeshore340 is a multi-sensor cryogenic temperature controller. Example usage: >>> import instruments as ik >>> import instruments.units as u >>> inst = ik.lakeshore.Lakeshore340.open_gpibusb('/dev/ttyUSB0', 1) >>> print(inst.sensor[0].temperature) >>> print(inst.sensor[1].temperature) """ # INNER CLASSES ## class Sensor: """ Class representing a sensor attached to the Lakeshore 340. .. warning:: This class should NOT be manually created by the user. It is designed to be initialized by the `Lakeshore340` class. """ def __init__(self, parent, idx): self._parent = parent self._idx = idx + 1 # PROPERTIES ## @property def temperature(self): """ Gets the temperature of the specified sensor. :units: Kelvin :type: `~pint.Quantity` """ value = self._parent.query(f"KRDG?{self._idx}") return u.Quantity(float(value), u.kelvin) # PROPERTIES ## @property def sensor(self): """ Gets a specific sensor object. The desired sensor is specified like one would access a list. For instance, this would query the temperature of the first sensor:: >>> bridge = Lakeshore340.open_serial("COM5") >>> print(bridge.sensor[0].temperature) The Lakeshore 340 supports up to 2 sensors (index 0-1). :rtype: `~Lakeshore340.Sensor` """ return ProxyList(self, Lakeshore340.Sensor, range(2)) ================================================ FILE: src/instruments/lakeshore/lakeshore370.py ================================================ #!/usr/bin/env python """ Provides support for the Lakeshore 370 AC resistance bridge. """ # IMPORTS ##################################################################### from instruments.generic_scpi import SCPIInstrument from instruments.units import ureg as u from instruments.util_fns import ProxyList # CLASSES ##################################################################### class Lakeshore370(SCPIInstrument): """ The Lakeshore 370 is a multichannel AC resistance bridge for use in low temperature dilution refridgerator setups. Example usage: >>> import instruments as ik >>> bridge = ik.lakeshore.Lakeshore370.open_gpibusb('/dev/ttyUSB0', 1) >>> print(bridge.channel[0].resistance) """ def __init__(self, filelike): super().__init__(filelike) # Disable termination characters and enable EOI self.sendcmd("IEEE 3,0") # INNER CLASSES ## class Channel: """ Class representing a sensor attached to the Lakeshore 370. .. warning:: This class should NOT be manually created by the user. It is designed to be initialized by the `Lakeshore370` class. """ def __init__(self, parent, idx): self._parent = parent self._idx = idx + 1 # PROPERTIES ## @property def resistance(self): """ Gets the resistance of the specified sensor. :units: Ohm :rtype: `~pint.Quantity` """ value = self._parent.query(f"RDGR? {self._idx}") return u.Quantity(float(value), u.ohm) # PROPERTIES ## @property def channel(self): """ Gets a specific channel object. The desired channel is specified like one would access a list. For instance, this would query the resistance of the first channel:: >>> import instruments as ik >>> bridge = ik.lakeshore.Lakeshore370.open_serial("COM5") >>> print(bridge.channel[0].resistance) The Lakeshore 370 supports up to 16 channels (index 0-15). :rtype: `~Lakeshore370.Channel` """ return ProxyList(self, Lakeshore370.Channel, range(16)) ================================================ FILE: src/instruments/lakeshore/lakeshore475.py ================================================ #!/usr/bin/env python """ Provides support for the Lakeshore 475 Gaussmeter. """ # IMPORTS ##################################################################### from enum import IntEnum 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: u.gauss, 2: u.tesla, 3: u.oersted, 4: u.amp / u.meter} LAKESHORE_TEMP_UNITS = {1: u.celsius, 2: u.kelvin} 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 ##################################################################### class Lakeshore475(SCPIInstrument): """ The Lakeshore475 is a DSP Gaussmeter with field ranges from 35mG to 350kG. Example usage: >>> import instruments as ik >>> import instruments.units as u >>> gm = ik.lakeshore.Lakeshore475.open_gpibusb('/dev/ttyUSB0', 1) >>> print(gm.field) >>> gm.field_units = u.tesla >>> gm.field_setpoint = 0.05 * u.tesla """ # ENUMS ## class Mode(IntEnum): """ Enum containing valid measurement modes for the Lakeshore 475 """ dc = 1 rms = 2 peak = 3 class Filter(IntEnum): """ Enum containing valid filter modes for the Lakeshore 475 """ wide = 1 narrow = 2 lowpass = 3 class PeakMode(IntEnum): """ Enum containing valid peak modes for the Lakeshore 475 """ periodic = 1 pulse = 2 class PeakDisplay(IntEnum): """ Enum containing valid peak displays for the Lakeshore 475 """ positive = 1 negative = 2 both = 3 # PROPERTIES ## @property def field(self): """ Read field from connected probe. :type: `~pint.Quantity` """ return float(self.query("RDGFIELD?")) * self.field_units @property def field_units(self): """ Gets/sets the units of the Gaussmeter. Acceptable units are Gauss, Tesla, Oersted, and Amp/meter. :type: `~pint.Unit` """ value = int(self.query("UNIT?")) return LAKESHORE_FIELD_UNITS[value] @field_units.setter def field_units(self, newval): if isinstance(newval, u.Unit): if newval in LAKESHORE_FIELD_UNITS_INV: self.sendcmd(f"UNIT {LAKESHORE_FIELD_UNITS_INV[newval]}") else: raise ValueError("Not an acceptable Python quantities object") else: raise TypeError("Field units must be a Python quantity") @property def temp_units(self): """ Gets/sets the temperature units of the Gaussmeter. Acceptable units are celcius and kelvin. :type: `~pint.Unit` """ value = int(self.query("TUNIT?")) return LAKESHORE_TEMP_UNITS[value] @temp_units.setter def temp_units(self, newval): if isinstance(newval, u.Unit): if newval in LAKESHORE_TEMP_UNITS_INV: self.sendcmd(f"TUNIT {LAKESHORE_TEMP_UNITS_INV[newval]}") else: raise TypeError("Not an acceptable Python quantities object") else: 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 `~pint.Quantity`) or assumed to be of units Gauss. :type: `~pint.Quantity` with units Gauss """ value = self.query("CSETP?").strip() units = self.field_units return float(value) * units @field_setpoint.setter def field_setpoint(self, 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): """ 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 `~pint.Quantity` """ params = self.query("CPARAM?").strip().split(",") params = [float(x) for x in params] 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") 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): """ Gets/sets the P value for the field control ramp. :type: `float` """ return self.field_control_params[0] @p_value.setter def p_value(self, newval): newval = float(newval) values = list(self.field_control_params) values[0] = newval self.field_control_params = tuple(values) @property def i_value(self): """ Gets/sets the I value for the field control ramp. :type: `float` """ return self.field_control_params[1] @i_value.setter def i_value(self, newval): newval = float(newval) values = list(self.field_control_params) values[1] = newval self.field_control_params = tuple(values) @property def ramp_rate(self): """ Gets/sets the ramp rate value for the field control ramp. :units: As specified (if a `~pint.Quantity`) or assumed to be of current field units / minute. :type: `~pint.Quantity` """ return self.field_control_params[2] @ramp_rate.setter def ramp_rate(self, newval): 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) @property def control_slope_limit(self): """ Gets/sets the I value for the field control ramp. :units: As specified (if a `~pint.Quantity`) or assumed to be of units volt / minute. :type: `~pint.Quantity` """ return self.field_control_params[3] @control_slope_limit.setter def control_slope_limit(self, newval): 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( command="CMODE", inst_true="1", inst_false="0", doc=""" Gets/sets the control mode setting. False corresponds to the field control ramp being disables, while True enables the closed loop PI field control. :type: `bool` """, ) # METHODS ## # pylint: disable=too-many-arguments def change_measurement_mode( self, mode, resolution, filter_type, peak_mode, peak_disp ): """ Change the measurement mode of the Gaussmeter. :param mode: The desired measurement mode. :type mode: `Lakeshore475.Mode` :param `int` resolution: Digit resolution of the measured field. One of `{3|4|5}`. :param filter_type: Specify the signal filter used by the instrument. Available types include wide band, narrow band, and low pass. :type filter_type: `Lakeshore475.Filter` :param peak_mode: Peak measurement mode to be used. :type peak_mode: `Lakeshore475.PeakMode` :param peak_disp: Peak display mode to be used. :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)) ) 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)) ) if not isinstance(peak_mode, Lakeshore475.PeakMode): 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( "Peak display type setting must be a " "`Lakeshore475.PeakDisplay` value, got {} " "instead.".format(type(peak_disp)) ) mode = mode.value filter_type = filter_type.value peak_mode = peak_mode.value peak_disp = peak_disp.value # Parse the resolution 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 ) ) ================================================ FILE: src/instruments/mettler_toledo/__init__.py ================================================ #!/usr/bin/env python """ Module containing Mettler Toledo instruments """ from .mt_sics import MTSICS ================================================ FILE: src/instruments/mettler_toledo/mt_sics.py ================================================ #!/usr/bin/env python """ Provides support for the Mettler Toledo balances via Standard Interface Command Set. """ from enum import Enum import warnings from instruments.abstract_instruments import Instrument from instruments.units import ureg as u from instruments.util_fns import assume_units class MTSICS(Instrument): """ Instrument class to communicate with Mettler Toledo balances using the MT-SICS Standared Interface Command Set. Example usage: >>> import instruments as ik >>> inst = ik.mettler_toledo.MTSICS.open_serial('/dev/ttyUSB0', 9600) >>> inst.weight """ class WeightMode(Enum): """ Enum class to select the weight mode. """ stable = False immediately = True def __init__(self, filelike, *args, **kwargs): super().__init__(filelike, *args, **kwargs) self.terminator = "\r\n" self._weight_mode = MTSICS.WeightMode.stable def clear_tare(self): """ Clear the tare value. Example usage: >>> import instruments as ik >>> inst = ik.mettler_toledo.MTSICS.open_serial('/dev/ttyUSB0', 9600) >>> inst.clear_tare() """ _ = self.query("TAC") def reset(self): """ Reset the balance. Example usage: >>> import instruments as ik >>> inst = ik.mettler_toledo.MTSICS.open_serial('/dev/ttyUSB0', 9600) >>> inst.reset() """ _ = self.query("@") def tare(self, immediately=None): """ Tare the balance. The mode is dependent on the weight mode, however, can be overwritten with the keyword `immediately`. :param bool immediately: Tare immediately if True, otherwise wait for stable weight. Example usage: >>> import instruments as ik >>> inst = ik.mettler_toledo.MTSICS.open_serial('/dev/ttyUSB0', 9600) >>> inst.tare() """ if immediately is None: immediately = self.weight_mode.value msg = "TI" if immediately else "T" _ = self.query(msg) def zero(self, immediately=None): """ Zero the balance after stable weight is obtained. Terminates processes such as zero, tare, calibration and testing etc. If the device is in standby mode, it is turned on. This function sets the currently read and the tare value to zero. The mode is dependent on the weight mode, however, can be overwritten with the keyword `immediately`. :param bool immediately: Zero immediately if True, otherwise wait for stable weight. Example usage: >>> import instruments as ik >>> inst = ik.mettler_toledo.MTSICS.open_serial('/dev/ttyUSB0', 9600) >>> inst.zero() """ if immediately is None: immediately = self.weight_mode.value msg = "ZI" if immediately else "Z" _ = self.query(msg) def query(self, cmd, size=-1): """ Query the instrument for a response. Error checking is performed on the response. :param str cmd: The command to send to the instrument. :param int size: Number of bytes to read from the instrument. :return: The response from the instrument. :rtype: str :raises: UserWarning if the balance is in dynamic mode. """ self.sendcmd(cmd) rval = self.read(size) rval = rval.split() # error checking self._general_error_checking(rval[0]) self._cmd_error_checking(rval[1]) # raise warning if balance in dynamic mode if rval[1] == "D": warnings.warn("Balance in dynamic mode.", UserWarning) return rval[2:] def _cmd_error_checking(self, value): """ Check for errors in the query response. :param value: Command specific error code. :return: None :raises: OSError if an error in the command occurred. """ if value == "I": raise OSError("Internal error (e.g. balance not ready yet).") elif value == "L": raise OSError("Logical error (e.g. parameter not allowed).") elif value == "+": raise OSError( "Weigh module or balance is in overload range" "(weighing range exceeded)." ) elif value == "-": raise OSError( "Weigh module or balance is in underload range" "(e.g. weighing pan is not in place)." ) def _general_error_checking(self, value): """ Check for general errors in the query response. :param value: General error code. :return: None :raises: OSError if a general error occurred. """ if value == "ES": raise OSError("Syntax Error.") elif value == "ET": raise OSError("Transmission Error.") elif value == "EL": raise OSError("Logical Error.") @property def mt_sics(self): """ Get MT-SICS level and MT-SICS versions. :return: Level, Version Level 0, Version Level 1, Version Level 2, Version Level 3 Example usage: >>> import instruments as ik >>> inst = ik.mettler_toledo.MTSICS.open_serial('/dev/ttyUSB0', 9600) >>> inst.mt_sics ['1', '1.0', '1.0', '1.0'] """ retval = [it.replace('"', "") for it in self.query("I1")] return retval @property def mt_sics_commands(self): """ Get MT-SICS commands. Please refer to manual for information on the commands. Not all of these commands are currently implemented in this class! :return: List of all implemented MT-SICS levels and commands :rtype: list Example usage: >>> import instruments as ik >>> inst = ik.mettler_toledo.MTSICS.open_serial('/dev/ttyUSB0', 9600) >>> in inst.mt_sics_commands [["0", "I0"], ["1", "D"]] """ timeout = self.timeout self.timeout = u.Quantity(0.1, u.s) retlist = [] self.sendcmd("I0") while True: try: lst = self.read().split() if lst == []: # data stream was empty break retlist.append(lst) except OSError: # communication timed out break self.timeout = timeout av_cmds = [[it[2], it[3].replace('"', "")] for it in retlist] return av_cmds @property def name(self): """Get / Set balance name. A maximum of 20 characters can be entered. :raises ValueError: If name is longer than 20 characters. Example usage: >>> import instruments as ik >>> inst = ik.mettler_toledo.MTSICS.open_serial('/dev/ttyUSB0', 9600) >>> inst.name = "My Balance" >>> inst.name 'My Balance' """ retval = " ".join(self.query("I10")) return retval.replace('"', "") @name.setter def name(self, value): if len(value) > 20: raise ValueError("Name must be 20 characters or less.") _ = self.query(f'I10 "{value}"') @property def serial_number(self): """ Get the serial number of the balance. :return: The serial number of the balance. :rtype: str Example usage: >>> import instruments as ik >>> inst = ik.mettler_toledo.MTSICS.open_serial('/dev/ttyUSB0', 9600) >>> inst.serial_number '123456789' """ return self.query("I4")[0].replace('"', "") @property def tare_value(self): """Get / set the tare value. If no unit is given, grams are assumed. Example usage: >>> import instruments as ik >>> inst = ik.mettler_toledo.MTSICS.open_serial('/dev/ttyUSB0', 9600) >>> inst.tare_value = 1.0 >>> inst.tare_value """ retval = self.query("TA") return u.Quantity(float(retval[0]), retval[1]) @tare_value.setter def tare_value(self, value): value = assume_units(value, u.gram) value = value.to(u.gram) _ = self.query(f"TA {value.magnitude} g") @property def weight(self): """ Get the weight. If you want to get the immediate (maybe unstable) weight, plese set the weight mode accordingly. :return: Weight :rtype: u.Quantity Example usage: >>> import instruments as ik >>> inst = ik.mettler_toledo.MTSICS.open_serial('/dev/ttyUSB0', 9600) >>> inst.weight """ msg = "SI" if self.weight_mode.value else "S" retval = self.query(msg) return u.Quantity(float(retval[0]), retval[1]) @property def weight_mode(self): """Get/set the weight mode. By default, it starts in ``MTSICS.WeightMode.stable``. :return: Weight mode :rtype: MTSICS.WeightMode :raises TypeError: Weight mode is not of type ``MTSICS.WeightMode`` Example usage: >>> import instruments as ik >>> inst = ik.mettler_toledo.MTSICS.open_serial('/dev/ttyUSB0', 9600) >>> inst.weight_mode = inst.WeightMode.immediately >>> inst.weight_mode """ return self._weight_mode @weight_mode.setter def weight_mode(self, value): if not isinstance(value, MTSICS.WeightMode): raise TypeError("Weight mode must be of type `MTSICS.WeightMode") self._weight_mode = value ================================================ FILE: src/instruments/minghe/__init__.py ================================================ #!/usr/bin/env python """ Module containing MingHe instruments """ from .mhs5200a import MHS5200 ================================================ FILE: src/instruments/minghe/mhs5200a.py ================================================ #!/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().__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() ================================================ FILE: src/instruments/named_struct.py ================================================ #!/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() ] ), ) ================================================ FILE: src/instruments/newport/__init__.py ================================================ #!/usr/bin/env python """ Module containing Newport instruments """ from .agilis import AGUC2 from .errors import NewportError from .newportesp301 import NewportESP301 from .newport_pmc8742 import PicoMotorController8742 ================================================ FILE: src/instruments/newport/agilis.py ================================================ #!/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." ================================================ FILE: src/instruments/newport/errors.py ================================================ #!/usr/bin/env python """ Provides common error handling for Newport devices. """ # IMPORTS #################################################################### import datetime # CLASSES #################################################################### 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", } def __init__(self, errcode=None, timestamp=None): if timestamp is None: self._timestamp = datetime.datetime.now() - NewportError.start_time else: self._timestamp = datetime.datetime.now() - timestamp if errcode is not None: # Break the error code into an axis number # and the rest of the code. self._errcode = int(errcode) % 100 self._axis = errcode // 100 if self._axis == 0: self._axis = None error_message = self.get_message(str(errcode)) error = "Newport Error: {}. Error Message: {}. " "At time : {}".format( str(errcode), error_message, self._timestamp ) super().__init__(error) else: 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().__init__("") # PRIVATE METHODS ## @staticmethod def get_message(code): """ Returns the error string for a given error code :param str code: Error code as returned by instrument :return: Full error code string :rtype: `str` """ return NewportError.messageDict.get(code, "Error code not recognised") # PROPERTIES ## @property def timestamp(self): """ Geturns the timestamp reported by the device as the time at which this error occured. :type: `datetime` """ return self._timestamp @property def errcode(self): """ Gets the error code reported by the device. :type: `int` """ return self._errcode @property def axis(self): """ Gets the axis with which this error is concerned, or `None` if the error was not associated with any particlar axis. :type: `int` """ return self._axis ================================================ FILE: src/instruments/newport/newport_pmc8742.py ================================================ #!/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 afterward 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 afterward 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 ================================================ FILE: src/instruments/newport/newportesp301.py ================================================ #!/usr/bin/env python """ Provides support for the Newport ESP-301 motor controller. Due to the complexity of this piece of equipment, and relative lack of documentation and following of normal SCPI guidelines, this file more than likely contains bugs and non-complete behaviour. """ # IMPORTS ##################################################################### from contextlib import contextmanager from enum import IntEnum 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 # CLASSES ##################################################################### # pylint: disable=too-many-lines class NewportESP301(Instrument): """ Handles communication with the Newport ESP-301 multiple-axis motor controller using the protocol documented in the `user's guide`_. Due to the complexity of this piece of equipment, and relative lack of documentation and following of normal SCPI guidelines, this class more than likely contains bugs and non-complete behaviour. .. _user's guide: http://assets.newport.com/webDocuments-EN/images/14294.pdf """ def __init__(self, filelike): super().__init__(filelike) self._execute_immediately = True self._command_list = [] self._bulk_query_resp = "" self.terminator = "\r" 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): """ Gets the axes of the motor controller as a sequence. For instance, to move along a given axis:: >>> controller = NewportESP301.open_serial("COM3") >>> controller.axis[0].move(-0.001, absolute=False) Note that the axes are numbered starting from zero, so that Python idioms can be used more easily. This is not the same convention used in the Newport ESP-301 user's manual, and so care must be taken when converting examples. :type: :class:`NewportESP301.Axis` """ return ProxyList(self, self.Axis, range(100)) # return _AxisList(self) # LOW-LEVEL COMMAND METHODS ## def _newport_cmd(self, cmd, params=tuple(), target=None, errcheck=True): """ The Newport ESP-301 command set supports checking for errors, specifying different axes and allows for multiple parameters. As such, it is convienent to wrap calls to the low-level `~instruments.abstract_instruments.Instrument.sendcmd` method in a method that is aware of the eccenticities of the controller. This method sends a command, checks for errors on the device and turns them into exceptions as needed. :param bool errcheck: If `False`, suppresses the standard error checking. Note that since error-checking is unsupported during device programming, ``errcheck`` must be `False` during ``PGM`` mode. """ query_resp = None 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)), ) if self._execute_immediately: query_resp = self._execute_cmd(raw_cmd, errcheck) else: self._command_list.append(raw_cmd) # This works because "return None" is equivalent to "return". return query_resp def _execute_cmd(self, raw_cmd, errcheck=True): """ Takes a string command and executes it on Newport :param str raw_cmd: :param bool errcheck: :return: response of device :rtype: `str` """ query_resp = None if "?" in raw_cmd: query_resp = self.query(raw_cmd) else: self.sendcmd(raw_cmd) if errcheck: err_resp = self.query("TB?") # pylint: disable=unused-variable code, timestamp, msg = err_resp.split(",") code = int(code) if code != 0: raise NewportError(code) return query_resp # SPECIFIC COMMANDS ## def _home(self, axis, search_mode, errcheck=True): """ Private method for searching for home "OR", so that 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) def search_for_home( self, axis=1, search_mode=HomeSearchMode.zero_position_count.value, errcheck=True, ): """ Searches the specified axis for home using the method specified by ``search_mode``. :param int axis: Axis ID for which home should be searched for. This value is 1-based indexing. :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. """ self._home(axis=axis, search_mode=search_mode, errcheck=errcheck) def reset(self): """ Causes the device to perform a hardware reset. Note that this method is only effective if the watchdog timer is enabled by the physical jumpers on the ESP-301. Please see the `user's guide`_ for more information. """ self._newport_cmd("RS", errcheck=False) # USER PROGRAMS ## @contextmanager def define_program(self, program_id): """ Erases any existing programs with a given program ID and instructs the device to record the commands within this ``with`` block to be saved as a program with that ID. For instance: >>> controller = NewportESP301.open_serial("COM3") >>> with controller.define_program(15): ... controller.axis[0].move(0.001, absolute=False) ... >>> controller.run_program(15) :param int program_id: An integer label for the new program. 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)." ) self._newport_cmd("XX", target=program_id) try: self._newport_cmd("EP", target=program_id) yield finally: self._newport_cmd("QP") @contextmanager def execute_bulk_command(self, errcheck=True): """ Context manager to execute multiple commands in a single communication with device Example:: with self.execute_bulk_command(): execute commands as normal... :param bool errcheck: Boolean to check for errors after each command that is sent to the instrument. """ self._execute_immediately = False yield 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 = [] self._execute_immediately = True def run_program(self, program_id): """ Runs a previously defined user program with a given 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) ================================================ FILE: src/instruments/ondax/__init__.py ================================================ #!/usr/bin/env python """ Module containing Ondax Instruments """ from .lm import LM ================================================ FILE: src/instruments/ondax/lm.py ================================================ #!/usr/bin/env python """ Provides the support for the Ondax LM Laser. Class originally contributed by Catherine Holloway. """ # IMPORTS ##################################################################### from enum import IntEnum from instruments.units import ureg as u from instruments.abstract_instruments import Instrument from instruments.util_fns import convert_temperature, assume_units # CLASSES ##################################################################### class LM(Instrument): """ The LM is the Ondax SureLock VHG-stabilized laser diode. The user manual can be found on the `Ondax website`_. .. _Ondax website: http://www.ondax.com/Downloads/SureLock/Compact%20laser%20module%20manual.pdf """ def __init__(self, filelike): super().__init__(filelike) self.terminator = "\r" self.apc = self._AutomaticPowerControl(self) self.acc = self._AutomaticCurrentControl(self) self.tec = self._ThermoElectricCooler(self) self.modulation = self._Modulation(self) self._enabled = None # ENUMS # class Status(IntEnum): """ Enum containing the valid states of the laser """ normal = 1 inner_modulation = 2 power_scan = 3 calibrate = 4 shutdown_current = 5 shutdown_overheat = 6 waiting_stable_temperature = 7 waiting = 8 # INNER CLASSES # class _AutomaticCurrentControl: """ Options and functions related to the laser diode's automatic current control driver. .. 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 @property def target(self): """ Gets the automatic current control target setting. This property is accessed via the `LM.acc` namespace. Example usage: >>> import instruments as ik >>> laser = ik.ondax.LM.open_serial('/dev/ttyUSB0', baud=1234) >>> print(laser.acc.target) :return: Current ACC of the Laser :units: mA :type: `~pint.Quantity` """ response = float(self._parent.query("rstli?")) return response * u.mA @property def enabled(self): """ Get/Set the enabled state of the ACC driver. This property is accessed via the `LM.acc` namespace. Example usage: >>> import instruments as ik >>> laser = ik.ondax.LM.open_serial('/dev/ttyUSB0', baud=1234) >>> print(laser.acc.enabled) >>> laser.acc.enabled = True :type: `bool` """ return self._enabled @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)) ) if newval: self._parent.sendcmd("lcen") else: self._parent.sendcmd("lcdis") self._enabled = newval def on(self): """ Turns on the automatic current control driver. This function is accessed via the `LM.acc` namespace. Example usage: >>> import instruments as ik >>> laser = ik.ondax.LM.open_serial('/dev/ttyUSB0', baud=1234) >>> laser.acc.on() """ self._parent.sendcmd("lcon") def off(self): """ Turn off the automatic current control driver. This function is accessed via the `LM.acc` namespace. Example usage: >>> import instruments as ik >>> laser = ik.ondax.LM.open_serial('/dev/ttyUSB0', baud=1234) >>> laser.acc.off() """ self._parent.sendcmd("lcoff") class _AutomaticPowerControl: """ Options and functions related to the laser diode's automatic power control driver. .. 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 @property def target(self): """ Gets the target laser power of the automatic power control in mW. This property is accessed via the `LM.apc` namespace. Example usage: >>> import instruments as ik >>> laser = ik.ondax.LM.open_serial('/dev/ttyUSB0', baud=1234) >>> print(laser.apc.target) :return: the target laser power :units: mW :type: `~pint.Quantity` """ response = self._parent.query("rslp?") return float(response) * u.mW @property def enabled(self): """ Get/Set the enabled state of the automatic power control driver. This property is accessed via the `LM.apc` namespace. Example usage: >>> import instruments as ik >>> laser = ik.ondax.LM.open_serial('/dev/ttyUSB0', baud=1234) >>> print(laser.apc.enabled) >>> laser.apc.enabled = True :type: `bool` """ return self._enabled @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)) ) if newval: self._parent.sendcmd("len") else: self._parent.sendcmd("ldis") self._enabled = newval def start(self): """ Start the automatic power control scan. This function is accessed via the `LM.apc` namespace. Example usage: >>> import instruments as ik >>> laser = ik.ondax.LM.open_serial('/dev/ttyUSB0', baud=1234) >>> laser.apc.start() """ self._parent.sendcmd("sps") def stop(self): """ Stop the automatic power control scan. This function is accessed via the `LM.apc` namespace. Example usage: >>> import instruments as ik >>> laser = ik.ondax.LM.open_serial('/dev/ttyUSB0', baud=1234) >>> laser.apc.stop() """ self._parent.sendcmd("cps") 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 @property def on_time(self): """ Gets/sets the TTL modulation on time, in milliseconds. This property is accessed via the `LM.modulation` namespace. Example usage: >>> import instruments as ik >>> 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 * u.ms :return: The TTL modulation on time :units: As specified (if a `~pint.Quantity`) or assumed to be of units milliseconds. :type: `~pint.Quantity` """ response = self._parent.query("stsont?") return float(response) * u.ms @on_time.setter def on_time(self, newval): newval = assume_units(newval, u.ms).to(u.ms).magnitude self._parent.sendcmd("stsont:" + str(newval)) @property def off_time(self): """ Gets/sets the TTL modulation off time, in milliseconds. This property is accessed via the `LM.modulation` namespace. Example usage: >>> import instruments as ik >>> 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 * u.ms :return: The TTL modulation off time. :units: As specified (if a `~pint.Quantity`) or assumed to be of units milliseconds. :type: `~pint.Quantity` """ response = self._parent.query("stsofft?") return float(response) * u.ms @off_time.setter def off_time(self, newval): newval = assume_units(newval, u.ms).to(u.ms).magnitude self._parent.sendcmd("stsofft:" + str(newval)) @property def enabled(self): """ Get/Set the TTL modulation output state. This property is accessed via the `LM.modulation` namespace. Example usage: >>> import instruments as ik >>> laser = ik.ondax.LM.open_serial('/dev/ttyUSB0', baud=1234) >>> print(laser.modulation.enabled) >>> laser.modulation.enabled = True :type: `bool` """ return self._enabled @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)) ) if newval: self._parent.sendcmd("stm") else: self._parent.sendcmd("ctm") self._enabled = newval class _ThermoElectricCooler: """ Options and functions relating to the laser diode's thermo electric cooler. .. 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 @property def current(self): """ Gets the thermoelectric cooler current setting. This property is accessed via the `LM.tec` namespace. Example usage: >>> import instruments as ik >>> laser = ik.ondax.LM.open_serial('/dev/ttyUSB0', baud=1234) >>> print(laser.tec.current) :units: mA :type: `~pint.Quantity` """ response = self._parent.query("rti?") return float(response) * u.mA @property def target(self): """ Gets the thermoelectric cooler target temperature. This property is acccessed via the `LM.tec` namespace. Example usage: >>> import instruments as ik >>> laser = ik.ondax.LM.open_serial('/dev/ttyUSB0', baud=1234) >>> print(laser.tec.target) :units: Degrees Celcius :type: `~pint.Quantity` """ response = self._parent.query("rstt?") return u.Quantity(float(response), u.degC) @property def enabled(self): """ Gets/sets the enable state for the thermoelectric cooler. This property is accessed via the `LM.tec` namespace. Example usage: >>> import instruments as ik >>> laser = ik.ondax.LM.open_serial('/dev/ttyUSB0', baud=1234) >>> print(laser.tec.enabled) >>> laser.tec.enabled = True :type: `bool` """ return self._enabled @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)) ) if newval: self._parent.sendcmd("tecon") else: self._parent.sendcmd("tecoff") self._enabled = newval def _ack_expected(self, msg=""): if msg.find("?") > 0: return None return "OK" @property def firmware(self): """ Gets the laser system firmware version. :type: `str` """ response = self.query("rsv?") return response @property def current(self): """ Gets/sets the laser diode current, in mA. :units: As specified (if a `~pint.Quantity`) or assumed to be of units mA. :type: `~pint.Quantity` """ response = self.query("rli?") return float(response) * u.mA @current.setter def current(self, newval): newval = assume_units(newval, u.mA).to(u.mA).magnitude self.sendcmd("slc:" + str(newval)) @property 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 `~pint.Quantity`) or assumed to be of units mA. :type: `~pint.Quantity` """ response = self.query("rlcm?") return float(response) * u.mA @maximum_current.setter def maximum_current(self, newval): newval = assume_units(newval, u.mA).to("mA").magnitude self.sendcmd("smlc:" + str(newval)) @property def power(self): """ Get/Set the laser's optical power in mW. :units: As specified (if a `~pint.Quantity`) or assumed to be of units mW. :rtype: `~pint.Quantity` """ response = self.query("rlp?") return float(response) * u.mW @power.setter def power(self, newval): newval = assume_units(newval, u.mW).to(u.mW).magnitude self.sendcmd("slp:" + str(newval)) @property def serial_number(self): """ Gets the laser controller serial number :type: `str` """ response = self.query("rsn?") return response @property def status(self): """ Read laser controller run status. :type: `LM.Status` """ response = self.query("rlrs?") return self.Status(int(response)) @property def temperature(self): """ Gets/sets laser diode temperature. :units: As specified (if a `~pint.Quantity`) or assumed to be of units degrees celcius. :type: `~pint.Quantity` """ response = self.query("rtt?") return u.Quantity(float(response), u.degC) @temperature.setter def temperature(self, newval): newval = convert_temperature(newval, u.degC).magnitude self.sendcmd("stt:" + str(newval)) @property def enabled(self): """ Gets/sets the laser emission enabled status. :type: `bool` """ return self._enabled @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)) ) if newval: self.sendcmd("lon") else: self.sendcmd("loff") self._enabled = newval def save(self): """ Save current settings in flash memory. """ self.sendcmd("ssc") def reset(self): """ Reset the laser controller. """ self.sendcmd("reset") ================================================ FILE: src/instruments/optional_dep_finder.py ================================================ """ 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 ================================================ FILE: src/instruments/oxford/__init__.py ================================================ #!/usr/bin/env python """ Module containing Oxford instruments """ from .oxforditc503 import OxfordITC503 ================================================ FILE: src/instruments/oxford/oxforditc503.py ================================================ #!/usr/bin/env python """ Provides support for the Oxford ITC 503 temperature controller. """ # IMPORTS ##################################################################### from instruments.abstract_instruments import Instrument from instruments.units import ureg as u from instruments.util_fns import ProxyList # CLASSES ##################################################################### class OxfordITC503(Instrument): """ The Oxford ITC503 is a multi-sensor temperature controller. Example usage:: >>> import instruments as ik >>> itc = ik.oxford.OxfordITC503.open_gpibusb('/dev/ttyUSB0', 1) >>> print(itc.sensor[0].temperature) >>> print(itc.sensor[1].temperature) """ def __init__(self, filelike): super().__init__(filelike) self.terminator = "\r" self.sendcmd("C3") # Enable remote commands # INNER CLASSES # class Sensor: """ Class representing a probe sensor on the Oxford ITC 503. .. warning:: This class should NOT be manually created by the user. It is designed to be initialized by the `OxfordITC503` class. """ def __init__(self, parent, idx): self._parent = parent self._idx = idx + 1 # PROPERTIES # @property def temperature(self): """ Read the temperature of the attached probe to the specified channel. :units: Kelvin :type: `~pint.Quantity` """ value = float(self._parent.query(f"R{self._idx}")[1:]) return u.Quantity(value, u.kelvin) # PROPERTIES # @property def sensor(self): """ Gets a specific sensor object. The desired sensor is specified like one would access a list. For instance, this would query the temperature of the first sensor:: >>> itc = ik.oxford.OxfordITC503.open_gpibusb('/dev/ttyUSB0', 1) >>> print(itc.sensor[0].temperature) :type: `OxfordITC503.Sensor` """ return ProxyList(self, OxfordITC503.Sensor, range(3)) ================================================ FILE: src/instruments/pfeiffer/__init__.py ================================================ #!/usr/bin/env python """ Module containing Pfeiffer instruments """ from .tpg36x import TPG36x ================================================ FILE: src/instruments/pfeiffer/tpg36x.py ================================================ #!/usr/bin/env python """ Driver for the Pfeiffer TPG36x vacumm gauge controller. """ # IMPORTS ##################################################################### from enum import Enum import ipaddress from instruments.abstract_instruments import Instrument from instruments.units import ureg as u from instruments.util_fns import ProxyList # CLASSES ##################################################################### class TPG36x(Instrument): """ The Pfeiffer TPG361/2 is a vacuum gauge controller with one/two channels. By default, the two channel version is intialized. If you have the one channel version (TPG361), set the `number_channels` property to 1. Example usage: >>> import instruments as ik >>> inst = ik.pfeiffer.TPG36x.open_serial("/dev/ttyUSB0", 9600) >>> ch = inst.channel[0] >>> ch.pressure 4.7634 millibar """ def __init__(self, filelike): super().__init__(filelike) self._number_channels = 2 self._defined_cmd = { "ETX": 3, "ENQ": 5, "ACK": 6, "NAK": 21, } self.terminator = "\r\n" class EthernetMode(Enum): """Enum go get/set the ethernet mode of the device when configuring.""" STATIC = 0 DHCP = 1 class Language(Enum): """Enum to get/set the language of the device.""" ENGLISH = 0 GERMAN = 1 FRENCH = 2 class Unit(Enum): """Enum for the pressure units.""" MBAR = 0 TORR = 1 PASCAL = 2 MICRON = 3 HPASCAL = 4 VOLT = 5 class Channel: """ Class representing a sensor attached to the TPG 362. .. warning:: This class should NOT be manually created by the user. It is designed to be initialized by the `TPG36x` class. """ class SensorStatus(Enum): """Enum to get the status of the sensor.""" CANNOT_TURN_ON_OFF = 0 OFF = 1 ON = 2 def __init__(self, parent, chan): if not isinstance(parent, TPG36x): raise TypeError("Don't do that.") self._chan = chan self._parent = parent @property def pressure(self): """ The pressure measured by the channel, returned as a pint.Quantity with the correct units attached (based on instrument settings). This routine also does error checking on the pressure reading and raises an IOError with adequate message if, e.g., no sensor is connected to the channel. :return: Pressure on given channel. :rtype: `u.Quantity` Example: >>> import instruments as ik >>> inst = ik.pfeiffer.TPG36x.open_serial("/dev/ttyUSB0", 9600) >>> ch = inst.channel[0] >>> ch.pressure 4.7634 millibar """ status_msgs = { 0: "OK", 1: "Underrange", 2: "Overrange", 3: "Sensor error", 4: "Sensor off", 5: "No sensor", 6: "Identification error", } raw_str = self._parent.query(f"PR{self._chan + 1}") # ex: "0,+1.7377E+00" status_str, val_str = raw_str.split(",") status = int(status_str) val = float(val_str) if status != 0: raise OSError(status_msgs.get(status, "Unknown error")) current_unit = self._parent.unit return val * u.Quantity(current_unit.name.lower()) @property def status(self): """ Get/set the status of a channel (sensor). :return: The status of the sensor. :rtype: `TPG36x.Channel.SensorStatus` Example: >>> import instruments as ik >>> inst = ik.pfeiffer.TPG36x.open_serial("/dev/ttyUSB0", 9600) >>> ch = inst.channel[0] >>> ch.status SensorStatus.ON """ val = self._parent.query("SEN") val = int(val.split(",")[self._chan]) return self.SensorStatus(val) @status.setter def status(self, value): if not isinstance(value, self.SensorStatus): raise ValueError("The status must be a SensorStatus enum.") if value == self.SensorStatus.CANNOT_TURN_ON_OFF: raise ValueError("You cannot set the status to this value.") status_to_send = [0 for _ in range(self._parent.number_channels)] status_to_send[self._chan] = value.value status_to_send_str = ",".join([str(x) for x in status_to_send]) self._parent.sendcmd(f"SEN,{status_to_send_str}") @property def channel(self): """ Gets a specific channel object. Note that the channel number is pythonic, i.e., the first channel is 0. :rtype: `TPG36x.Channel` Example: >>> import instruments as ik >>> inst = ik.pfeiffer.TPG36x.open_serial("/dev/ttyUSB0", 9600) >>> ch = inst.channel[0] """ return ProxyList(self, self.Channel, range(self._number_channels)) @property def ethernet_configuration(self): """ Get / set the ethernet configuration of the TPG36x. .. note:: If you set the configuration to DHCP, you should simply send `TPG36x.EthernetMode.DHCP` as the sole value. To set it to static, you must provide a list of 4 elements: `[EthernetMode, IP, Subnet, Gateway]`. The types are as follows: `TPG36x.EthernetMode`, `str`, `str`, `str`. :return: List of the current configuration: 0. Configuration enum `TPG36x.EthernetMode` 1. IP address as string (or `ipaddress.ip_address`) 2. Subnet mask as string (or `ipaddress.ip_address`) 3. Gateway as string (or `ipaddress.ip_address`) Example: >>> import instruments as ik >>> inst = ik.pfeiffer.TPG36x.open_serial("/dev/ttyUSB0", 9600) >>> inst.ethernet_configuration = [inst.EthernetMode.STATIC, "192.168.1.42", "255.255.255.0", "192.168.1.1"] >>> inst.ethernet_configuration [, "192.168.1.42", "255.255.255.0", "192.168.1.1"] """ return_list = self.query("ETH").split(",") return_list[0] = self.EthernetMode(int(return_list[0])) return return_list @ethernet_configuration.setter def ethernet_configuration(self, value): if not isinstance(value, list) or len(value) != 4: # check for correct format if value == self.EthernetMode.DHCP: # DHCP is a special case self.sendcmd(f"ETH,{value.value}") return else: raise ValueError( "The ethernet configuration must be a list of 4 elements." ) if not isinstance(value[0], self.EthernetMode): # check for correct type raise ValueError("The first element must be an EthernetMode.") for addr in value[1:]: _ = ipaddress.ip_address(addr) # check for valid IP address self.sendcmd(f"ETH,{value[0].value},{value[1]},{value[2]},{value[3]}") @property def language(self): """ Get/set the language of the TPG36x. :rtype: `TPG36x.Language` Example: >>> import instruments as ik >>> inst = ik.pfeiffer.TPG36x.open_serial("/dev/ttyUSB0", 9600) >>> inst.language Language.ENGLISH """ val = int(self.query("LNG")) return self.Language(val) @language.setter def language(self, value): self.sendcmd(f"LNG,{value.value}") @property def mac_address(self): """ Get the MAC address of the TPG36x. :return: MAC address of the TPG36x. :rtype: str Example: >>> import instruments as ik >>> inst = ik.pfeiffer.TPG36x.open_serial("/dev/ttyUSB0", 9600) >>> inst.mac_address "00:1A:2B:3C:4D:5E" """ return self.query("MAC") @property def name(self): """ Get the name from the TPG36x. :rtype: str Example: >>> import instruments as ik >>> inst = ik.pfeiffer.TPG36x.open_serial("/dev/ttyUSB0", 9600) >>> inst.name "TPG362" """ return self.query("AYT").split(",")[0] @property def number_channels(self): """ The number of channels on the TPG36x. This defaults to two channels. Set this to 1 if you have a one gauge instrument, i.e., a TPG361. :rtype: int Example: >>> import instruments as ik >>> inst = ik.pfeiffer.TPG36x.open_serial("/dev/ttyUSB0", 9600) >>> inst.number_channels 2 """ return self._number_channels @number_channels.setter def number_channels(self, value): if value not in (1, 2): raise ValueError("The TPG36x only supports 1 or 2 channels.") self._number_channels = value @property def pressure(self): """ The pressure measured by the first channel. To select the channel, get a channel first and then call the pressure method on it. :rtype: `pint.Quantity` Example: >>> import instruments as ik >>> inst = ik.pfeiffer.TPG36x.open_serial("/dev/ttyUSB0", 9600) >>> inst.pressure 0.02 * u.mbar """ return self.channel[0].pressure @property def unit(self): """ Get/set the unit of the TPG36x (global to the instrument). :return: The current unit. :rtype: `TPG36x.Unit` Example: >>> import instruments as ik >>> inst = ik.pfeiffer.TPG36x.open_serial("/dev/ttyUSB0", 9600) >>> inst.unit Unit.MBAR """ val = self.query("UNI") val = int(val) return self.Unit(val) @unit.setter def unit(self, new_unit): if isinstance(new_unit, str): new_unit = self.Unit[new_unit.upper()] cmd_val = new_unit.value self.sendcmd(f"UNI,{cmd_val}") def query(self, cmd): """ Query the TPG36x with the enquire command. :return: The response from the TPG36x. """ self.sendcmd(cmd) self.write(chr(self._defined_cmd["ENQ"])) return self.read() def _ack_expected(self, msg=""): return [chr(self._defined_cmd["ACK"])] ================================================ FILE: src/instruments/phasematrix/__init__.py ================================================ #!/usr/bin/env python """ Module containing Phase Matrix instruments """ from .phasematrix_fsw0020 import PhaseMatrixFSW0020 ================================================ FILE: src/instruments/phasematrix/phasematrix_fsw0020.py ================================================ #!/usr/bin/env python """ Provides support for the Phase Matrix FSW0020 signal generator. """ # IMPORTS ##################################################################### from instruments.abstract_instruments.signal_generator import SingleChannelSG from instruments.units import ureg as u from instruments.util_fns import assume_units # CLASSES ##################################################################### class PhaseMatrixFSW0020(SingleChannelSG): """ Communicates with a Phase Matrix FSW-0020 signal generator via the "Native SPI" protocol, supported on all FSW firmware versions. Example:: >>> import instruments as ik >>> import instruments.units as u >>> inst = ik.phasematrix.PhaseMatrixFSW0020.open_serial("/dev/ttyUSB0", baud=115200) >>> inst.frequency = 1 * u.GHz >>> inst.power = 0 * ik.units.dBm # Can omit units and will assume dBm >>> inst.output = True """ def reset(self): r""" Causes the connected signal generator to perform a hardware reset. Note that no commands will be accepted by the generator for at least :math:`5 \mu\text{s}`. """ self.sendcmd("0E.") @property def frequency(self): """ Gets/sets the output frequency of the signal generator. If units are not specified, the frequency is assumed to be in gigahertz (GHz). :type: `~pint.Quantity` :units: frequency, assumed to be 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, 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(f"0C{newval:012X}.") # No return data, so no readline needed. @property def power(self): """ Gets/sets the output power of the signal generator. If units are not specified, the power is assumed to be in decibel-milliwatts (dBm). :type: `~pint.Quantity` :units: log-power, assumed to be dBm """ return u.Quantity((int(self.query("0D."), 16)), u.cBm).to(u.dBm) @power.setter def power(self, newval): # TODO: convert UnitPower Quantity instances to UnitLogPower. # That is, convert [W] to [dBm]. # The Phase Matrix unit speaks in units of centibel-milliwats, # so convert and take the integer part. newval = int(assume_units(newval, u.dBm).to(u.cBm).magnitude) # Command code 0x03, parameter length 2 bytes (4 nybbles) self.sendcmd(f"03{newval:04X}.") @property def phase(self): raise NotImplementedError @phase.setter def phase(self, newval): raise NotImplementedError @property def blanking(self): """ Gets/sets the blanking status of the FSW0020 :type: `bool` """ raise NotImplementedError @blanking.setter def blanking(self, newval): self.sendcmd(f"05{1 if newval else 0:02X}.") @property def ref_output(self): """ Gets/sets the reference output status of the FSW0020 :type: `bool` """ raise NotImplementedError @ref_output.setter def ref_output(self, newval): self.sendcmd(f"08{1 if newval else 0:02X}.") @property def output(self): """ Gets/sets the channel output status of the FSW0020. Setting this property to `True` will turn the output on. :type: `bool` """ raise NotImplementedError @output.setter def output(self, newval): self.sendcmd(f"0F{1 if newval else 0:02X}.") @property def pulse_modulation(self): """ Gets/sets the pulse modulation status of the FSW0020 :type: `bool` """ raise NotImplementedError @pulse_modulation.setter def pulse_modulation(self, newval): self.sendcmd(f"09{1 if newval else 0:02X}.") @property def am_modulation(self): """ Gets/sets the amplitude modulation status of the FSW0020 :type: `bool` """ raise NotImplementedError @am_modulation.setter def am_modulation(self, newval): self.sendcmd(f"0A{1 if newval else 0:02X}.") ================================================ FILE: src/instruments/picowatt/__init__.py ================================================ #!/usr/bin/env python """ Module containing Picowatt instruments """ from .picowattavs47 import PicowattAVS47 ================================================ FILE: src/instruments/picowatt/picowattavs47.py ================================================ #!/usr/bin/env python """ Provides support for the Picowatt AVS 47 resistance bridge """ # IMPORTS ##################################################################### from enum import IntEnum from instruments.generic_scpi import SCPIInstrument from instruments.units import ureg as u from instruments.util_fns import enum_property, bool_property, int_property, ProxyList # CLASSES ##################################################################### class PicowattAVS47(SCPIInstrument): """ The Picowatt AVS 47 is a resistance bridge used to measure the resistance of low-temperature sensors. Example usage: >>> import instruments as ik >>> bridge = ik.picowatt.PicowattAVS47.open_gpibusb('/dev/ttyUSB0', 1) >>> print bridge.sensor[0].resistance """ def __init__(self, filelike): super().__init__(filelike) self.sendcmd("HDR 0") # Disables response headers from replies # INNER CLASSES # class Sensor: """ Class representing a sensor on the PicowattAVS47 .. warning:: This class should NOT be manually created by the user. It is designed to be initialized by the `PicowattAVS47` class. """ def __init__(self, parent, idx): self._parent = parent self._idx = idx # The AVS47 is actually zero-based indexing! Wow! @property def resistance(self): """ Gets the resistance. It first ensures that the next measurement reading is up to date by first sending the "ADC" command. :units: :math:`\\Omega` (ohms) :rtype: `~pint.Quantity` """ # First make sure the mux is on the correct channel if self._parent.mux_channel != self._idx: self._parent.input_source = self._parent.InputSource.ground self._parent.mux_channel = self._idx 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?")) * u.ohm # ENUMS # class InputSource(IntEnum): """ Enum containing valid input source modes for the AVS 47 """ ground = 0 actual = 1 reference = 2 # PROPERTIES # @property def sensor(self): """ Gets a specific sensor object. The desired sensor is specified like one would access a list. :rtype: `~PicowattAVS47.Sensor` .. seealso:: `PicowattAVS47` for an example using this property. """ return ProxyList(self, PicowattAVS47.Sensor, range(8)) remote = bool_property( command="REM", inst_true="1", inst_false="0", doc=""" Gets/sets the remote mode state. Enabling the remote mode allows all settings to be changed by computer interface and locks-out the front panel. :type: `bool` """, ) input_source = enum_property( command="INP", enum=InputSource, input_decoration=int, doc=""" Gets/sets the input source. :type: `PicowattAVS47.InputSource` """, ) mux_channel = int_property( command="MUX", doc=""" Gets/sets the multiplexer sensor number. It is recommended that you ground the input before switching the multiplexer channel. Valid mux channel values are 0 through 7 (inclusive). :type: `int` """, valid_set=range(8), ) excitation = int_property( command="EXC", doc=""" Gets/sets the excitation sensor number. Valid excitation sensor values are 0 through 7 (inclusive). :type: `int` """, valid_set=range(8), ) display = int_property( command="DIS", doc=""" Gets/sets the sensor that is displayed on the front panel. Valid display sensor values are 0 through 7 (inclusive). :type: `int` """, valid_set=range(8), ) ================================================ FILE: src/instruments/qubitekk/__init__.py ================================================ #!/usr/bin/env python """ Module containing Qubitekk instruments """ from .cc1 import CC1 from .mc1 import MC1 ================================================ FILE: src/instruments/qubitekk/cc1.py ================================================ #!/usr/bin/env python """ Provides support for the Qubitekk CC1 Coincidence Counter instrument. CC1 Class originally contributed by Catherine Holloway. """ # IMPORTS ##################################################################### from enum import Enum from instruments.generic_scpi.scpi_instrument import SCPIInstrument from instruments.units import ureg as u from instruments.util_fns import ProxyList, assume_units, split_unit_str # CLASSES ##################################################################### class CC1(SCPIInstrument): """ The CC1 is a hand-held coincidence counter. It has two setting values, the dwell time and the coincidence window. The coincidence window determines the amount of time (in ns) that the two detections may be from each other and still be considered a coincidence. The dwell time is the amount of time that passes before the counter will send the clear signal. More information can be found at : http://www.qubitekk.com """ def __init__(self, filelike): super().__init__(filelike) self.terminator = "\n" self._channel_count = 3 self._firmware = None self._ack_on = False self.sendcmd(":ACKN OF") # a readline is required because if the firmware is prior to 2.2, # the cc1 will respond with 'Unknown Command'. After # 2.2, it will either respond by acknowledging the command (turning # acknowledgements off does not take place until after the current # exchange has been completed), or not acknowledging it (if the # acknowledgements are off). The try/except block is required to # handle the case in which acknowledgements are off. try: self.read(-1) except OSError: pass _ = self.firmware # prime the firmware if self.firmware[0] >= 2 and self.firmware[1] > 1: self._bool = ("ON", "OFF") self._set_fmt = ":{}:{}" self.TriggerMode = self._TriggerModeNew else: self._bool = ("1", "0") self._set_fmt = ":{} {}" 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 ) # ENUMS # class _TriggerModeNew(Enum): """ Enum containing valid trigger modes for the CC1 """ continuous = "MODE CONT" start_stop = "MODE STOP" class _TriggerModeOld(Enum): """ Enum containing valid trigger modes for the CC1 """ continuous = "0" start_stop = "1" # INNER CLASSES # class Channel: """ Class representing a channel on the Qubitekk CC1. """ __CHANNEL_NAMES = {1: "C1", 2: "C2", 3: "CO"} def __init__(self, cc1, idx): self._cc1 = cc1 # 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 # PROPERTIES # @property def count(self): """ Gets the counts of this channel. :rtype: `int` """ count = self._cc1.query(f"COUN:{self._chan}?") tries = 5 try: count = int(count) except ValueError: count = 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 # PROPERTIES # @property def acknowledge(self): """ Gets/sets the acknowledge message state. If True, the CC1 will echo back every command sent, then print the response (either Unable to comply, Unknown command or the response to a query). If False, the CC1 will only print the response. :units: None :type: boolean """ return self._ack_on @acknowledge.setter def acknowledge(self, new_val): if self.firmware[0] >= 2 and self.firmware[1] > 1: if self._ack_on and not new_val: self.sendcmd(":ACKN OF") self._ack_on = False elif not self._ack_on and new_val: self.sendcmd(":ACKN ON") self._ack_on = True else: raise NotImplementedError( "Acknowledge message not implemented in " "this version." ) @property def gate(self): """ Gets/sets the gate enable status :type: `bool` """ return self.query("GATE?").strip() == self._bool[0] @gate.setter def gate(self, newval): if not isinstance(newval, bool): 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]) ) @property def subtract(self): """ Gets/sets the subtract enable status :type: `bool` """ return self.query("SUBT?").strip() == self._bool[0] @subtract.setter def subtract(self, newval): if not isinstance(newval, bool): 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]) ) @property def trigger_mode(self): """ Gets/sets the trigger mode setting for the CC1. This can be set to ``continuous`` or ``start/stop`` modes. :type: `CC1.TriggerMode` """ return self.TriggerMode(self.query("TRIG?").strip()) @trigger_mode.setter def trigger_mode(self, newval): try: # First assume newval is Enum.value newval = self.TriggerMode[newval] except KeyError: # Check if newval is Enum.name instead try: newval = self.TriggerMode(newval) except ValueError: raise ValueError("Enum property new value not in enum.") self.sendcmd(self._set_fmt.format("TRIG", self.TriggerMode(newval).value)) @property def window(self): """ Gets/sets the length of the coincidence window between the two signals. :units: As specified (if a `~pint.Quantity`) or assumed to be of units nanoseconds. :type: `~pint.Quantity` """ 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, 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(f":WIND {new_val_mag}") @property def delay(self): """ Get/sets the delay value (in nanoseconds) on Channel 1. When setting, ``N`` may be ``0, 2, 4, 6, 8, 10, 12, or 14ns``. :rtype: `~pint.Quantity` :return: the delay value """ return u.Quantity(*split_unit_str(self.query("DELA?"), "ns")) @delay.setter def delay(self, new_val): 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))) @property def dwell_time(self): """ Gets/sets the length of time before a clear signal is sent to the counters. :units: As specified (if a `~pint.Quantity`) or assumed to be of units seconds. :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 = u.Quantity(*split_unit_str(self.query("DWEL?"), "s")) if self.firmware[0] <= 2 and self.firmware[1] <= 1: return dwell_time / 1000.0 return dwell_time @dwell_time.setter def dwell_time(self, new_val): 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(f":DWEL {new_val_mag}") @property def firmware(self): """ Gets the firmware version :rtype: `tuple`(Major:`int`, Minor:`int`, Patch`int`) """ # the firmware is assumed not to change while the device is active # firmware is stored locally as it will be gotten often # pylint: disable=no-member if self._firmware is None: while self._firmware is None: self._firmware = self.query("FIRM?") if self._firmware.find("Unknown") >= 0: self._firmware = None else: value = self._firmware.replace("Firmware v", "").split(".") if len(value) < 3: for _ in range(3 - len(value)): value.append(0) value = tuple(map(int, value)) self._firmware = value return self._firmware @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:: >>> cc = ik.qubitekk.CC1.open_serial('COM8', 19200, timeout=1) >>> print(cc.channel[0].count) :rtype: `CC1.Channel` """ return ProxyList(self, CC1.Channel, range(self._channel_count)) # METHODS # def clear_counts(self): """ Clears the current total counts on the counters. """ self.sendcmd("CLEA") ================================================ FILE: src/instruments/qubitekk/mc1.py ================================================ #!/usr/bin/env python """ Provides support for the Qubitekk MC1 Motor Controller. MC1 Class originally contributed by Catherine Holloway. """ # IMPORTS ##################################################################### from enum import Enum 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, ) # CLASSES ##################################################################### 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().__init__(filelike) self.terminator = "\r" self._increment = 1 * u.ms self._lower_limit = -300 * u.ms self._upper_limit = 300 * u.ms self._firmware = None self._controller = None # ENUMS # class MotorType(Enum): """ Enum for the motor types for the MC1 """ radio = "Radio" relay = "Relay" # PROPERTIES # @property def increment(self): """ Gets/sets the stepping increment value of the motor controller :units: As specified, or assumed to be of units milliseconds :type: `~pint.Quantity` """ return self._increment @increment.setter def increment(self, newval): self._increment = assume_units(newval, u.ms).to(u.ms) @property 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: `~pint.Quantity` """ return self._lower_limit @lower_limit.setter def lower_limit(self, newval): self._lower_limit = assume_units(newval, u.ms).to(u.ms) @property 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: `~pint.Quantity` """ return self._upper_limit @upper_limit.setter def upper_limit(self, newval): self._upper_limit = assume_units(newval, u.ms).to(u.ms) direction = unitful_property( command="DIRE", doc=""" Get the internal direction variable, which is a function of how far the motor needs to go. :type: `~pint.Quantity` :units: milliseconds """, units=u.ms, readonly=True, ) inertia = unitful_property( command="INER", doc=""" Gets/Sets the amount of force required to overcome static inertia. Must be between 0 and 100 milliseconds. :type: `~pint.Quantity` :units: milliseconds """, format_code="{:.0f}", units=u.ms, valid_range=(0 * u.ms, 100 * u.ms), set_fmt=":{} {}", ) @property def internal_position(self): """ Get the internal motor state position, which is equivalent to the total number of milliseconds that voltage has been applied to the motor in the positive direction minus the number of milliseconds that voltage has been applied to the motor in the negative direction. :type: `~pint.Quantity` :units: milliseconds """ response = int(self.query("POSI?")) * self.step_size return response metric_position = unitful_property( command="METR", doc=""" Get the estimated motor position, in millimeters. :type: `~pint.Quantity` :units: millimeters """, units=u.mm, readonly=True, ) setting = int_property( 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 input 1 is directed to output 2 and input 2 is directed to output 1. :type: `int` """, valid_set=range(2), set_fmt=":{} {}", ) step_size = unitful_property( command="STEP", doc=""" Gets/Sets the number of milliseconds per step. Must be between 1 and 100 milliseconds. :type: `~pint.Quantity` :units: milliseconds """, format_code="{:.0f}", units=u.ms, valid_range=(1 * u.ms, 100 * u.ms), set_fmt=":{} {}", ) @property def firmware(self): """ Gets the firmware version :rtype: `tuple`(Major:`int`, Minor:`int`, Patch`int`) """ # the firmware is assumed not to change while the device is active # firmware is stored locally as it will be gotten often # pylint: disable=no-member if self._firmware is None: while self._firmware is None: self._firmware = self.query("FIRM?") value = self._firmware.split(".") if len(value) < 3: for _ in range(3 - len(value)): value.append(0) value = tuple(map(int, value)) self._firmware = value return self._firmware controller = enum_property( "MOTO", MotorType, doc=""" Get the motor controller type. """, readonly=True, ) @property def move_timeout(self): """ Get the motor's timeout value, which indicates the number of milliseconds before the motor can start moving again. :type: `~pint.Quantity` :units: milliseconds """ response = int(self.query("TIME?")) return response * self.step_size # METHODS # def is_centering(self): """ Query whether the motor is in its centering phase :return: False if not centering, True if centering :rtype: `bool` """ response = self.query("CENT?") return True if int(response) == 1 else False def center(self): """ Commands the motor to go to the center of its travel range """ self.sendcmd(":CENT") def reset(self): """ Sends the stage to the limit of one of its travel ranges """ self.sendcmd(":RESE") def move(self, new_position): """ Move to a specified location. Position is unitless and is defined as the number of motor steps. It varies between motors. :param new_position: the location :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: clock_cycles = new_position / self.step_size cmd = f":MOVE {int(clock_cycles)}" self.sendcmd(cmd) else: raise ValueError("Location out of range") ================================================ FILE: src/instruments/rigol/__init__.py ================================================ #!/usr/bin/env python """ Module containing Rigol instruments """ from .rigolds1000 import RigolDS1000Series ================================================ FILE: src/instruments/rigol/rigolds1000.py ================================================ #!/usr/bin/env python """ Provides support for Rigol DS-1000 series oscilloscopes. """ # IMPORTS ##################################################################### from enum import Enum from instruments.abstract_instruments import Oscilloscope from instruments.generic_scpi import SCPIInstrument from instruments.util_fns import ProxyList, bool_property, enum_property # CLASSES ##################################################################### class RigolDS1000Series(SCPIInstrument, Oscilloscope): """ The Rigol DS1000-series is a popular budget oriented oscilloscope that has featured wide adoption across hobbyist circles. .. warning:: This instrument is not complete, and probably not even functional! """ # ENUMS # class AcquisitionType(Enum): """ Enum containing valid acquisition types for the Rigol DS1000 """ normal = "NORM" average = "AVER" peak_detect = "PEAK" # INNER CLASSES # class DataSource(Oscilloscope.DataSource): """ Class representing a data source (channel, math, or ref) on the Rigol DS1000 .. warning:: This class should NOT be manually created by the user. It is designed to be initialized by the `RigolDS1000Series` class. """ @property def name(self): return self._name 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(f":WAV:DATA? {self.name}") data = self._parent.binblockread(2) # TODO: check width return data class Channel(DataSource, Oscilloscope.Channel): """ Class representing a channel on the Rigol DS1000. This class inherits from `~RigolDS1000Series.DataSource`. .. 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().__init__(self._parent, f"CHAN{self._idx}") def sendcmd(self, cmd): """ Passes a command from the `Channel` class to the parent `RigolDS1000Series`, appending the required channel identification. :param str cmd: The command string to send to the instrument """ self._parent.sendcmd(f":CHAN{self._idx}:{cmd}") def query(self, cmd): """ Passes a command from the `Channel` class to the parent `RigolDS1000Series`, appending the required channel identification. :param str cmd: The command string to send to the instrument :return: The result as returned by the instrument :rtype: `str` """ return self._parent.query(f":CHAN{self._idx}:{cmd}") coupling = enum_property("COUP", Coupling) 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", inst_true="ON", inst_false="OFF") # TODO: :CHAN:MEMoryDepth vernier = bool_property("VERN", inst_true="ON", inst_false="OFF") # PROPERTIES # @property def channel(self): # Rigol DS1000 series oscilloscopes all have two channels, # according to the documentation. return ProxyList(self, self.Channel, range(2)) @property def math(self): return self.DataSource(parent=self, name="MATH") @property def ref(self): return self.DataSource(parent=self, name="REF") acquire_type = enum_property(":ACQ:TYPE", AcquisitionType) # TODO: implement :ACQ:MODE. This is confusing in the documentation, # though. @property def acquire_averages(self): """ Gets/sets the number of averages the oscilloscope should take per acquisition. :type: `int` """ return int(self.query(":ACQ:AVER?")) @acquire_averages.setter def acquire_averages(self, newval): 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(f":ACQ:AVER {newval}") # TODO: implement :ACQ:SAMP in a meaningful way. This should probably be # under Channel, and needs to be unitful. # TODO: I don't understand :ACQ:MEMD yet. # METHODS ## def force_trigger(self): self.sendcmd(":FORC") # TODO: consider moving the next few methods to Oscilloscope. def run(self): """ Starts running the oscilloscope trigger. """ self.sendcmd(":RUN") def stop(self): """ Stops running the oscilloscope trigger. """ self.sendcmd(":STOP") # TODO: unitful timebase! # FRONT-PANEL KEY EMULATION METHODS ## # These methods correspond one-to-one with physical keys on the front # (local) control panel, except for release_panel, which enables the local # panel and disables any remote lockouts, and for panel_locked. # # Many of the :KEY: commands are not yet implemented as methods. panel_locked = bool_property(":KEY:LOCK", inst_true="ENAB", inst_false="DIS") def release_panel(self): # TODO: better name? # NOTE: method may be redundant with the panel_locked property. """ Releases any lockout of the local control panel. """ self.sendcmd(":KEY:FORC") ================================================ FILE: src/instruments/srs/__init__.py ================================================ #!/usr/bin/env python """ Module containing Lakeshore instruments """ from .srs345 import SRS345 from .srs830 import SRS830 from .srsdg645 import SRSDG645 from .srsctc100 import SRSCTC100 ================================================ FILE: src/instruments/srs/srs345.py ================================================ #!/usr/bin/env python """ Provides support for the SRS 345 function generator. """ # IMPORTS ##################################################################### from enum import IntEnum from instruments.units import ureg as u from instruments.abstract_instruments import FunctionGenerator from instruments.generic_scpi import SCPIInstrument from instruments.util_fns import enum_property, unitful_property # CLASSES ##################################################################### class SRS345(SCPIInstrument, FunctionGenerator): """ The SRS DS345 is a 30MHz function generator. Example usage: >>> import instruments as ik >>> import instruments.units as u >>> srs = ik.srs.SRS345.open_gpib('/dev/ttyUSB0', 1) >>> 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. # CONSTANTS # _UNIT_MNEMONICS = { FunctionGenerator.VoltageMode.peak_to_peak: "VP", FunctionGenerator.VoltageMode.rms: "VR", FunctionGenerator.VoltageMode.dBm: "DB", } _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:]]) def _set_amplitude_(self, magnitude, units): self.sendcmd(f"AMPL {magnitude}{self._UNIT_MNEMONICS[units]}") # ENUMS ## class Function(IntEnum): """ Enum containing valid output function modes for the SRS 345 """ sinusoid = 0 square = 1 triangle = 2 ramp = 3 noise = 4 arbitrary = 5 # PROPERTIES ## frequency = unitful_property( 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 `~pint.Quantity` """, ) function = enum_property( command="FUNC", enum=Function, input_decoration=int, doc=""" Gets/sets the output function of the function generator. :type: `~SRS345.Function` """, ) offset = unitful_property( 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 `~pint.Quantity` """, ) phase = unitful_property( 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 `~pint.Quantity` """, ) ================================================ FILE: src/instruments/srs/srs830.py ================================================ #!/usr/bin/env python """ Provides support for the SRS 830 lock-in amplifier. """ # IMPORTS ##################################################################### import math import time import warnings from enum import Enum, IntEnum from instruments.abstract_instruments.comm import ( GPIBCommunicator, SerialCommunicator, 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, ) # CONSTANTS ################################################################### VALID_SAMPLE_RATES = [2.0**n for n in range(-4, 10)] VALID_SAMPLE_RATES += ["trigger"] # CLASSES ##################################################################### class SRS830(SCPIInstrument): """ Communicates with a Stanford Research Systems 830 Lock-In Amplifier. Example usage: >>> import instruments as ik >>> import instruments.units as u >>> srs = ik.srs.SRS830.open_gpibusb('/dev/ttyUSB0', 1) >>> 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): """ Class initialization method. :param int outx_mode: Manually over-ride which ``OUTX`` command to send at startup. This is a command that needs to be sent as specified by the SRS830 manual. If left default, the correct ``OUTX`` command will be sent depending on what type of communicator self._file is. """ super().__init__(filelike) if outx_mode == 1: self.sendcmd("OUTX 1") elif outx_mode == 2: self.sendcmd("OUTX 2") else: if isinstance(self._file, GPIBCommunicator): self.sendcmd("OUTX 1") elif isinstance(self._file, SerialCommunicator): self.sendcmd("OUTX 2") elif isinstance(self._file, LoopbackCommunicator): pass else: warnings.warn( "OUTX command has not been set. Instrument " "behaviour is unknown.", UserWarning, ) # ENUMS # class FreqSource(IntEnum): """ Enum for the SRS830 frequency source settings. """ external = 0 internal = 1 class Coupling(IntEnum): """ Enum for the SRS830 channel coupling settings. """ ac = 0 dc = 1 class BufferMode(IntEnum): """ Enum for the SRS830 buffer modes. """ one_shot = 0 loop = 1 class Mode(Enum): """ Enum containing valid modes for the SRS 830 """ x = "x" y = "y" r = "r" theta = "theta" xnoise = "xnoise" ynoise = "ynoise" aux1 = "aux1" aux2 = "aux2" aux3 = "aux3" aux4 = "aux4" ref = "ref" ch1 = "ch1" ch2 = "ch2" none = "none" # CONSTANTS # _XYR_MODE_MAP = {Mode.x: 1, Mode.y: 2, Mode.r: 3} # PROPERTIES # frequency_source = enum_property( "FMOD", FreqSource, input_decoration=int, doc=""" Gets/sets the frequency source used. This is either an external source, or uses the internal reference. :type: `SRS830.FreqSource` """, ) frequency = unitful_property( "FREQ", u.hertz, valid_range=(0, None), doc=""" Gets/sets the lock-in amplifier reference frequency. :units: As specified (if a `~pint.Quantity`) or assumed to be of units Hertz. :type: `~pint.Quantity` with units Hertz. """, ) phase, phase_min, phase_max = bounded_unitful_property( "PHAS", 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 `~pint.Quantity`) or assumed to be of units degrees. :type: `~pint.Quantity` with units degrees. """, ) amplitude, amplitude_min, amplitude_max = bounded_unitful_property( "SLVL", 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 `~pint.Quantity`) or assumed to be of units volts. Value should be specified as peak-to-peak. :type: `~pint.Quantity` with units volts peak-to-peak. """, ) input_shield_ground = bool_property( "IGND", inst_true="1", inst_false="0", doc=""" Function sets the input shield grounding to either 'float' or 'ground'. :type: `bool` """, ) coupling = enum_property( "ICPL", Coupling, input_decoration=int, doc=""" Gets/sets the input coupling to either 'ac' or 'dc'. :type: `SRS830.Coupling` """, ) @property def sample_rate(self): r""" Gets/sets the data sampling rate of the lock-in. Acceptable set values are :math:`2^n` where :math:`n \in \{-4...+9\}` or the string `trigger`. :type: `~pint.Quantity` with units Hertz. """ value = int(self.query("SRAT?")) if value == 14: return "trigger" return u.Quantity(VALID_SAMPLE_RATES[value], u.Hz) @sample_rate.setter def sample_rate(self, newval): if isinstance(newval, str): newval = newval.lower() if newval in VALID_SAMPLE_RATES: self.sendcmd(f"SRAT {VALID_SAMPLE_RATES.index(newval)}") else: raise ValueError( "Valid samples rates given by {} " 'and "trigger".'.format(VALID_SAMPLE_RATES) ) buffer_mode = enum_property( "SEND", BufferMode, input_decoration=int, doc=""" Gets/sets the end of buffer mode. This sets the behaviour of the instrument when the data storage buffer is full. Setting to `one_shot` will stop acquisition, while `loop` will repeat from the start. :type: `SRS830.BufferMode` """, ) @property def num_data_points(self): """ Gets the number of data sets in the SRS830 buffer. :type: `int` """ resp = None i = 0 while not resp and i < 10: resp = self.query("SPTS?").strip() i += 1 if not resp: raise OSError( f"Expected integer response from instrument, got {repr(resp)}" ) return int(resp) data_transfer = bool_property( "FAST", inst_true="2", inst_false="0", doc=""" Gets/sets the data transfer status. Note that this function only makes use of 2 of the 3 data transfer modes supported by the SRS830. The supported modes are FAST0 and FAST2. The other, FAST1, is for legacy systems which this package does not support. :type: `bool` """, ) # AUTO- METHODS # def auto_offset(self, mode): """ Sets a specific channel mode to auto offset. This is the same as pressing the auto offset key on the display. It sets the offset of the mode specified to zero. :param mode: Target mode of auto_offset function. Valid inputs are {X|Y|R}. :type mode: `~SRS830.Mode` or `str` """ if isinstance(mode, str): mode = mode.lower() mode = SRS830.Mode[mode] if mode not in self._XYR_MODE_MAP: raise ValueError("Specified mode not valid for this function.") mode = self._XYR_MODE_MAP[mode] self.sendcmd(f"AOFF {mode}") def auto_phase(self): """ Sets the lock-in to auto phase. This does the same thing as pushing the auto phase button. Do not send this message again without waiting the correct amount of time for the lock-in to finish. """ self.sendcmd("APHS") # META-METHODS # def init(self, sample_rate, buffer_mode): r""" Wrapper function to prepare the SRS830 for measurement. Sets both the data sampling rate and the end of 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: `~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 `one_shot` will stop acquisition, while `loop` will repeat from the start. """ self.clear_data_buffer() self.sample_rate = sample_rate self.buffer_mode = buffer_mode def start_data_transfer(self): """ Wrapper function to start the actual data transfer. Sets the transfer mode to FAST2, and triggers the data transfer to start after a delay of 0.5 seconds. """ self.data_transfer = True self.start_scan() def take_measurement(self, sample_rate, num_samples): """ Wrapper function that allows you to easily take measurements with a specified sample rate and number of desired samples. Function will call time.sleep() for the required amount of time it will take the instrument to complete this sampling operation. Returns a list containing two items, each of which are lists containing the channel data. The order is [[Ch1 data], [Ch2 data]]. :param `int` sample_rate: Set the desired sample rate of the measurement. See `~SRS830.sample_rate` for more information. :param `int` num_samples: Number of samples to take. :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.") sample_time = math.ceil(num_samples / sample_rate) self.init(sample_rate, SRS830.BufferMode["one_shot"]) self.start_data_transfer() time.sleep(sample_time + 0.1) self.pause() # The following should fail. We do this to force the instrument # to flush its internal buffers. # Note that this causes a redundant transmission, and should be fixed # in future versions. try: self.num_data_points except OSError: pass ch1 = self.read_data_buffer("ch1") ch2 = self.read_data_buffer("ch2") if numpy: return numpy.array([ch1, ch2]) return ch1, ch2 # OTHER METHODS # def set_offset_expand(self, mode, offset, expand): """ Sets the channel offset and expand parameters. Offset is a percentage, and expand is given as a multiplication factor of 1, 10, or 100. :param mode: The channel mode that you wish to change the offset and/or the expand of. Valid modes are X, Y, and R. :type mode: `SRS830.Mode` or `str` :param float offset: Offset of the mode, given as a percent. offset = <-105...+105>. :param int expand: Expansion factor for the measurement. Valid input is {1|10|100}. """ if isinstance(mode, str): mode = mode.lower() mode = SRS830.Mode[mode] if mode not in self._XYR_MODE_MAP: 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.") if not isinstance(expand, (int, 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.") valid = [1, 10, 100] if expand in valid: expand = valid.index(expand) else: raise ValueError("Expand must be 1, 10, 100.") self.sendcmd(f"OEXP {mode},{int(offset)},{expand}") def start_scan(self): """ After setting the data transfer on via the dataTransfer function, this is used to start the scan. The scan starts after a delay of 0.5 seconds. """ self.sendcmd("STRD") def pause(self): """ Has the instrument pause data capture. """ 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, } def data_snap(self, mode1, mode2): """ Takes a snapshot of the current parameters are defined by variables mode1 and mode2. For combinations (X,Y) and (R,THETA), they are taken at the same instant. All other combinations are done sequentially, and may not represent values taken from the same timestamp. Returns a list of floats, arranged in the order that they are given in the function input parameters. :param mode1: Mode to take data snap for channel 1. Valid inputs are given by: {X|Y|R|THETA|AUX1|AUX2|AUX3|AUX4|REF|CH1|CH2} :type mode1: `~SRS830.Mode` or `str` :param mode2: Mode to take data snap for channel 2. Valid inputs are given by: {X|Y|R|THETA|AUX1|AUX2|AUX3|AUX4|REF|CH1|CH2} :type mode2: `~SRS830.Mode` or `str` :rtype: `list` """ if isinstance(mode1, str): mode1 = mode1.lower() mode1 = SRS830.Mode[mode1] if isinstance(mode2, str): 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.") 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.") result = self.query(f"SNAP? {mode1},{mode2}") return list(map(float, result.split(","))) _valid_read_data_buffer = {Mode.ch1: 1, Mode.ch2: 2} def read_data_buffer(self, channel): """ Reads the entire data buffer for a specific channel. Transfer is done in ASCII mode. Although binary would be faster, this is not currently implemented. Returns a list of floats containing instrument's measurements. :param channel: Channel data buffer to read from. Valid channels are given by {CH1|CH2}. :type channel: `SRS830.Mode` or `str` :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.") channel = self._valid_read_data_buffer[channel] N = self.num_data_points # Retrieve number of data points stored # Query device for entire buffer, returning in ASCII, then # converting to a list of floats before returning to the # calling method 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") _valid_channel_display = [ {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, }, ] _valid_channel_ratio = [ {Mode.none: 0, Mode.aux1: 1, Mode.aux2: 2}, # channel1 {Mode.none: 0, Mode.aux3: 1, Mode.aux4: 2}, # channel2 ] _valid_channel = {Mode.ch1: 1, Mode.ch2: 2} def set_channel_display(self, channel, display, ratio): """ Sets the display of the two channels. Channel 1 can display X, R, X Noise, Aux In 1, Aux In 2 Channel 2 can display Y, Theta, Y Noise, Aux In 3, Aux In 4 Channel 1 can have ratio of None, Aux In 1, Aux In 2 Channel 2 can have ratio of None, Aux In 3, Aux In 4 :param channel: Channel you wish to set the display of. Valid input is one of {CH1|CH2}. :type channel: `~SRS830.Mode` or `str` :param display: Setting the channel will be changed to. Valid input is one of {X|Y|R|THETA|XNOISE|YNOISE|AUX1|AUX2|AUX3|AUX4} :type display: `~SRS830.Mode` or `str` :param ratio: Desired ratio setting for this channel. Valid input is one of {NONE|AUX1|AUX2|AUX3|AUX4} :type ratio: `~SRS830.Mode` or `str` """ if isinstance(channel, str): channel = channel.lower() channel = SRS830.Mode[channel] if isinstance(display, str): display = display.lower() display = SRS830.Mode[display] if isinstance(ratio, str): ratio = ratio.lower() ratio = SRS830.Mode[ratio] if channel not in self._valid_channel: 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.") if ratio not in self._valid_channel_ratio[channel - 1]: 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(f"DDEF {channel},{display},{ratio}") ================================================ FILE: src/instruments/srs/srsctc100.py ================================================ #!/usr/bin/env python """ Provides support for the SRS CTC-100 cryogenic temperature controller. """ # IMPORTS ##################################################################### from contextlib import contextmanager from enum import Enum 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 ##################################################################### class SRSCTC100(SCPIInstrument): """ Communicates with a Stanford Research Systems CTC-100 cryogenic temperature controller. """ def __init__(self, filelike): super().__init__(filelike) self._do_errcheck = True # DICTIONARIES # _BOOL_NAMES = {"On": True, "Off": False} # Note that the SRS CTC-100 uses '\xb0' to represent '°'. _UNIT_NAMES = { "\xb0C": u.celsius, "W": u.watt, "V": u.volt, "\xea": u.ohm, "": u.dimensionless, } # INNER CLASSES ## class SensorType(Enum): """ Enum containing valid sensor types for the SRS CTC-100 """ rtd = "RTD" thermistor = "Thermistor" diode = "Diode" rox = "ROX" class Channel: """ Represents an input or output channel on an SRS CTC-100 cryogenic temperature controller. """ def __init__(self, ctc, chan_name): self._ctc = ctc # Save the pretty name that we are given. self._chan_name = chan_name # Strip spaces from the name used in remote programming, as # specified on page 14 of the manual. self._rem_name = chan_name.replace(" ", "") # PRIVATE METHODS # def _get(self, prop_name): return self._ctc.query(f"{self._rem_name}.{prop_name}?").strip() def _set(self, 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 # and on the front-panel display. @property def name(self): """ Gets/sets the name of the channel that will be used by the instrument to identify the channel in programming and on the display. :type: `str` """ return self._chan_name @name.setter def name(self, newval): self._set("name", newval) # TODO: check for errors! self._chan_name = newval self._rem_name = newval.replace(" ", "") # BASICS # @property def value(self): """ Gets the measurement value of the channel. Units depend on what kind of sensor and/or channel you have specified. Units can be one of ``celsius``, ``watt``, ``volt``, ``ohm``, or ``dimensionless``. :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 u.Quantity(float(self._get("value")), self.units) @property def units(self): """ Gets the appropriate units for the specified channel. Units can be one of ``celsius``, ``watt``, ``volt``, ``ohm``, or ``dimensionless``. :type: `~pint.Unit` """ # FIXME: does not respect "chan.d/dt" property. return self._ctc.channel_units()[self._chan_name] # FIXME: the following line doesn't do what I'd expect, and so it's # commented out. # return # self._ctc._UNIT_NAMES[self._ctc.query('{}.units?'.format(self._rem_name)).strip()] @property def sensor_type(self): """ Gets the type of sensor attached to the specified channel. :type: `SRSCTC100.SensorType` """ return self._ctc.SensorType(self._get("sensor")) # STATS # # The following properties control and query the statistics of the # channel. @property def stats_enabled(self): """ Gets/sets enabling the statistics for the specified channel. :type: `bool` """ 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") @property def stats_points(self): """ Gets/sets the number of sample points to use for the channel statistics. :type: `int` """ return int(self._get("points")) @stats_points.setter def stats_points(self, newval): self._set("points", int(newval)) @property def average(self): """ Gets the average measurement for the specified channel as determined by the statistics gathering. :type: `~pint.Quantity` """ return u.Quantity(float(self._get("average")), self.units) @property def std_dev(self): """ Gets the standard deviation for the specified channel as determined by the statistics gathering. :type: `~pint.Quantity` """ return u.Quantity(float(self._get("SD")), self.units) # LOGGING # def get_log_point(self, which="next", units=None): """ Get a log data point from the instrument. :param str which: Which data point you want. Valid examples include ``first``, and ``next``. Consult the instrument manual for the complete list :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: `~pint.Unit` :return: The log data point with units :rtype: `~pint.Quantity` """ if units is None: units = self.units point = [ s.strip() for s in self._ctc.query(f"getLog.xy {self._chan_name}, {which}").split( "," ) ] return u.Quantity(float(point[0]), "ms"), u.Quantity(float(point[1]), units) def get_log(self): """ Gets all of the log data points currently saved in the instrument memory. :return: Tuple of all the log data points. First value is time, second is the measurement value. :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(f"getLog.xy? {self._chan_name}")) # Make an empty quantity that size for the times and for the channel # values. 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) for idx in range(1, n_points): 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 ## def _channel_names(self): """ Returns the names of valid channels, using the ``getOutput.names`` command, as documented in the example on page 14 of the `CTC-100 manual`_. Note that ``getOutput`` also lists input channels, confusingly enough. .. _CTC-100 manual: http://www.thinksrs.com/downloads/PDFs/Manuals/CTC100m.pdf """ # We need to split apart the comma-separated list and make sure that # no newlines or other whitespace gets carried along for the ride. # Note that we do NOT strip spaces here, as this is done inside # the Channel object. Doing things that way allows us to present # the actual pretty name to users, but to use the remote-programming # name in commands. # 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(",")] return names 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 `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(",") ] return { chan_name: self._UNIT_NAMES[unit_str] for chan_name, unit_str in zip(self._channel_names(), unit_strings) } def errcheck(self): """ Performs an error check query against the CTC100. This function does not return anything, but will raise an `IOError` if the error code received by the instrument is not zero. :return: Nothing """ 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 OSError(err_descript.strip()) @contextmanager def _error_checking_disabled(self): old = self._do_errcheck self._do_errcheck = False yield self._do_errcheck = old # PROPERTIES ## @property def channel(self): """ Gets a specific measurement channel on the SRS CTC100. This is accessed like one would access a `dict`. Here you must use the actual channel names to address a specific channel. This is different from most other instruments in InstrumentKit because the CRC100 channel names can change by the user. The list of current valid channel names can be accessed by the `SRSCTC100._channel_names()` function. :type: `SRSCTC100.Channel` """ # Note that since the names can change, we need to query channel names # each time. This is inefficient, but alas. return ProxyList(self, self.Channel, self._channel_names()) @property def display_figures(self): """ Gets/sets the number of significant figures to display. Valid range is 0-6 inclusive. :type: `int` """ 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(f"system.display.figures = {newval}") @property def error_check_toggle(self): """ Gets/sets if errors should be checked for after every command. :bool: """ return self._do_errcheck @error_check_toggle.setter def error_check_toggle(self, newval): if not isinstance(newval, bool): raise TypeError self._do_errcheck = newval # OVERRIDEN METHODS # # We override sendcmd() and query() to do error checking after each # command. def sendcmd(self, cmd): super().sendcmd(cmd) if self._do_errcheck: self.errcheck() def query(self, cmd, size=-1): resp = super().query(cmd, size) if self._do_errcheck: self.errcheck() return resp # LOGGING COMMANDS # def clear_log(self): """ Clears the SRS CTC100 log Not sure if this works. """ self.sendcmd("System.Log.Clear yes") ================================================ FILE: src/instruments/srs/srsdg645.py ================================================ #!/usr/bin/env python """ Provides support for the SRS DG645 digital delay generator. """ # IMPORTS ##################################################################### from enum import IntEnum 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 SRSDG645(SCPIInstrument): """ Communicates with a Stanford Research Systems DG645 digital delay generator, using the SCPI commands documented in the `user's guide`_. Example usage: >>> 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") .. _user's guide: http://www.thinksrs.com/downloads/PDFs/Manuals/DG645m.pdf """ class Channel: """ Class representing a sensor attached to the SRS DG644. .. warning:: This class should NOT be manually created by the user. It is designed to be initialized by the `SRSDG644` class. """ def __init__(self, parent, chan): if not isinstance(parent, SRSDG645): raise TypeError("Don't do that.") if isinstance(chan, parent.Channels): self._chan = chan.value else: self._chan = chan self._ddg = parent # PROPERTIES # @property def idx(self): """ Gets the channel identifier number as used for communication :return: The communication identification number for the specified channel :rtype: `int` """ 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, ``(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().__init__(filelike) # This instrument requires stripping two characters. if isinstance(filelike, GPIBCommunicator): filelike.strip = 2 # ENUMS # class LevelPolarity(IntEnum): """ Polarities for output levels. """ positive = 1 negative = 0 class Outputs(IntEnum): """ Enumeration of valid outputs from the DDG. """ T0 = 0 AB = 1 CD = 2 EF = 3 GH = 4 class Channels(IntEnum): """ Enumeration of valid delay channels for the DDG. """ T0 = 0 T1 = 1 A = 2 B = 3 C = 4 D = 5 E = 6 F = 7 G = 8 H = 9 class DisplayMode(IntEnum): """ Enumeration of possible modes for the physical front-panel display. """ trigger_rate = 0 trigger_threshold = 1 trigger_single_shot = 2 trigger_line = 3 adv_triggering_enable = 4 trigger_holdoff = 5 prescale_config = 6 burst_mode = 7 burst_delay = 8 burst_count = 9 burst_period = 10 channel_delay = 11 channel_levels = 12 channel_polarity = 13 burst_T0_config = 14 class TriggerSource(IntEnum): """ Enumeration of the different allowed trigger sources and modes. """ internal = 0 external_rising = 1 external_falling = 2 ss_external_rising = 3 ss_external_falling = 4 single_shot = 5 line = 6 # INNER CLASSES # class Output: """ An output from the DDG. """ def __init__(self, parent, idx): self._parent = parent self._idx = int(idx) @property def polarity(self): """ Polarity of this output. :type: :class:`SRSDG645.LevelPolarity` """ return self._parent.LevelPolarity( 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(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:`~pint.Quantity` :units: As specified, or :math:`\\text{V}` by default. """ 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(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 # @property 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: >>> import instruments as ik >>> inst = ik.srs.SRSDG645.open_gpibusb('/dev/ttyUSB0', 1) >>> inst.channel[inst.Channels.A] See the example in `SRSDG645` for a more complete example. :rtype: `SRSDG645.Channel` """ return ProxyList(self, self.Channel, SRSDG645.Channels) @property def output(self): """ Gets the specified output port. :type: :class:`SRSDG645.Output` """ return ProxyList(self, self.Output, self.Outputs) @property def display(self): """ Gets/sets the front-panel display mode for the connected DDG. The mode is a tuple of the display mode and the channel. :type: `tuple` of an `SRSDG645.DisplayMode` and an `SRSDG645.Channels` """ disp_mode, chan = map(int, self.query("DISP?").split(",")) return SRSDG645.DisplayMode(disp_mode), SRSDG645.Channels(chan) @display.setter def display(self, newval): # TODO: check types here. self.sendcmd("DISP {},{}".format(*map(int, newval))) @property def enable_adv_triggering(self): """ Gets/sets whether advanced triggering is enabled. :type: `bool` """ return bool(int(self.query("ADVT?"))) @enable_adv_triggering.setter def enable_adv_triggering(self, newval): self.sendcmd(f"ADVT {1 if newval else 0}") @property def trigger_rate(self): """ Gets/sets the rate of the internal trigger. :type: `~pint.Quantity` or `float` :units: As passed or Hz if not specified. """ return u.Quantity(float(self.query("TRAT?")), u.Hz) @trigger_rate.setter def trigger_rate(self, newval): newval = assume_units(newval, u.Hz) self.sendcmd(f"TRAT {newval.to(u.Hz).magnitude}") @property def trigger_source(self): """ Gets/sets the source for the trigger. :type: :class:`SRSDG645.TriggerSource` """ return SRSDG645.TriggerSource(int(self.query("TSRC?"))) @trigger_source.setter def trigger_source(self, newval): self.sendcmd(f"TSRC {int(newval)}") @property def holdoff(self): """ Gets/sets the trigger holdoff time. :type: `~pint.Quantity` or `float` :units: As passed, or s if not specified. """ return u.Quantity(float(self.query("HOLD?")), u.s) @holdoff.setter def holdoff(self, newval): 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}") ================================================ FILE: src/instruments/sunpower/__init__.py ================================================ #!/usr/bin/env python """ Module containing Sunpower instruments """ from .cryotel_gt import CryoTelGT ================================================ FILE: src/instruments/sunpower/cryotel_gt.py ================================================ #!/usr/bin/env python """ Driver for the Sunpower CryoTel GT generation 2 cryocooler. """ # IMPORTS ##################################################################### from collections import OrderedDict from enum import Enum import warnings from instruments.abstract_instruments import Instrument from instruments.units import ureg as u from instruments.util_fns import assume_units # CLASSES ##################################################################### class CryoTelGT(Instrument): """ The Sunpower CyroTel GT is a cryocooler. This driver is for the GT generation 2. According to the Sunpower website, this means for cryocoolers purchased after May 2012. Caution: Do not use this driver to established a closed loop control of the cryocooler, as this may cause malfunction and potentially damage to the device (see the manual for details). You can use this driver however to adjust the setpoint temperature and read the current temperature. For communications, the default baudrate is 4800, 8 data bits, 1 stop bit, and no flow control. Example usage: >>> import instruments as ik >>> inst = ik.sunpower.CryoTelGT.open_serial("/dev/ttyACM0", 4800) >>> inst.temperature 82.0 Kelvin >>> inst.temperature_setpoint 77.0 Kelvin """ class ControlMode(Enum): """ Control modes for the Cryocooler. """ POWER = 0 TEMPERATURE = 2 class ThermostatStatus(Enum): """ Thermostat status for the CryoTel GT. Off means that the thermostat is open and the cryocooler is shutting down or shut down. """ OFF = 0 ON = 1 class StopMode(Enum): """ Stop mode for the cryocooler. `HOST` means that the start/stop command can be controlled from the host computer. `DIGIO` means that the start/stop command can be set from the digital input/output pin 1 on the cryocooler. """ HOST = 0 DIGIO = 1 def __init__(self, filelike): super().__init__(filelike) self._error_codes = OrderedDict( { 1: "Over Current", 2: "Jumper Error", 4: "Serial Error", 8: "Non-volatile Memory Error", 16: "Watchdog Error", 32: "Temperature Sensor Error", } ) self.terminator = "\r" @property def at_temperature_band(self): """ Get/set the temperature band of the CryoTel GT in Kelvin. Returns the temperature band within the green LED and "At Temperature" pin on the I/O connector will be activated. If no unit is provided, Kelvin are assumed. :return: The current temperature band in Kelvin. """ ret_val = self.query("SET TBAND") return float(ret_val) * u.K @at_temperature_band.setter def at_temperature_band(self, value): value = assume_units(value, u.K).to(u.K) self.query("SET TBAND", float(value.magnitude)) @property def control_mode(self): """ Get/set the control mode of the CryoTel GT. Valid options are `ControlMode.POWER` and `ControlMode.TEMPERATURE`. .. note:: The set control mode will be reset after a power cycle unless you also call the `save_control_mode()` method. :return: The current control mode. """ ret_val = int(float(self.query("SET PID"))) return self.ControlMode(ret_val) @control_mode.setter def control_mode(self, value): if not isinstance(value, self.ControlMode): raise ValueError( "Invalid control mode. Use ControlMode.POWER or ControlMode.TEMPERATURE." ) self.query("SET PID", value.value) @property def errors(self): """Get any error codes from the CryoTel GT. Only error codes that are currently active will be added to the list. If no error codes are active, an empty list is returned. :return: List of human readable strings. """ ret_val = int(self.query("ERROR"), 2) errors = [] for errcode, errstr in self._error_codes.items(): if ret_val & errcode: errors.append(errstr) return errors @property def ki(self): """Set/get the integral constant of the temperature control loop. The default integral constant is 1.0 and will be reset to this value if the reset method is called. :return: The current integral constant. :rtype: float """ ret_val = self.query("SET KI") return float(ret_val) @ki.setter def ki(self, value): _ = self.query("SET KI", float(value)) @property def kp(self): """Set/get the proportional constant of the temperature control loop. The default proportional constant is 50.0 and will be reset to this value if the reset method is called. :return: The current proportional constant. :rtype: float """ ret_val = self.query("SET KP") return float(ret_val) @kp.setter def kp(self, value): _ = self.query("SET KP", float(value)) @property def power(self): """ Get the current power in Watts. :return: The current power in Watts. """ ret_val = self.query("P") return float(ret_val) * u.W @property def power_current_and_limits(self): """ Get the current power and power limits in Watts. :return: Three u.Quantity objects representing the maximum allowable power at the current temperature, the minimum allowable power at the current temperature, and the current power. """ ret_vals = self.query_multiline("E", 3) max_power = float(ret_vals[0]) * u.W min_power = float(ret_vals[1]) * u.W current_power = float(ret_vals[2]) * u.W return max_power, min_power, current_power @property def power_max(self): """ Get/set the maximum user defined power in Watts. The cooler will automatically limit the power to a safe value if this number exceeds the maximum allowable power. :return: The maximum user defined power in Watts. """ ret_val = self.query("SET MAX") return float(ret_val) * u.W @power_max.setter def power_max(self, value): value = assume_units(value, u.W).to(u.W) if value.magnitude < 0 or value.magnitude > 999.99: raise ValueError("Maximum power must be between 0 and 999.99 Watts.") self.query("SET MAX", float(value.magnitude)) @property def power_min(self): """ Get/set the minimum user defined power in Watts. The cooler will automatically limit the power to a safe value if this number exceeds the minimum allowable power. :return: The minimum user defined power in Watts. """ ret_val = self.query("SET MIN") return float(ret_val) * u.W @power_min.setter def power_min(self, value): value = assume_units(value, u.W).to(u.W) if value.magnitude < 0 or value.magnitude > 999.99: raise ValueError("Minimum power must be between 0 and 999.99 Watts.") self.query("SET MIN", float(value.magnitude)) @property def power_setpoint(self): """ Get/set the setpoint power in Watts. This setpoint is used when the control mode is set to `ControlMode.POWER`. Setting the power is unitful. If no unit is given, it is assumed to be in Watts. While any number from 0 to 999.99 can be set, the controller will only command a power that will not damage the cryocooler. :return: The setpoint power in Watts. :raises ValueError: If the power is set to a value outside the allowed range. """ ret_val = self.query("SET PWOUT") return float(ret_val) * u.W @power_setpoint.setter def power_setpoint(self, value): value = assume_units(value, u.W).to(u.W) if value.magnitude < 0 or value.magnitude > 999.99: raise ValueError("Power setpoint must be between 0 and 999.99 Watts.") self.query("SET PWOUT", float(value.magnitude)) @property def serial_number(self): """ Get the serial number and revision of the CryoTel GT. :return: List of serial number string and revision string. """ return self.query_multiline("SERIAL", 2) @property def state(self): """ Get a list of most of the control parameters and their values. Note: This is the direct list from the CryoTel GT controller. See the manual for the meaning of the parameters. :return: A list of strings representing the control parameters and their values. """ return self.query_multiline("STATE", 14) @property def temperature(self): """ Get the current temperature in Kelvin. :return: The current temperature in Kelvin. """ ret_val = self.query("TC") return float(ret_val) * u.K @property def temperature_setpoint(self): """ Get/set the setpoint temperature in Kelvin. This setpoint is used when the control mode is set to `ControlMode.TEMPERATURE`. Setting the temperature is unitful. If no unit is given, it is assumed to be in Kelvin. :return: The setpoint temperature in Kelvin. """ ret_val = self.query("SET TTARGET") return float(ret_val) * u.K @temperature_setpoint.setter def temperature_setpoint(self, value): value = assume_units(value, u.K).to(u.K) self.query("SET TTARGET", float(value.magnitude)) @property def thermostat(self): """Get/set the thermostat mode of the CryoTel GT. Set this to `True` to enable the thermostat mode, or `False` to disable it. :return: The current thermostat mode state. :rtype: bool """ ret_val = int(float(self.query("SET TSTATM"))) return bool(ret_val) @thermostat.setter def thermostat(self, value): if not isinstance(value, bool): raise ValueError("Invalid thermostat mode. Use True or False.") self.query("SET TSTATM", int(value)) @property def thermostat_status(self): """ Get the current thermostat status of the CryoTel GT. Returns `ThermostatStatus.ON` if the thermostat is enabled, and `ThermostatStatus.OFF` if it is disabled. :return: The current thermostat status. :rtype: ThermostatStatus """ ret_val = int(float(self.query("TSTAT"))) return self.ThermostatStatus(ret_val) @property def stop(self): """ Get/set the stop state of the CryoTel GT. Valid options are `True` (stop) and `False` (start). :return: The current stop state. """ ret_val = int(float(self.query("SET SSTOP"))) return bool(ret_val) @stop.setter def stop(self, value): if not isinstance(value, bool): raise ValueError("Invalid stop state. Use True or False.") self.query("SET SSTOP", int(value)) @property def stop_mode(self): """ Get/set the stop mode of the CryoTel GT. Valid options are `StopMode.HOST` and `StopMode.DIGIO`. :return: The current stop mode. """ ret_val = int(float(self.query("SET SSTOPM"))) return self.StopMode(ret_val) @stop_mode.setter def stop_mode(self, value): if not isinstance(value, self.StopMode): raise ValueError("Invalid stop mode. Use StopMode.HOST or StopMode.DIGIO.") self.query("SET SSTOPM", value.value) # CryoCooler Methods def reset(self): """ Reset the CryoTel GT to factory defaults. """ _ = self.query_multiline("RESET=F", 2) def save_control_mode(self): """ Save the current control mode as the default control mode. """ _ = self.query("SAVE PID") # Driver methods def query(self, command, value=None): """ Send a query to the cooler and return the response if no value is given. When setting a variable, the CryoTel GT will generally return the value that was set. This is checked for accuracy and a warning is raised if the return value is not the same as the set value. For an actual query where we expect a result, the result is returned unchanged. :param command: The command to send to the cooler. :param value: The value to be set. If not given or None, it is assumed that you want to query the cryocooler. :return: The response from the cooler or None. """ if value is None: self.sendcmd(command) return self.read().strip() else: if isinstance(value, float): value_to_send = f"{value:.2f}" else: value_to_send = str(value) self.sendcmd(f"{command}={value_to_send}") ret_val = self.read().strip() if float(ret_val) != value: warnings.warn( f"Set value {value} does not match returned value {ret_val}." ) def query_multiline(self, command, num_lines): """ Send a query to the cooler and return the response. This is used for commands that return multiple lines of data. :param command: The command to send to the cooler. :param num_lines: The number of lines to read from the cooler. :return: The response from the cooler as a list of lines. """ self.sendcmd(command) ret_val = [self.read().strip() for _ in range(num_lines)] return ret_val def sendcmd(self, command): """ Send a command to the cooler. :param command: The command to send to the cooler. """ self._file.sendcmd(command) _ = self.read() # echo ================================================ FILE: src/instruments/tektronix/__init__.py ================================================ #!/usr/bin/env python """ Module containing Tektronix instruments """ from .tekdpo4104 import TekDPO4104 from .tekdpo70000 import TekDPO70000 from .tekawg2000 import TekAWG2000 from .tektds224 import TekTDS224 from .tektds5xx import TekTDS5xx ================================================ FILE: src/instruments/tektronix/tekawg2000.py ================================================ #!/usr/bin/env python """ Provides support for the Tektronix AWG2000 series arbitrary wave generators. """ # IMPORTS ##################################################################### from enum import Enum 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 ##################################################################### class TekAWG2000(SCPIInstrument): """ Communicates with a Tektronix AWG2000 series instrument using the SCPI commands documented in the user's guide. """ # INNER CLASSES # class Channel: """ Class representing a physical channel on the Tektronix AWG 2000 .. warning:: This class should NOT be manually created by the user. It is designed to be initialized by the `TekAWG2000` class. """ 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 = f"CH{idx + 1}" # Remember what the old data source was for use as a context manager self._old_dsrc = None # PROPERTIES # @property def name(self): """ Gets the name of this AWG channel :type: `str` """ return self._name @property def amplitude(self): """ Gets/sets the amplitude of the specified channel. :units: As specified (if a `~pint.Quantity`) or assumed to be of units Volts. :type: `~pint.Quantity` with units Volts peak-to-peak. """ 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, u.V).to(u.V).magnitude ) ) @property def offset(self): """ Gets/sets the offset of the specified channel. :units: As specified (if a `~pint.Quantity`) or assumed to be of units Volts. :type: `~pint.Quantity` with units Volts. """ 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, u.V).to(u.V).magnitude ) ) @property def frequency(self): """ Gets/sets the frequency of the specified channel when using the built-in function generator. :units: As specified (if a `~pint.Quantity`) or assumed to be of units Hertz. :type: `~pint.Quantity` with units Hertz. """ return u.Quantity(float(self._tek.query("FG:FREQ?").strip()), u.Hz) @frequency.setter def frequency(self, newval): self._tek.sendcmd( f"FG:FREQ {assume_units(newval, u.Hz).to(u.Hz).magnitude}HZ" ) @property def polarity(self): """ Gets/sets the polarity of the specified channel. :type: `TekAWG2000.Polarity` """ 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)) ) self._tek.sendcmd(f"FG:{self._name}:POL {newval.value}") @property def shape(self): """ Gets/sets the waveform shape of the specified channel. The AWG will use the internal function generator for these shapes. :type: `TekAWG2000.Shape` """ 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(f"FG:{self._name}:SHAP {newval.value}") # ENUMS # class Polarity(Enum): """ Enum containing valid polarity modes for the AWG2000 """ normal = "NORMAL" inverted = "INVERTED" class Shape(Enum): """ Enum containing valid waveform shape modes for hte AWG2000 """ sine = "SINUSOID" pulse = "PULSE" ramp = "RAMP" square = "SQUARE" triangle = "TRIANGLE" # Properties # @property def waveform_name(self): """ Gets/sets the destination waveform name for upload. This is the file name that will be used on the AWG for any following waveform data that is uploaded. :type: `str` """ return self.query("DATA:DEST?").strip() @waveform_name.setter def waveform_name(self, newval): if not isinstance(newval, str): raise TypeError("Waveform name must be specified as a string.") self.sendcmd(f'DATA:DEST "{newval}"') @property def channel(self): """ Gets a specific channel on the AWG2000. The desired channel is accessed like one would access a list. Example usage: >>> import instruments as ik >>> inst = ik.tektronix.TekAWG2000.open_gpibusb("/dev/ttyUSB0", 1) >>> print(inst.channel[0].frequency) :return: A channel object for the AWG2000 :rtype: `TekAWG2000.Channel` """ return ProxyList(self, self.Channel, range(2)) # METHODS # def upload_waveform(self, yzero, ymult, xincr, waveform): """ Uploads a waveform from the PC to the instrument. :param yzero: Y-axis origin offset :type yzero: `float` or `int` :param ymult: Y-axis data point multiplier :type ymult: `float` or `int` :param xincr: X-axis data point increment :type xincr: `float` or `int` :param `numpy.ndarray` waveform: Numpy array of values representing the waveform to be uploaded. This array should be normalized. This means 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") if not isinstance(ymult, float) and not isinstance(ymult, int): raise TypeError("ymult must be specified as a float or int") 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, numpy.ndarray): raise TypeError("waveform must be specified as a numpy array") if numpy.max(numpy.abs(waveform)) > 1: raise ValueError("The max value for an element in waveform is 1.") self.sendcmd(f"WFMP:YZERO {yzero}") self.sendcmd(f"WFMP:YMULT {ymult}") self.sendcmd(f"WFMP:XINCR {xincr}") 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() """ class DataSource(Oscilloscope.DataSource): """ Class representing a data source (channel, math, or ref) on the Tektronix DPO 4104. .. warning:: This class should NOT be manually created by the user. It is designed to be initialized by the `TekDPO4104` class. """ 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 __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 return other.name == self.name __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=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): """ Class representing a channel on the Tektronix DPO 4104. 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. """ 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: `TekDPO4104.Coupling` """ return TekDPO4104.Coupling(self._tek.query(f"CH{self._idx}:COUPL?")) @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)) ) self._tek.sendcmd(f"CH{self._idx}:COUPL {newval.value}") # ENUMS # class Coupling(Enum): """ Enum containing valid coupling modes for the channels on the Tektronix DPO 4104 """ ac = "AC" dc = "DC" ground = "GND" # PROPERTIES # @property def channel(self): """ Gets a specific oscilloscope channel object. The desired channel is specified like one would access a list. For instance, this would transfer the waveform from the first channel:: >>> tek = ik.tektronix.TekDPO4104.open_tcpip("192.168.0.2", 8888) >>> [x, y] = tek.channel[0].read_waveform() :rtype: `TekDPO4104.Channel` """ return ProxyList(self, self.Channel, range(4)) @property def ref(self): """ Gets a specific oscilloscope reference channel object. The desired channel is specified like one would access a list. For instance, this would transfer the waveform from the first channel:: >>> import instruments as ik >>> tek = ik.tektronix.TekDPO4104.open_tcpip("192.168.0.2", 8888) >>> [x, y] = tek.ref[0].read_waveform() :rtype: `TekDPO4104.DataSource` """ 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: `TekDPO4104.DataSource` """ return self.DataSource(self, "MATH") @property def data_source(self): """ Gets/sets the the data source for waveform transfer. """ name = self.query("DAT:SOU?") if name.startswith("CH"): return self.Channel(self, int(name[2:]) - 1) return self.DataSource(self, name) @data_source.setter def data_source(self, newval): # TODO: clean up type-checking here. if not isinstance(newval, str): if hasattr(newval, "value"): # Is an enum with a value. newval = newval.value elif hasattr(newval, "name"): # Is a datasource with a name. newval = newval.name self.sendcmd(f"DAT:SOU {newval}") sleep(0.01) # Let the instrument catch up. @property def aquisition_length(self): """ Gets/sets the aquisition length of the oscilloscope :type: `int` """ return int(self.query("HOR:RECO?")) @aquisition_length.setter def aquisition_length(self, newval): self.sendcmd(f"HOR:RECO {newval}") @property def aquisition_running(self): """ Gets/sets the aquisition state of the attached instrument. This property is `True` if the aquisition is running, and is `False` otherwise. :type: `bool` """ return bool(int(self.query("ACQ:STATE?").strip())) @aquisition_running.setter def aquisition_running(self, newval): self.sendcmd(f"ACQ:STATE {1 if newval else 0}") @property def aquisition_continuous(self): """ Gets/sets whether the aquisition is continuous ("run/stop mode") or whether aquisiton halts after the next sequence ("single mode"). :type: `bool` """ return self.query("ACQ:STOPA?").strip().startswith("RUNST") @aquisition_continuous.setter def aquisition_continuous(self, newval): self.sendcmd("ACQ:STOPA {}".format("RUNST" if newval else "SEQ")) @property def data_width(self): """ Gets/sets the data width (number of bytes wide per data point) for waveforms transfered to/from the oscilloscope. Valid widths are 1 or 2. :type: `int` """ return int(self.query("DATA:WIDTH?")) @data_width.setter def data_width(self, newval): if int(newval) not in [1, 2]: raise ValueError("Only one or two byte-width is supported.") self.sendcmd(f"DATA:WIDTH {newval}") # TODO: convert to read in unitful quantities. @property def y_offset(self): """ Gets/sets the Y offset of the currently selected data source. """ yoffs = float(self.query("WFMP:YOF?")) return yoffs @y_offset.setter def y_offset(self, newval): self.sendcmd(f"WFMP:YOF {newval}") # METHODS # def force_trigger(self): """ Forces a trigger event to occur on the attached oscilloscope. Note that this is distinct from the standard SCPI ``*TRG`` functionality. """ self.sendcmd("TRIG FORCE") ================================================ FILE: src/instruments/tektronix/tekdpo70000.py ================================================ #!/usr/bin/env python """ Provides support for the Tektronix DPO 70000 oscilloscope series """ # IMPORTS ##################################################################### import abc from enum import Enum import time 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, ) # CLASSES ##################################################################### # pylint: disable=too-many-lines class TekDPO70000(SCPIInstrument, Oscilloscope): """ The Tektronix DPO70000 series is a multi-channel oscilloscope with analog bandwidths ranging up to 33GHz. This class inherits from `~instruments.generic_scpi.SCPIInstrument`. Example usage: >>> import instruments as ik >>> tek = ik.tektronix.TekDPO70000.open_tcpip("192.168.0.2", 8888) >>> [x, y] = tek.channel[0].read_waveform() """ # CONSTANTS # # The number of horizontal and vertical divisions. HOR_DIVS = 10 VERT_DIVS = 10 # ENUMS # class AcquisitionMode(Enum): """ Enum containing valid acquisition modes for the Tektronix 70000 series oscilloscopes. """ sample = "SAM" peak_detect = "PEAK" hi_res = "HIR" average = "AVE" waveform_db = "WFMDB" envelope = "ENV" class AcquisitionState(Enum): """ Enum containing valid acquisition states for the Tektronix 70000 series oscilloscopes. """ on = "ON" off = "OFF" run = "RUN" stop = "STOP" class StopAfter(Enum): """ Enum containing valid stop condition modes for the Tektronix 70000 series oscilloscopes. """ run_stop = "RUNST" sequence = "SEQ" class SamplingMode(Enum): """ Enum containing valid sampling modes for the Tektronix 70000 series oscilloscopes. """ real_time = "RT" equivalent_time_allowed = "ET" interpolation_allowed = "IT" class HorizontalMode(Enum): """ Enum containing valid horizontal scan modes for the Tektronix 70000 series oscilloscopes. """ auto = "AUTO" constant = "CONST" manual = "MAN" 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" binary = "BINARY" 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! class ByteOrder(Enum): """ Enum containing valid byte order (big-/little-endian) for the Tektronix 70000 series oscilloscopes. """ little_endian = "LSB" big_endian = "MSB" class TriggerState(Enum): """ Enum containing valid trigger states for the Tektronix 70000 series oscilloscopes. """ armed = "ARMED" auto = "AUTO" dpo = "DPO" partial = "PARTIAL" ready = "READY" # STATIC METHODS # @staticmethod def _dtype(binary_format, byte_order, 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(Oscilloscope.DataSource): """ Class representing a data source (channel, math, or ref) on the Tektronix DPO 70000. .. warning:: This class should NOT be manually created by the user. It is designed to be initialized by the `TekDPO70000` class. """ @property def name(self): return self._name @abc.abstractmethod def _scale_raw_data(self, data): """ Takes the int16 data and figures out how to make it unitful. """ # pylint: disable=protected-access def read_waveform(self, bin_format=True): # We want to get the data back in binary, as it's just too much # otherwise. with self: self._parent.select_fastest_encoding() n_bytes = self._parent.outgoing_n_bytes dtype = self._parent._dtype( self._parent.outgoing_binary_format, self._parent.outgoing_byte_order, n_bytes=None, ) self._parent.sendcmd("CURV?") raw = self._parent.binblockread(n_bytes, fmt=dtype) # Clear the queue by reading the end of line character self._parent._file.read_raw(1) return self._scale_raw_data(raw) 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 def __exit__(self, type, value, traceback): if self._old_dsrc is not None: self._parent.data_source = self._old_dsrc class Math(DataSource): """ Class representing a math channel on the Tektronix DPO 70000. This class inherits from `TekDPO70000.DataSource`. .. warning:: This class should NOT be manually created by the user. It is designed to be initialized by the `TekDPO70000` class. """ def __init__(self, parent, idx): self._parent = parent self._idx = idx + 1 # 1-based. # Initialize as a data source with name MATH{}. super().__init__(parent, f"MATH{self._idx}") def sendcmd(self, cmd): """ Wraps commands sent from property factories in this class with identifiers for the specified math channel. :param str cmd: Command to send to the instrument """ self._parent.sendcmd(f"MATH{self._idx}:{cmd}") def query(self, cmd, size=-1): """ Wraps queries sent from property factories in this class with identifiers for the specified math channel. :param str cmd: Query command to send to the instrument :param int size: Number of characters to read from the response. Default value reads until a termination character is found. :return: The query response :rtype: `str` """ 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" class Mag(Enum): """ Enum containing valid amplitude units for a math channel on the TekDPO70000 series oscilloscope. """ linear = "LINEA" db = "DB" dbm = "DBM" class Phase(Enum): """ Enum containing valid phase units for a math channel on the TekDPO70000 series oscilloscope. """ degrees = "DEG" radians = "RAD" group_delay = "GROUPD" class SpectralWindow(Enum): """ Enum containing valid spectral windows for a math channel on the TekDPO70000 series oscilloscope. """ rectangular = "RECTANG" hamming = "HAMM" hanning = "HANN" kaiser_besse = "KAISERB" blackman_harris = "BLACKMANH" flattop2 = "FLATTOP2" gaussian = "GAUSS" tek_exponential = "TEKEXP" define = string_property( "DEF", doc=""" A text string specifying the math to do, ex. CH1+CH2 """, ) filter_mode = enum_property("FILT:MOD", FilterMode) 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( "NUMAV", doc=""" The number of acquisistions over which exponential averaging is performed. """, ) spectral_center = unitful_property( "SPEC:CENTER", u.Hz, doc=""" The desired frequency of the spectral analyzer output data span in Hz. """, ) spectral_gatepos = unitful_property( "SPEC:GATEPOS", u.second, doc=""" The gate position. Units are represented in seconds, with respect to trigger position. """, ) spectral_gatewidth = unitful_property( "SPEC:GATEWIDTH", 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_mag = enum_property( "SPEC:MAG", Mag, doc=""" Whether the spectral magnitude is linear, db, or dbm. """, ) spectral_phase = enum_property( "SPEC:PHASE", Phase, doc=""" Whether the spectral phase is degrees, radians, or group delay. """, ) spectral_reflevel = unitless_property( "SPEC:REFL", doc=""" The value that represents the topmost display screen graticule. The units depend on spectral_mag. """, ) spectral_reflevel_offset = unitless_property("SPEC:REFLEVELO") spectral_resolution_bandwidth = unitful_property( "SPEC:RESB", u.Hz, doc=""" The desired resolution bandwidth value. Units are represented in Hertz. """, ) spectral_span = unitful_property( "SPEC:SPAN", u.Hz, doc=""" Specifies the frequency span of the output data vector from the spectral analyzer. """, ) spectral_suppress = unitless_property( "SPEC:SUPP", doc=""" The magnitude level that data with magnitude values below this value are displayed as zero phase. """, ) spectral_unwrap = bool_property( "SPEC:UNWR", inst_true="ON", inst_false="OFF", doc=""" Enables or disables phase wrapping. """, ) spectral_window = enum_property("SPEC:WIN", SpectralWindow) threshhold = unitful_property( "THRESH", 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( "VERT:AUTOSC", inst_true="ON", 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", 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 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, Oscilloscope.Channel): """ Class representing a channel on the Tektronix DPO 70000. This class inherits from `TekDPO70000.DataSource`. .. warning:: This class should NOT be manually created by the user. It is designed to be initialized by the `TekDPO70000` class. """ def __init__(self, parent, idx): self._parent = parent self._idx = idx + 1 # 1-based. # Initialize as a data source with name CH{}. super().__init__(self._parent, f"CH{self._idx}") 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"CH{self._idx}:{cmd}") def query(self, cmd, size=-1): """ Wraps queries sent from property factories in this class with identifiers for the specified channel. :param str cmd: Query command to send to the instrument :param int size: Number of characters to read from the response. Default value reads until a termination character is found. :return: The query response :rtype: `str` """ 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" ground = "GND" coupling = enum_property( "COUP", Coupling, doc=""" Gets/sets the coupling for the specified channel. Example usage: >>> import instruments as ik >>> inst = ik.tektronix.TekDPO70000.open_tcpip("192.168.0.1", 8080) >>> channel = inst.channel[0] >>> channel.coupling = channel.Coupling.ac """, ) bandwidth = unitful_property("BAN", u.Hz) deskew = unitful_property("DESK", u.second) termination = unitful_property("TERM", u.ohm) 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. """, ) offset = unitful_property( "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", 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", 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): 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 ## @property def channel(self): return ProxyList(self, self.Channel, range(4)) @property def math(self): return ProxyList(self, self.Math, range(4)) @property def ref(self): raise NotImplementedError # 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="", 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", ) acquire_interp_8bit = string_property( "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_mode = enum_property("ACQ:MOD", AcquisitionMode) acquire_mode_actual = enum_property("ACQ:MOD:ACT", AcquisitionMode, readonly=True) acquire_num_acquisitions = int_property( "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", doc=""" The number of waveform acquisitions to average. """, ) acquire_num_envelop = int_property( "ACQ:NUME", doc=""" The number of waveform acquisitions to be enveloped """, ) acquire_num_frames = int_property( "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", 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_state = enum_property( "ACQ:STATE", AcquisitionState, doc=""" This command starts or stops acquisitions. """, ) acquire_stop_after = enum_property( "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_framestop = int_property("DAT:FRAMESTOP") data_start = int_property( "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", doc=""" The last data point that will be transferred. """, ) data_sync_sources = bool_property("DAT:SYNCSOU", inst_true="ON", inst_false="OFF") @property def data_source(self): """ Gets/sets the data source for the oscilloscope. This will return the actual Channel/Math/DataSource object as if it was accessed through the usual `TekDPO70000.channel`, `TekDPO70000.math`, or `TekDPO70000.ref` properties. :type: `TekDPO70000.Channel` or `TekDPO70000.Math` """ val = self.query("DAT:SOU?") if val[0:2] == "CH": out = self.channel[int(val[2]) - 1] elif val[0:2] == "MA": out = self.math[int(val[4]) - 1] elif val[0:2] == "RE": out = self.ref[int(val[3]) - 1] else: raise NotImplementedError return out @data_source.setter def data_source(self, newval): if not isinstance(newval, self.DataSource): 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. time.sleep(0.02) horiz_acq_duration = unitful_property( "HOR:ACQDURATION", u.second, readonly=True, doc=""" The duration of the acquisition. """, ) horiz_acq_length = int_property( "HOR:ACQLENGTH", readonly=True, doc=""" The record length. """, ) horiz_delay_mode = bool_property("HOR:DEL:MOD", inst_true="1", inst_false="0") horiz_delay_pos = unitful_property( "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", u.second, doc=""" The base trigger delay time setting. """, ) horiz_interp_ratio = unitless_property( "HOR:MAI:INTERPR", readonly=True, doc=""" The ratio of interpolated points to measured points. """, ) horiz_main_pos = unitful_property( "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_mode = enum_property("HOR:MODE", HorizontalMode) horiz_record_length_lim = int_property( "HOR:MODE:AUTO:LIMIT", doc=""" The recond length limit in samples. """, ) horiz_record_length = int_property( "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", u.Hz, doc=""" The sample rate in samples per second. """, ) horiz_scale = unitful_property( "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", 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="", doc=""" Valid arguments are AUTO, OFF, and ON. """, ) trigger_state = enum_property("TRIG:STATE", TriggerState) # Waveform Transfer Properties outgoing_waveform_encoding = enum_property( "WFMO:ENC", WaveformEncoding, doc=""" Controls the encoding used for outgoing waveforms (instrument → host). """, ) outgoing_binary_format = enum_property( "WFMO:BN_F", BinaryFormat, doc=""" Controls the data type of samples when transferring waveforms from the instrument to the host using binary encoding. """, ) outgoing_byte_order = enum_property( "WFMO:BYT_O", ByteOrder, doc=""" Controls whether binary data is returned in little or big endian. """, ) outgoing_n_bytes = int_property( "WFMO:BYT_N", 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 # def select_fastest_encoding(self): """ Sets the encoding for data returned by this instrument to be the fastest encoding method consistent with the current data source. """ self.sendcmd("DAT:ENC FAS") def force_trigger(self): """ Forces a trigger event to happen for the oscilloscope. """ self.sendcmd("TRIG FORC") # TODO: consider moving the next few methods to Oscilloscope. def run(self): """ Enables the trigger for the oscilloscope. """ self.sendcmd(":RUN") def stop(self): """ Disables the trigger for the oscilloscope. """ self.sendcmd(":STOP") ================================================ FILE: src/instruments/tektronix/tektds224.py ================================================ #!/usr/bin/env python """ Provides support for the Tektronix TDS 224 oscilloscope """ # IMPORTS ##################################################################### import time from enum import Enum 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 from instruments.units import ureg as u # CLASSES ##################################################################### class TekTDS224(SCPIInstrument, Oscilloscope): """ The Tektronix TDS224 is a multi-channel oscilloscope with analog bandwidths of 100MHz. This class inherits from `~instruments.generic_scpi.SCPIInstrument`. Example usage: >>> import instruments as ik >>> tek = ik.tektronix.TekTDS224.open_gpibusb("/dev/ttyUSB0", 1) >>> [x, y] = tek.channel[0].read_waveform() """ def __init__(self, filelike): super().__init__(filelike) self._file.timeout = 3 * u.second class DataSource(Oscilloscope.DataSource): """ 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. """ 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=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. This class inherits from `TekTDS224.DataSource`. .. warning:: This class should NOT be manually created by the user. It is designed to be initialized by the `TekTDS224` 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: `TekTDS224.Coupling` """ return TekTDS224.Coupling(self._tek.query(f"CH{self._idx}:COUPL?")) @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 # class Coupling(Enum): """ Enum containing valid coupling modes for the Tek TDS224 """ ac = "AC" dc = "DC" ground = "GND" # PROPERTIES # @property def channel(self): """ Gets a specific oscilloscope channel object. The desired channel is specified like one would access a list. For instance, this would transfer the waveform from the first channel:: >>> import instruments as ik >>> tek = ik.tektronix.TekTDS224.open_tcpip('192.168.0.2', 8888) >>> [x, y] = tek.channel[0].read_waveform() :rtype: `TekTDS224.Channel` """ return ProxyList(self, self.Channel, range(4)) @property def ref(self): """ Gets a specific oscilloscope reference channel object. The desired channel is specified like one would access a list. For instance, this would transfer the waveform from the first channel:: >>> import instruments as ik >>> tek = ik.tektronix.TekTDS224.open_tcpip('192.168.0.2', 8888) >>> [x, y] = tek.ref[0].read_waveform() :rtype: `TekTDS224.DataSource` """ 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: `TekTDS224.DataSource` """ return self.DataSource(self, "MATH") @property def data_source(self): """ Gets/sets the the data source for waveform transfer. """ name = self.query("DAT:SOU?") if name.startswith("CH"): return self.Channel(self, int(name[2:]) - 1) return self.DataSource(self, name) @data_source.setter def data_source(self, newval): # TODO: clean up type-checking here. if not isinstance(newval, str): if hasattr(newval, "value"): # Is an enum with a value. newval = newval.value elif hasattr(newval, "name"): # Is a datasource with a name. newval = newval.name self.sendcmd(f"DAT:SOU {newval}") time.sleep(0.01) # Let the instrument catch up. @property def data_width(self): """ Gets/sets the byte-width of the data points being returned by the instrument. Valid widths are ``1`` or ``2``. :type: `int` """ return int(self.query("DATA:WIDTH?")) @data_width.setter def data_width(self, newval): if int(newval) not in [1, 2]: raise ValueError("Only one or two byte-width is supported.") self.sendcmd(f"DATA:WIDTH {newval}") def force_trigger(self): raise NotImplementedError ================================================ FILE: src/instruments/tektronix/tektds5xx.py ================================================ #!/usr/bin/env python # # tektds5xx.py: Driver for the Tektronix TDS 5xx series oscilloscope. # # © 2014 Chris Schimp (silverchris@gmail.com) # # Modified from tektds224.py # © 2013 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 . # """ Provides support for the Tektronix DPO 500 oscilloscope series. Originally contributed by Chris Schimp (silverchris@gmail.com) in 2014. Based off of tektds224.py written by Steven Casagrande. """ # IMPORTS ##################################################################### from datetime import datetime from enum import Enum from functools import reduce import operator import struct import time 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 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 """ 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 return self._data 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. """ @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: # 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=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. :type: `float` """ return float(self._parent.query(f"CH{self._idx}:SCA?")) @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 ## class Coupling(Enum): """ Available coupling options for input sources and trigger """ ac = "AC" dc = "DC" ground = "GND" class Bandwidth(Enum): """ Bandwidth in MHz """ Twenty = "TWE" OneHundred = "HUN" TwoHundred = "TWO" FULL = "FUL" class Impedance(Enum): """ Available options for input source impedance """ Fifty = "FIF" OneMeg = "MEG" class Edge(Enum): """ Available Options for trigger slope """ Rising = "RIS" Falling = "FALL" class Trigger(Enum): """ Available Trigger sources (AUX not Available on TDS520A/TDS540A) """ CH1 = "CH1" CH2 = "CH2" CH3 = "CH3" CH4 = "CH4" AUX = "AUX" LINE = "LINE" class Source(Enum): """ Available Data sources """ CH1 = "CH1" CH2 = "CH2" CH3 = "CH3" CH4 = "CH4" Math1 = "MATH1" Math2 = "MATH2" Math3 = "MATH3" Ref1 = "REF1" Ref2 = "REF2" Ref3 = "REF3" Ref4 = "REF4" # PROPERTIES ## @property def measurement(self): """ Gets a specific oscilloscope measurement object. The desired channel is specified like one would access a list. :rtype: `TekTDS5xx.Measurement` """ return ProxyList(self, self.Measurement, range(3)) @property def channel(self): """ Gets a specific oscilloscope channel object. The desired channel is specified like one would access a list. For instance, this would transfer the waveform from the first channel:: >>> tek = ik.tektronix.TekTDS5xx.open_tcpip('192.168.0.2', 8888) >>> [x, y] = tek.channel[0].read_waveform() :rtype: `TekTDS5xx.Channel` """ return ProxyList(self, self.Channel, range(4)) @property def ref(self): """ Gets a specific oscilloscope reference channel object. The desired channel is specified like one would access a list. For instance, this would transfer the waveform from the first channel:: >>> tek = ik.tektronix.TekTDS5xx.open_tcpip('192.168.0.2', 8888) >>> [x, y] = tek.ref[0].read_waveform() :rtype: `TekTDS5xx.DataSource` """ 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: `TekTDS5xx.DataSource` """ return ProxyList( self, lambda s, idx: self.DataSource(s, f"MATH{idx + 1}"), range(3), ) @property def sources(self): """ Returns list of all active sources :rtype: `list` """ active = [] channels = list(map(int, self.query("SEL?").split(";")[0:11])) for idx in range(0, 4): if channels[idx]: active.append(self.Channel(self, idx)) for idx in range(4, 7): if channels[idx]: active.append(self.DataSource(self, f"MATH{idx - 3}")) for idx in range(7, 11): if channels[idx]: active.append(self.DataSource(self, f"REF{idx - 6}")) return active @property def data_source(self): """ Gets/sets the the data source for waveform transfer. :type: `TekTDS5xx.Source` or `TekTDS5xx.DataSource` :rtype: `TekTDS5xx.DataSource` """ name = self.query("DAT:SOU?") if name.startswith("CH"): return self.Channel(self, int(name[2:]) - 1) return self.DataSource(self, name) @data_source.setter def data_source(self, newval): 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)) ) self.sendcmd(f"DAT:SOU {newval.value}") time.sleep(0.01) # Let the instrument catch up. @property def data_width(self): """ Gets/Sets the data width for waveform transfers :type: `int` """ return int(self.query("DATA:WIDTH?")) @data_width.setter def data_width(self, newval): if int(newval) not in [1, 2]: raise ValueError("Only one or two byte-width is supported.") self.sendcmd(f"DATA:WIDTH {newval}") def force_trigger(self): raise NotImplementedError @property def horizontal_scale(self): """ Get/Set Horizontal Scale :type: `float` """ return float(self.query("HOR:MAI:SCA?")) @horizontal_scale.setter def horizontal_scale(self, newval): 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) ) @property def trigger_level(self): """ Get/Set trigger level :type: `float` """ return float(self.query("TRIG:MAI:LEV?")) @trigger_level.setter def trigger_level(self, newval): 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) ) @property def trigger_coupling(self): """ Get/Set trigger coupling :type: `TekTDS5xx.Coupling` """ 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)) ) self.sendcmd(f"TRIG:MAI:EDGE:COUP {newval.value}") @property def trigger_slope(self): """ Get/Set trigger slope :type: `TekTDS5xx.Edge` """ return TekTDS5xx.Edge(self.query("TRIG:MAI:EDGE:SLO?")) @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)) ) self.sendcmd(f"TRIG:MAI:EDGE:SLO {newval.value}") @property def trigger_source(self): """ Get/Set trigger source :type: `TekTDS5xx.Trigger` """ return TekTDS5xx.Trigger(self.query("TRIG:MAI:EDGE:SOU?")) @trigger_source.setter def trigger_source(self, newval): if not isinstance(newval, TekTDS5xx.Trigger): raise TypeError( "Trigger source setting must be a " "`TekTDS5xx.Trigger` value, got {} " "instead.".format(type(newval)) ) self.sendcmd(f"TRIG:MAI:EDGE:SOU {newval.value}") @property def clock(self): """ Get/Set oscilloscope clock :type: `datetime.datetime` """ 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)) ) self.sendcmd(newval.strftime('DATE "%Y-%m-%d";:TIME "%H:%M:%S"')) @property def display_clock(self): """ Get/Set the visibility of clock on the display :type: `bool` """ 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(f"DISPLAY:CLOCK {int(newval)}") def get_hardcopy(self): """ Gets a screenshot of the display :rtype: `string` """ 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=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().__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().__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 assumed that 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 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." ) ================================================ FILE: src/instruments/thorlabs/__init__.py ================================================ #!/usr/bin/env python """ Module containing Thorlabs instruments """ from .thorlabsapt import ( ThorLabsAPT, APTPiezoInertiaActuator, APTPiezoStage, APTStrainGaugeReader, APTMotorController, ) from .pm100usb import PM100USB from .lcc25 import LCC25 from .sc10 import SC10 from .tc200 import TC200 ================================================ FILE: src/instruments/thorlabs/_abstract.py ================================================ #!/usr/bin/env python """ Defines a generic Thorlabs instrument to define some common functionality. """ # IMPORTS ##################################################################### import time from instruments.units import ureg as u from instruments.thorlabs import _packets from instruments.abstract_instruments.instrument import Instrument from instruments.util_fns import assume_units # CLASSES ##################################################################### class ThorLabsInstrument(Instrument): """ Generic class for ThorLabs instruments which require wrapping of commands and queries in packets. """ def __init__(self, filelike): super().__init__(filelike) self.terminator = "" def sendpacket(self, packet): """ Sends a packet to the connected APT instrument, and waits for a packet in response. Optionally, checks whether the received packet type is matches that the caller expects. :param packet: The thorlabs data packet that will be queried :type packet: `ThorLabsPacket` """ self._file.write_raw(packet.pack()) # pylint: disable=protected-access def querypacket(self, packet, expect=None, timeout=None, expect_data_len=None): """ Sends a packet to the connected APT instrument, and waits for a packet in response. Optionally, checks whether the received packet type is matches that the caller expects. :param packet: The thorlabs data packet that will be queried :type packet: `ThorLabsPacket` :param expect: The expected message id from the response. If an an incorrect id is received then an `IOError` is raised. If left with the default value of `None` then no checking occurs. :type expect: `str` or `None` :param timeout: Sets a timeout to wait before returning `None`, indicating no packet was received. If the timeout is set to `None`, then the timeout is inherited from the underlying communicator and no additional timeout is added. If timeout is set to `False`, then this method waits indefinitely. If timeout is set to a unitful quantity, then it is interpreted as a time and used as the timeout value. Finally, if the timeout is a unitless number (e.g. `float` or `int`), then seconds are assumed. :param int expect_data_len: Number of bytes to expect as the data for the returned packet. :return: Returns the response back from the instrument wrapped up in a ThorLabs APT packet, or None if no packet was received. :rtype: `ThorLabsPacket` """ t_start = time.time() if timeout is not None: timeout = assume_units(timeout, u.second).to("second").magnitude while True: self._file.write_raw(packet.pack()) resp = self._file.read_raw( expect_data_len + 6 # the header is six bytes. if expect_data_len else 6 ) if resp or timeout is None: break else: tic = time.time() if tic - t_start > timeout: break if not resp: if expect is None: return None else: 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 OSError( "APT returned message ID {}, expected {}".format( pkt._message_id, expect ) ) return pkt ================================================ FILE: src/instruments/thorlabs/_cmds.py ================================================ #!/usr/bin/python """ Contains command mneonics for the ThorLabs APT protocol Class originally contributed by Catherine Holloway. """ # IMPORTS ##################################################################### from enum import IntEnum # CLASSES ##################################################################### class ThorLabsCommands(IntEnum): """ Enum containing command mneonics for the ThorLabs APT protocol """ # General System Commands MOD_IDENTIFY = 0x0223 MOD_SET_CHANENABLESTATE = 0x0210 MOD_REQ_CHANENABLESTATE = 0x0211 MOD_GET_CHANENABLESTATE = 0x0212 HW_DISCONNECT = 0x0002 HW_RESPONSE = 0x0080 HW_RICHRESPONSE = 0x0081 HW_START_UPDATEMSGS = 0x0011 HW_STOP_UPDATEMSGS = 0x0012 HW_REQ_INFO = 0x0005 HW_GET_INFO = 0x0006 RACK_REQ_BAYUSED = 0x0060 RACK_GET_BAYUSED = 0x0061 HUB_REQ_BAYUSED = 0x0065 HUB_GET_BAYUSED = 0x0066 RACK_REQ_STATUSBITS = 0x0226 RACK_GET_STATUSBITS = 0x0227 RACK_SET_DIGOUTPUTS = 0x0228 RACK_REQ_DIGOUTPUTS = 0x0229 RACK_GET_DIGOUTPUTS = 0x0230 MOD_SET_DIGOUTPUTS = 0x0213 MOD_REQ_DIGOUTPUTS = 0x0214 MOD_GET_DIGOUTPUTS = 0x0215 # Motor Control Messages MOT_SET_POSCOUNTER = 0x0410 MOT_REQ_POSCOUNTER = 0x0411 MOT_GET_POSCOUNTER = 0x0412 MOT_SET_ENCCOUNTER = 0x0409 MOT_REQ_ENCCOUNTER = 0x040A MOT_GET_ENCCOUNTER = 0x040B MOT_SET_VELPARAMS = 0x0413 MOT_REQ_VELPARAMS = 0x0414 MOT_GET_VELPARAMS = 0x0415 MOT_SET_JOGPARAMS = 0x0416 MOT_REQ_JOGPARAMS = 0x0417 MOT_GET_JOGPARAMS = 0x0418 MOT_REQ_ADCINPUTS = 0x042B MOT_GET_ADCINPUTS = 0x042C MOT_SET_POWERPARAMS = 0x0426 MOT_REQ_POWERPARAMS = 0x0427 MOT_GET_POWERPARAMS = 0x0428 MOT_SET_GENMOVEPARAMS = 0x043A MOT_REQ_GENMOVEPARAMS = 0x043B MOT_GET_GENMOVEPARAMS = 0x043C MOT_SET_MOVERELPARAMS = 0x0445 MOT_REQ_MOVERELPARAMS = 0x0446 MOT_GET_MOVERELPARAMS = 0x0447 MOT_SET_MOVEABSPARAMS = 0x0450 MOT_REQ_MOVEABSPARAMS = 0x0451 MOT_GET_MOVEABSPARAMS = 0x0452 MOT_SET_HOMEPARAMS = 0x0440 MOT_REQ_HOMEPARAMS = 0x0441 MOT_GET_HOMEPARAMS = 0x0442 MOT_SET_LIMSWITCHPARAMS = 0x0423 MOT_REQ_LIMSWITCHPARAMS = 0x0424 MOT_GET_LIMSWITCHPARAMS = 0x0425 MOT_MOVE_HOME = 0x0443 MOT_MOVE_HOMED = 0x0444 MOT_MOVE_RELATIVE = 0x0448 MOT_MOVE_COMPLETED = 0x0464 MOT_MOVE_ABSOLUTE = 0x0453 MOT_MOVE_JOG = 0x046A MOT_MOVE_VELOCITY = 0x0457 MOT_MOVE_STOP = 0x0465 MOT_MOVE_STOPPED = 0x0466 MOT_SET_DCPIDPARAMS = 0x04A0 MOT_REQ_DCPIDPARAMS = 0x04A1 MOT_GET_DCPIDPARAMS = 0x04A2 MOT_SET_AVMODES = 0x04B3 MOT_REQ_AVMODES = 0x04B4 MOT_GET_AVMODES = 0x04B5 MOT_SET_POTPARAMS = 0x04B0 MOT_REQ_POTPARAMS = 0x04B1 MOT_GET_POTPARAMS = 0x04B2 MOT_SET_BUTTONPARAMS = 0x04B6 MOT_REQ_BUTTONPARAMS = 0x04B7 MOT_GET_BUTTONPARAMS = 0x04B8 MOT_SET_EEPROMPARAMS = 0x04B9 MOT_SET_PMDPOSITIONLOOPPARAMS = 0x04D7 MOT_REQ_PMDPOSITIONLOOPPARAMS = 0x04D8 MOT_GET_PMDPOSITIONLOOPPARAMS = 0x04D9 MOT_SET_PMDMOTOROUTPUTPARAMS = 0x04DA MOT_REQ_PMDMOTOROUTPUTPARAMS = 0x04DB MOT_GET_PMDMOTOROUTPUTPARAMS = 0x04DC MOT_SET_PMDTRACKSETTLEPARAMS = 0x04E0 MOT_REQ_PMDTRACKSETTLEPARAMS = 0x04E1 MOT_GET_PMDTRACKSETTLEPARAMS = 0x04E2 MOT_SET_PMDPROFILEMODEPARAMS = 0x04E3 MOT_REQ_PMDPROFILEMODEPARAMS = 0x04E4 MOT_GET_PMDPROFILEMODEPARAMS = 0x04E5 MOT_SET_PMDJOYSTICKPARAMS = 0x04E6 MOT_REQ_PMDJOYSTICKPARAMS = 0x04E7 MOT_GET_PMDJOYSTICKPARAMS = 0x04E8 MOT_SET_PMDCURRENTLOOPPARAMS = 0x04D4 MOT_REQ_PMDCURRENTLOOPPARAMS = 0x04D5 MOT_GET_PMDCURRENTLOOPPARAMS = 0x04D6 MOT_SET_PMDSETTLEDCURRENTLOOPPARAMS = 0x04E9 MOT_REQ_PMDSETTLEDCURRENTLOOPPARAMS = 0x04EA MOT_GET_PMDSETTLEDCURRENTLOOPPARAMS = 0x04EB MOT_SET_PMDSTAGEAXISPARAMS = 0x04F0 MOT_REQ_PMDSTAGEAXISPARAMS = 0x04F1 MOT_GET_PMDSTAGEAXISPARAMS = 0x04F2 MOT_GET_STATUSUPDATE = 0x0481 MOT_REQ_STATUSUPDATE = 0x0480 MOT_GET_DCSTATUSUPDATE = 0x0491 MOT_REQ_DCSTATUSUPDATE = 0x0490 MOT_ACK_DCSTATUSUPDATE = 0x0492 MOT_REQ_STATUSBITS = 0x0429 MOT_GET_STATUSBITS = 0x042A MOT_SUSPEND_ENDOFMOVEMSGS = 0x046B MOT_RESUME_ENDOFMOVEMSGS = 0x046C MOT_SET_TRIGGER = 0x0500 MOT_REQ_TRIGGER = 0x0501 MOT_GET_TRIGGER = 0x0502 # Solenoid Control Messages MOT_SET_SOL_OPERATINGMODE = 0x04C0 MOT_REQ_SOL_OPERATINGMODE = 0x04C1 MOT_GET_SOL_OPERATINGMODE = 0x04C2 MOT_SET_SOL_CYCLEPARAMS = 0x04C3 MOT_REQ_SOL_CYCLEPARAMS = 0x04C4 MOT_GET_SOL_CYCLEPARAMS = 0x04C5 MOT_SET_SOL_INTERLOCKMODE = 0x04C6 MOT_REQ_SOL_INTERLOCKMODE = 0x04C7 MOT_GET_SOL_INTERLOCKMODE = 0x04C8 MOT_SET_SOL_STATE = 0x04CB MOT_REQ_SOL_STATE = 0x04CC MOT_GET_SOL_STATE = 0x04CD # Piezo Control Messages PZ_SET_POSCONTROLMODE = 0x0640 PZ_REQ_POSCONTROLMODE = 0x0641 PZ_GET_POSCONTROLMODE = 0x0642 PZ_SET_OUTPUTVOLTS = 0x0643 PZ_REQ_OUTPUTVOLTS = 0x0644 PZ_GET_OUTPUTVOLTS = 0x0645 PZ_SET_OUTPUTPOS = 0x0646 PZ_REQ_OUTPUTPOS = 0x0647 PZ_GET_OUTPUTPOS = 0x0648 PZ_SET_INPUTVOLTSSRC = 0x0652 PZ_REQ_INPUTVOLTSSRC = 0x0653 PZ_GET_INPUTVOLTSSRC = 0x0654 PZ_SET_PICONSTS = 0x0655 PZ_REQ_PICONSTS = 0x0656 PZ_GET_PICONSTS = 0x0657 PZ_REQ_PZSTATUSBITS = 0x065B PZ_GET_PZSTATUSBITS = 0x065C PZ_GET_PZSTATUSUPDATE = 0x0661 PZ_ACK_PZSTATUSUPDATE = 0x0662 PZ_SET_OUTPUTLUT = 0x0700 PZ_REQ_OUTPUTLUT = 0x0701 PZ_GET_OUTPUTLUT = 0x0702 PZ_SET_OUTPUTLUTPARAMS = 0x0703 PZ_REQ_OUTPUTLUTPARAMS = 0x0704 PZ_GET_OUTPUTLUTPARAMS = 0x0705 PZ_START_LUTOUTPUT = 0x0706 PZ_STOP_LUTOUTPUT = 0x0707 PZ_SET_EEPROMPARAMS = 0x07D0 PZ_SET_TPZ_DISPSETTINGS = 0x07D1 PZ_REQ_TPZ_DISPSETTINGS = 0x07D2 PZ_GET_TPZ_DISPSETTINGS = 0x07D3 PZ_SET_TPZ_IOSETTINGS = 0x07D4 PZ_REQ_TPZ_IOSETTINGS = 0x07D5 PZ_GET_TPZ_IOSETTINGS = 0x07D6 PZ_SET_ZERO = 0x0658 PZ_REQ_MAXTRAVEL = 0x0650 PZ_GET_MAXTRAVEL = 0x0651 PZ_SET_IOSETTINGS = 0x0670 PZ_REQ_IOSETTINGS = 0x0671 PZ_GET_IOSETTINGS = 0x06723 PZ_SET_OUTPUTMAXVOLTS = 0x0680 PZ_REQ_OUTPUTMAXVOLTS = 0x0681 PZ_GET_OUTPUTMAXVOLTS = 0x0682 PZ_SET_TPZ_SLEWRATES = 0x0683 PZ_REQ_TPZ_SLEWRATES = 0x0684 PZ_GET_TPZ_SLEWRATES = 0x068 MOT_SET_PZSTAGEPARAMDEFAULTS = 0x0686 PZ_SET_LUTVALUETYPE = 0x0708 PZ_SET_TSG_IOSETTINGS = 0x07DA PZ_REQ_TSG_IOSETTINGS = 0x07DB PZ_GET_TSG_IOSETTINGS = 0x07DC PZ_REQ_TSG_READING = 0x07DD PZ_GET_TSG_READING = 0x07DE # NanoTrak Control Messages PZ_SET_NTMODE = 0x0603 PZ_REQ_NTMODE = 0x0604 PZ_GET_NTMODE = 0x0605 PZ_SET_NTTRACKTHRESHOLD = 0x0606 PZ_REQ_NTTRACKTHRESHOLD = 0x0607 PZ_GET_NTTRACKTHRESHOLD = 0x0608 PZ_SET_NTCIRCHOMEPOS = 0x0609 PZ_REQ_NTCIRCHOMEPOS = 0x0610 PZ_GET_NTCIRCHOMEPOS = 0x0611 PZ_MOVE_NTCIRCTOHOMEPOS = 0x0612 PZ_REQ_NTCIRCCENTREPOS = 0x0613 PZ_GET_NTCIRCCENTREPOS = 0x0614 PZ_SET_NTCIRCPARAMS = 0x0618 PZ_REQ_NTCIRCPARAMS = 0x0619 PZ_GET_NTCIRCPARAMS = 0x0620 PZ_SET_NTCIRCDIA = 0x061A PZ_SET_NTCIRCDIALUT = 0x0621 PZ_REQ_NTCIRCDIALUT = 0x0622 PZ_GET_NTCIRCDIALUT = 0x0623 PZ_SET_NTPHASECOMPPARAMS = 0x0626 PZ_REQ_NTPHASECOMPPARAMS = 0x0627 PZ_GET_NTPHASECOMPPARAMS = 0x0628 PZ_SET_NTTIARANGEPARAMS = 0x0630 PZ_REQ_NTTIARANGEPARAMS = 0x0631 PZ_GET_NTTIARANGEPARAMS = 0x0632 PZ_SET_NTGAINPARAMS = 0x0633 PZ_REQ_NTGAINPARAMS = 0x0634 PZ_GET_NTGAINPARAMS = 0x0635 PZ_SET_NTTIALPFILTERPARAMS = 0x0636 PZ_REQ_NTTIALPFILTERPARAMS = 0x0637 PZ_GET_NTTIALPFILTERPARAMS = 0x0638 PZ_REQ_NTTIAREADING = 0x0639 PZ_GET_NTTIAREADING = 0x063A PZ_SET_NTFEEDBACKSRC = 0x063B PZ_REQ_NTFEEDBACKSRC = 0x063C PZ_GET_NTFEEDBACKSRC = 0x063D PZ_REQ_NTSTATUSBITS = 0x063E PZ_GET_NTSTATUSBITS = 0x063F PZ_REQ_NTSTATUSUPDATE = 0x0664 PZ_GET_NTSTATUSUPDATE = 0x0665 PZ_ACK_NTSTATUSUPDATE = 0x0666 NT_SET_EEPROMPARAMS = 0x07E7 NT_SET_TNA_DISPSETTINGS = 0x07E8 NT_REQ_TNA_DISPSETTINGS = 0x07E9 NT_GET_TNA_DISPSETTINGS = 0x07EA NT_SET_TNAIOSETTINGS = 0x07EB NT_REQ_TNAIOSETTINGS = 0x07EC NT_GET_TNAIOSETTINGS = 0x07ED # Laser Control Messages 181 LA_SET_PARAMS = 0x0800 LA_REQ_PARAMS = 0x0801 LA_GET_PARAMS = 0x0802 LA_ENABLEOUTPUT = 0x0811 LA_DISABLEOUTPUT = 0x0812 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 ================================================ FILE: src/instruments/thorlabs/_packets.py ================================================ #!/usr/bin/env python """ Module for working with ThorLabs packets. """ # IMPORTS ##################################################################### import struct # STRUCTS ##################################################################### message_header_nopacket = struct.Struct("> 1) % 2 return TC200.Mode(response_code) @mode.setter def mode(self, newval): if not isinstance(newval, TC200.Mode): raise TypeError( "Mode setting must be a `TC200.Mode` value, " "got {} instead.".format(type(newval)) ) out_query = f"mode={newval.name}" # there is an issue with the TC200; it responds with a spurious # Command Error on mode=normal. Thus, the sendcmd() method cannot # be used. if newval == TC200.Mode.normal: self.prompt = "Command error CMD_ARG_RANGE_ERR\n\r> " self.sendcmd(out_query) self.prompt = "> " else: self.sendcmd(out_query) @property def enable(self): """ Gets/sets the heater enable status. If output enable is on (`True`), there is a voltage on the output. :type: `bool` """ response = self.status return True if int(response) % 2 == 1 else False @enable.setter def enable(self, newval): if not isinstance(newval, bool): raise TypeError( "TC200 enable property must be specified with a " "boolean." ) # the "ens" command is a toggle, we need to track two different cases, # when it should be on and it is off, and when it is off and # should be on # if no sensor is attached, the unit will respond with an error. # There is no current error handling in the way that thorlabs # responds with errors if newval and not self.enable: response1 = self._file.query("ens") while response1 != ">": response1 = self._file.read(1) self._file.read(1) elif not newval and self.enable: response1 = self._file.query("ens") while response1 != ">": response1 = self._file.read(1) self._file.read(1) @property def status(self): """ Gets the the status code of the TC200 :rtype: `int` """ _ = self._file.query("stat?") response = self.read(5) return int(response.split(" ")[0]) temperature = unitful_property( "tact", 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 `~pint.Quantity`) or assumed to be of units degrees C. :type: `~pint.Quantity` or `int` :return: the temperature (in degrees C) :rtype: `~pint.Quantity` """, ) max_temperature = unitful_property( "tmax", units=u.degC, format_code="{:.1f}", set_fmt="{}={}", valid_range=(u.Quantity(20, u.degC), u.Quantity(205, u.degC)), doc=""" Gets/sets the maximum temperature :return: the maximum temperature (in deg C) :units: As specified or assumed to be degree Celsius. Returns with units degC. :rtype: `~pint.Quantity` """, ) @property def temperature_set(self): """ Gets/sets the actual temperature of the sensor :units: As specified (if a `~pint.Quantity`) or assumed to be of units degrees C. :type: `~pint.Quantity` or `int` :return: the temperature (in degrees C) :rtype: `~pint.Quantity` """ response = ( self.query("tset?") .replace(" Celsius", "") .replace(" C", "") .replace(" F", "") .replace(" K", "") ) return u.Quantity(float(response), u.degC) @temperature_set.setter def temperature_set(self, newval): # the set temperature is always in celsius 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 = f"tset={newval.magnitude}" self.sendcmd(out_query) @property def p(self): """ Gets/sets the p-gain. Valid numbers are [1,250]. :return: the p-gain (in nnn) :rtype: `int` """ return self.pid[0] @p.setter def p(self, newval): if newval not in range(1, 251): raise ValueError("P-value not in [1, 250]") self.sendcmd(f"pgain={newval}") @property def i(self): """ Gets/sets the i-gain. Valid numbers are [1,250] :return: the i-gain (in nnn) :rtype: `int` """ return self.pid[1] @i.setter def i(self, newval): if newval not in range(0, 251): raise ValueError("I-value not in [0, 250]") self.sendcmd(f"igain={newval}") @property def d(self): """ Gets/sets the d-gain. Valid numbers are [0, 250] :return: the d-gain (in nnn) :type: `int` """ return self.pid[2] @d.setter def d(self, newval): if newval not in range(0, 251): raise ValueError("D-value not in [0, 250]") self.sendcmd(f"dgain={newval}") @property def pid(self): """ Gets/sets all three PID values at the same time. See `TC200.p`, `TC200.i`, and `TC200.d` for individual restrictions. If `None` is specified then the corresponding PID value is not changed. :return: List of integers of PID values. In order [P, I, D]. :type: `list` or `tuple` :rtype: `list` """ return list(map(int, self.query("pid?").split())) @pid.setter def pid(self, newval): if not isinstance(newval, (list, tuple)): raise TypeError("Setting PID must be specified as a list or tuple") if newval[0] is not None: self.p = newval[0] if newval[1] is not None: self.i = newval[1] if newval[2] is not None: self.d = newval[2] @property def degrees(self): """ Gets/sets the units of the temperature measurement. :return: The temperature units (degC/F/K) the TC200 is measuring in :type: `~pint.Unit` """ response = self.status if (response >> 4) % 2 and (response >> 5) % 2: return u.degC elif (response >> 5) % 2: return u.degK return u.degF @degrees.setter def degrees(self, newval): if newval == u.degC: self.sendcmd("unit=c") elif newval == u.degF: self.sendcmd("unit=f") elif newval == u.degK: self.sendcmd("unit=k") else: raise TypeError("Invalid temperature type") sensor = enum_property( "sns", Sensor, input_decoration=lambda x: x.split(",")[0].split("=")[1].strip().lower(), set_fmt="{}={}", doc=""" Gets/sets the current thermistor type. Used for converting resistances to temperatures. :return: The thermistor type :type: `TC200.Sensor` """, ) beta = int_property( "beta", valid_set=range(2000, 6001), set_fmt="{}={}", doc=""" Gets/sets the beta value of the thermistor curve. Value within [2000, 6000] :return: the gain (in nnn) :type: `int` """, ) max_power = unitful_property( "pmax", units=u.W, format_code="{:.1f}", set_fmt="{}={}", 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: `~pint.Quantity` """, ) ================================================ FILE: src/instruments/thorlabs/thorlabs_utils.py ================================================ #!/usr/bin/python """ Contains common utility functions for Thorlabs-brand instruments """ def check_cmd(response): """ Checks the for the two common Thorlabs error messages; CMD_NOT_DEFINED and CMD_ARG_INVALID :param response: the response from the device :return: 1 if not found, 0 otherwise :rtype: int """ return 1 if response != "CMD_NOT_DEFINED" and response != "CMD_ARG_INVALID" else 0 ================================================ FILE: src/instruments/thorlabs/thorlabsapt.py ================================================ #!/usr/bin/env python """ Provides the support for the Thorlabs APT Controller. """ # IMPORTS ##################################################################### import re import struct import logging 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 ##################################################################### logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) # CLASSES ##################################################################### # pylint: disable=too-many-lines class ThorLabsAPT(_abstract.ThorLabsInstrument): """ Generic ThorLabs APT hardware device controller. Communicates using the ThorLabs APT communications protocol, whose documentation is found in the thorlabs source folder. """ class APTChannel: """ Represents a channel within the hardware device. One device can have many channels, each labeled by an index. """ def __init__(self, apt, idx_chan): self._apt = apt # APT is 1-based, but we want the Python representation to be # 0-based. self._idx_chan = idx_chan + 1 @property 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, ) resp = self._apt.querypacket( pkt, expect=_cmds.ThorLabsCommands.MOD_GET_CHANENABLESTATE ) return not bool(resp.parameters[1] - 1) @enabled.setter def enabled(self, newval): pkt = _packets.ThorLabsPacket( message_id=_cmds.ThorLabsCommands.MOD_SET_CHANENABLESTATE, param1=self._idx_chan, param2=0x01 if newval else 0x02, dest=self._apt.destination, source=0x01, data=None, ) self._apt.sendpacket(pkt) _channel_type = APTChannel def __init__(self, filelike): super().__init__(filelike) self._dest = 0x50 # Generic USB device; make this configurable later. # Provide defaults in case an exception occurs below. self._serial_number = None self._model_number = None self._hw_type = None self._fw_version = None self._notes = "" self._hw_version = None self._mod_state = None self._n_channels = 0 self._channel = () # Perform a HW_REQ_INFO to figure out the model number, serial number, # etc. try: req_packet = _packets.ThorLabsPacket( message_id=_cmds.ThorLabsCommands.HW_REQ_INFO, param1=0x00, param2=0x00, dest=self._dest, source=0x01, data=None, ) hw_info = self.querypacket( req_packet, expect=_cmds.ThorLabsCommands.HW_GET_INFO, expect_data_len=84, ) 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) ) @property def serial_number(self): """ Gets the serial number for the APT controller :type: `str` """ return self._serial_number @property def model_number(self): """ Gets the model number for the APT controller :type: `str` """ return self._model_number @property def name(self): """ Gets the name of the APT controller. This is a human readable string containing the model, serial number, hardware version, and firmware version. :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, ) ) @property def channel(self): """ Gets the list of channel objects attached to the APT controller. A specific channel object can then be accessed like one would access a list. :type: `tuple` of `APTChannel` """ return self._channel @property def n_channels(self): """ Gets/sets the number of channels attached to the APT controller :type: `int` """ return self._n_channels @n_channels.setter def n_channels(self, nch): # Change the number of channels so as not to modify those instances # already existing: # 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 = 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 def identify(self): """ Causes a light on the APT instrument to blink, so that it can be identified. """ pkt = _packets.ThorLabsPacket( message_id=_cmds.ThorLabsCommands.MOD_IDENTIFY, param1=0x00, param2=0x00, dest=self._dest, source=0x01, data=None, ) self.sendpacket(pkt) @property def destination(self): """ Gets the destination for the APT controller :type: `int` """ return self._dest class APTPiezoDevice(ThorLabsAPT): """ Generic ThorLabs APT piezo device, superclass of more specific piezo devices. """ class PiezoDeviceChannel(ThorLabsAPT.APTChannel): """ Represents a channel within the hardware device. One device can have many channels, each labeled by an index. This class represents piezo stage channels. """ # PIEZO COMMANDS # @property def max_travel(self): """ Gets the maximum travel for the specified piezo channel. :type: `~pint.Quantity` :units: Nanometers """ pkt = _packets.ThorLabsPacket( message_id=_cmds.ThorLabsCommands.PZ_REQ_MAXTRAVEL, param1=self._idx_chan, param2=0x00, dest=self._apt.destination, source=0x01, data=None, ) 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 # packet, so that the response is empty. if resp is None: 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): """ Class representing a single motor attached to a Thorlabs APT motor controller (`APTMotorController`). """ # 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 #: scale is not known. #: #: In keeping with the APT protocol documentation, the scale factor #: is multiplied by the physical quantity to get the encoder count, #: such that scale factors should have units similar to microsteps/mm, #: in the example of a linear motor. #: #: Encoder counts are represented by the quantities package unit #: "ct", which is considered dimensionally equivalent to dimensionless. #: Finally, note that the "/s" and "/s**2" are not included in scale #: factors, so as to produce quantities of dimension "ct/s" and #: "ct/s**2" #: from dimensionful input. #: #: For more details, see the APT protocol documentation. scale_factors = (u.Quantity(1, "dimensionless"),) * 3 _motion_timeout = u.Quantity(10, "second") __SCALE_FACTORS_BY_MODEL = { # 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": (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"), u.Quantity(42941.66, u.sec / u.deg), u.Quantity(14.66, u.sec**2 / u.deg), ), }, } __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, } # 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): """ Sets the scale factors for this motor channel, based on the model of the attached motor and the specifications of the driver of which this is a channel. :param str motor_model: Name of the model of the attached motor, as indicated in the APT protocol documentation (page 14, v9). """ for driver_re, motor_dict in self.__SCALE_FACTORS_BY_MODEL.items(): if driver_re.match(self._apt.model_number) is not None: if motor_model in motor_dict: self.scale_factors = motor_dict[motor_model] return else: 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, ) # 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 # @property def backlash_correction(self): """Get / set backlash correctionf or given stage. If no units are given, ``u.counts`` are assumed. If you have the stage defined (see example below), unitful values can be used for setting the backlash correction, e.g., ``u.mm`` or ``u.deg``. :return: Unitful quantity of backlash correction. Example: >>> 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] >>> ch.motor_model = 'PRM1-Z8' # select rotation stage >>> ch.backlash_correction = 4 * u.deg # set it to 4 degrees >>> ch.backlash_correction # read it back """ pkt = _packets.ThorLabsPacket( message_id=_cmds.ThorLabsCommands.MOT_REQ_GENMOVEPARAMS, param1=self._idx_chan, param2=0x00, dest=self._apt.destination, source=0x01, data=None, ) response = self._apt.querypacket( pkt, expect=_cmds.ThorLabsCommands.MOT_GET_GENMOVEPARAMS, 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] >>> ch.motor_model = 'PRM1-Z8' # select rotation stage >>> # set offset distance to 4 degrees, leave other values >>> ch.home_parameters = None, None, None, 4 * u.deg >>> ch.home_parameters # read it back (2, 1, , ) """ pkt = _packets.ThorLabsPacket( message_id=_cmds.ThorLabsCommands.MOT_REQ_HOMEPARAMS, param1=self._idx_chan, param2=0x00, dest=self._apt.destination, source=0x01, data=None, ) response = self._apt.querypacket( pkt, expect=_cmds.ThorLabsCommands.MOT_GET_HOMEPARAMS, expect_data_len=14, ) # chan, home_dir, limit_switch, velocity, ,offset_dist _, home_dir, lim_sw, vel, offset = struct.unpack(" 0) for key, bit_mask in self.__STATUS_BIT_MASK.items() } return status_dict @property def position(self): """ Gets the current position of the specified motor channel :type: `~pint.Quantity` """ pkt = _packets.ThorLabsPacket( message_id=_cmds.ThorLabsCommands.MOT_REQ_POSCOUNTER, param1=self._idx_chan, param2=0x00, dest=self._apt.destination, source=0x01, data=None, ) response = self._apt.querypacket( 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, u.Quantity): pos_ec = int(pos) else: if pos.units == u.counts: pos_ec = int(pos.magnitude) else: scaled_pos = pos * self.scale_factors[0] # Force a unit error. try: pos_ec = int(scaled_pos.to(u.counts).magnitude) except: 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 else _cmds.ThorLabsCommands.MOT_MOVE_RELATIVE ), param1=None, param2=None, dest=self._apt.destination, source=0x01, data=struct.pack(">> import instruments as ik >>> tm = ik.toptica.TopMode.open_serial('/dev/ttyUSB0', 115200) >>> print(tm.laser[0].wavelength) """ def __init__(self, filelike): super().__init__(filelike) self.prompt = "> " self.terminator = "\r\n" def _ack_expected(self, msg=""): if "reboot" in msg: return [msg, "reboot process started."] elif "start-correction" in msg: return [msg, "()"] return msg # ENUMS # class CharmStatus(IntEnum): """ Enum containing valid charm statuses for the lasers """ un_initialized = 0 in_progress = 1 success = 2 failure = 3 # INNER CLASSES # class Laser: """ Class representing a laser on the Toptica Topmode. .. warning:: This class should NOT be manually created by the user. It is designed to be initialized by the `Topmode` class. """ def __init__(self, parent, idx): self.parent = parent self.name = f"laser{idx + 1}" # PROPERTIES # @property def serial_number(self): """ Gets the serial number of the laser :return: The serial number of the specified laser :type: `str` """ return self.parent.reference(self.name + ":serial-number") @property def model(self): """ Gets the model type of the laser :return: The model of the specified laser :type: `str` """ return self.parent.reference(self.name + ":model") @property def wavelength(self): """ Gets the wavelength of the laser :return: The wavelength of the specified laser :units: Nanometers (nm) :type: `~pint.Quantity` """ return float(self.parent.reference(self.name + ":wavelength")) * u.nm @property def production_date(self): """ Gets the production date of the laser :return: The production date of the specified laser :type: `str` """ return self.parent.reference(self.name + ":production-date") @property def enable(self): """ Gets/sets the enable/disable status of the laser. Value of `True` is for enabled, and `False` for disabled. :return: Enable status of the specified laser :type: `bool` """ return ctbool(self.parent.reference(self.name + ":emission")) @enable.setter def enable(self, newval): if not isinstance(newval, bool): raise TypeError( "Emission status must be a boolean, got: " "{}".format(type(newval)) ) if not self.is_connected: raise RuntimeError( "Laser was not recognized by charm " "controller. Is it plugged in?" ) self.parent.set(self.name + ":enable-emission", newval) @property def is_connected(self): """ Check whether a laser is connected. :return: Whether the controller successfully connected to a laser :type: `bool` """ if self.serial_number == "unknown": return False return True @property def on_time(self): """ Gets the 'on time' value for the laser :return: The 'on time' value for the specified laser :units: Seconds (s) :type: `~pint.Quantity` """ return float(self.parent.reference(self.name + ":ontime")) * u.s @property def charm_status(self): """ Gets the 'charm status' of the laser :return: The 'charm status' of the specified laser :type: `bool` """ response = int(self.parent.reference(self.name + ":health")) return (response >> 7) % 2 == 1 @property def temperature_control_status(self): """ Gets the temperature control status of the laser :return: The temperature control status of the specified laser :type: `bool` """ response = int(self.parent.reference(self.name + ":health")) return (response >> 5) % 2 == 1 @property def current_control_status(self): """ Gets the current control status of the laser :return: The current control status of the specified laser :type: `bool` """ response = int(self.parent.reference(self.name + ":health")) return (response >> 6) % 2 == 1 @property def tec_status(self): """ Gets the TEC status of the laser :return: The TEC status of the specified laser :type: `bool` """ return ctbool(self.parent.reference(self.name + ":tec:ready")) @property def intensity(self): """ Gets the intensity of the laser. This property is unitless. :return: the intensity of the specified laser :units: Unitless :type: `float` """ return float(self.parent.reference(self.name + ":intensity")) @property def mode_hop(self): """ Gets whether the laser has mode-hopped :return: Mode-hop status of the specified laser :type: `bool` """ response = self.parent.reference(self.name + ":charm:reg:mh-occurred") return ctbool(response) @property def lock_start(self): """ Gets the date and time of the start of mode-locking :return: The datetime of start of mode-locking for specified laser :type: `datetime` """ # if mode locking has not started yet, the device will respond with # an empty date string. This causes a problem with ctdate. _corr_stat = self.correction_status if ( _corr_stat == TopMode.CharmStatus.un_initialized or _corr_stat == TopMode.CharmStatus.failure ): raise RuntimeError("Laser has not yet successfully locked") response = self.parent.reference(self.name + ":charm:reg:started") return ctdate(response) @property def first_mode_hop_time(self): """ Gets the date and time of the first mode hop :return: The datetime of the first mode hop for the specified laser :type: `datetime` """ # if the mode has not hopped, the device will respond with an empty # date string. This causes a problem with ctdate. if not self.mode_hop: raise RuntimeError("Mode hop not detected") response = self.parent.reference(self.name + ":charm:reg:first-mh") return ctdate(response) @property def latest_mode_hop_time(self): """ Gets the date and time of the latest mode hop :return: The datetime of the latest mode hop for the specified laser :type: `datetime` """ # if the mode has not hopped, the device will respond with an empty # date string. This causes a problem with ctdate. if not self.mode_hop: raise RuntimeError("Mode hop not detected") response = self.parent.reference(self.name + ":charm:reg:latest-mh") return ctdate(response) @property def correction_status(self): """ Gets the correction status of the laser :return: The correction status of the specified laser :type: `~TopMode.CharmStatus` """ value = self.parent.reference(self.name + ":charm:correction-status") return TopMode.CharmStatus(int(value)) # METHODS # def correction(self): """ Run the correction against the specified laser """ if self.correction_status == TopMode.CharmStatus.un_initialized: self.parent.execute(self.name + ":charm:start-correction-initial") else: self.parent.execute(self.name + ":charm:start-correction") # TOPMODE CONTROL LANGUAGE # def execute(self, command): """ Sends an execute command to the Topmode. This is used to automatically append (exec ' + command + ) to your command. :param str command: The command to be executed. """ self.sendcmd("(exec '" + command + ")") def set(self, param, value): """ Sends a param-set command to the Topmode. This is used to automatically handle appending "param-set!" and the rest of the param-set message structure to your message. :param str param: Parameter that will be set :param value: Value that the parameter will be set to :type value: `str`, `tuple`, `list`, or `bool` """ if isinstance(value, str): self.query(f'(param-set! \'{param} "{value}")') elif isinstance(value, (tuple, list)): self.query("(param-set! '{} '({}))".format(param, " ".join(value))) elif isinstance(value, bool): value = "t" if value else "f" self.query(f"(param-set! '{param} #{value})") def reference(self, param): """ Sends a reference commands to the Topmode. This is effectively a query request. It will append the required (param-ref ' + param + ). :param str param: Parameter that should be queried :return: Response to the reference request :rtype: `str` """ response = self.query(f"(param-ref '{param})").replace('"', "") return response def display(self, param): """ Sends a display command to the Topmode. :param str param: Parameter that will be sent with a display request :return: Response to the display request """ return self.query(f"(param-disp '{param})") # PROPERTIES # @property def laser(self): """ Gets a specific Topmode laser object. The desired laser is specified like one would access a list. For example, the following would print the wavelength from laser 1: >>> import instruments as ik >>> import instruments.units as u >>> tm = ik.toptica.TopMode.open_serial('/dev/ttyUSB0', 115200) >>> print(tm.laser[0].wavelength) :rtype: `~TopMode.Laser` """ return ProxyList(self, self.Laser, range(2)) @property def enable(self): """ is the laser lasing? :return: """ return ctbool(self.reference("emission")) @enable.setter def enable(self, newval): if not isinstance(newval, bool): raise TypeError( "Emission status must be a boolean, " "got: {}".format(type(newval)) ) self.set("enable-emission", newval) @property def locked(self): """ Gets the key switch lock status :return: `True` if key switch is locked, `False` otherwise :type: `bool` """ return ctbool(self.reference("front-key-locked")) @property def interlock(self): """ Gets the interlock switch open state :return: `True` if interlock switch is open, `False` otherwise :type: `bool` """ return ctbool(self.reference("interlock-open")) @property def firmware(self): """ Gets the firmware version of the charm controller :return: The firmware version of the charm controller :type: `tuple` """ firmware = tuple(map(int, self.reference("fw-ver").split("."))) return firmware @property def fpga_status(self): """ Gets the FPGA health status :return: `False` if there has been a failure for the FPGA, `True` otherwise :type: `bool` """ response = self.reference("system-health") if response.find("#f") >= 0: return False response = int(response) return False if response % 2 else True @property def serial_number(self): """ Gets the serial number of the charm controller :return: The serial number of the charm controller :type: `str` """ return self.reference("serial-number") @property def temperature_status(self): """ Gets the temperature controller board health status :return: `False` if there has been a failure for the temperature controller board, `True` otherwise :type: `bool` """ response = int(self.reference("system-health")) return False if (response >> 1) % 2 else True @property def current_status(self): """ Gets the current controller board health status :return: `False` if there has been a failure for the current controller board, `True` otherwise :type: `bool` """ response = int(self.reference("system-health")) return False if (response >> 2) % 2 else True # METHODS # def reboot(self): """ Reboots the system (note that the serial connect might have to be re-opened after this) """ self.execute("reboot-system") ================================================ FILE: src/instruments/toptica/toptica_utils.py ================================================ #!/usr/bin/env python """ Contains common utility functions for Toptica-brand instruments """ from datetime import datetime def convert_toptica_boolean(response): """ Converts the toptica boolean expression to a boolean :param response: response string :type response: str :return: the converted boolean :rtype: bool """ if response.find("Error: -3") > -1: return None elif response.find("f") > -1: return False elif response.find("t") > -1: return True else: raise ValueError("cannot convert: " + str(response) + " to boolean") def convert_toptica_datetime(response): """ Converts the toptical date format to a python time date :param response: the string from the topmode :type response: str :return: the converted date :rtype: 'datetime.datetime' """ if response.find('""') >= 0: return None return datetime.strptime(response, "%Y-%m-%d %H:%M:%S") ================================================ FILE: src/instruments/units.py ================================================ #!/usr/bin/env python """ Module containing custom units used by various instruments. """ # IMPORTS ##################################################################### import pint # UNITS ####################################################################### ureg = pint.get_application_registry() ureg.define("centibelmilliwatt = 1e-3 watt; logbase: 10; logfactor: 100 = cBm") ================================================ FILE: src/instruments/util_fns.py ================================================ #!/usr/bin/env python """ Module containing various utility functions """ # IMPORTS ##################################################################### import re from enum import Enum, IntEnum from instruments.units import ureg as u # CONSTANTS ################################################################### _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 `~pint.Quantity` with magnitude given by ``value`` and units given by ``units``. :param value: A value that may or may not be unitful. :param units: Units to be assumed for ``value`` if it does not already have units. :return: A unitful quantity that has either the units of ``value`` or ``units``, depending on if ``value`` is unitful. :rtype: `Quantity` """ if isinstance(value, u.Quantity): return value elif isinstance(value, str): value = u.Quantity(value) if value.dimensionless: return u.Quantity(value.magnitude, units) return value return u.Quantity(value, units) 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): """ Obsolete with the transition to Pint from Quantities. :param temperature: A quantity with units of Kelvin, Celsius, or Fahrenheit :type temperature: `pint.Quantity` :param base: A temperature unit to convert to :type base: `pint.Quantity` :return: The converted temperature :rtype: `pint.Quantity` """ newval = assume_units(temperature, u.degC) return newval.to(base) 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:`u.Quantity`:: >>> u.Quantity(*split_unit_str("1 s")) array(1) * s For this reason, the second element of the tuple may be a unit or a string, depending, since the quantity constructor takes either. :param str s: Input string that will be split up :param default_units: If no units are specified, this argument is given as the units. :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 `u.Quantity` """ if lookup is None: lookup = lambda x: x # Borrowed from: # http://stackoverflow.com/questions/430079/how-to-split-strings-into-text-and-number # Reg exp tweaked on May 30, 2015 by scasagrande to match on input with # scientific notation. General flow borrowed from: # http://www.regular-expressions.info/floatingpoint.html regex = r"([-+]?[0-9]*\.?[0-9]+)([eE][-+]?[0-9]+)?\s*([a-z]+)?" match = re.match(regex, str(s).strip(), re.I) if match: if match.groups()[1] is None: val, _, units = match.groups() else: val = float(match.groups()[0]) * 10 ** float(match.groups()[1][1:]) units = match.groups()[2] if units is None: return float(val), default_units 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): """ Creates and returns a new property based on the input parameters. :param function fget: Function to be called for the new property's getter :param function fset: Function to be called for the new property's setter :param str doc: Docstring for the new property :param bool readonly: If `True`, the returned property does not have a setter. :param bool writeonly: If `True`, the returned property does not have a getter. Both readonly and writeonly cannot both be `True`. """ if readonly and writeonly: raise ValueError("Properties cannot be both read- and write-only.") if readonly: return property(fget=fget, fset=None, doc=doc) elif writeonly: return property(fget=None, fset=fset, doc=doc) return property(fget=fget, fset=fset, doc=doc) 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", ... 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 `False` values. :param str doc: Docstring to be associated with the new property. :param bool readonly: If `True`, the returned property does not have a setter. :param bool writeonly: If `True`, the returned property does not have a getter. Both readonly and writeonly cannot both be `True`. :param str set_fmt: Specify the string format to use when sending a 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. """ def _getter(self): 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( 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( 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. The decorations can be functions which modify the incoming and outgoing values for dumb instruments that do stuff like include superfluous quotes that you might not want in your enum. Example: my_property = bool_property("BEST:PROPERTY", enum_class) :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. :param callable output_decoration: Function called on commands to the instrument. :param str doc: Docstring to be associated with the new property. :param bool readonly: If `True`, the returned property does not have a setter. :param bool writeonly: If `True`, the returned property does not have a getter. Both readonly and writeonly cannot both be `True`. :param str set_fmt: Specify the string format to use when sending a 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 elif hasattr(input_decoration, "__get__"): return input_decoration.__get__(None, object)(val) return input_decoration(val) def _out_decor_fcn(val): if output_decoration is None: return val elif hasattr(output_decoration, "__get__"): return output_decoration.__get__(None, object)(val) return output_decoration(val) def _getter(self): return enum(_in_decor_fcn(self.query(f"{command}?").strip())) def _setter(self, newval): try: # First assume newval is Enum.value newval = enum[newval] except KeyError: # Check if newval is Enum.name instead try: newval = enum(newval) except ValueError: raise ValueError("Enum property new value not in enum.") 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( 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 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. :param bool readonly: If `True`, the returned property does not have a setter. :param bool writeonly: If `True`, the returned property does not have a getter. Both readonly and writeonly cannot both be `True`. :param str set_fmt: Specify the string format to use when sending a 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. """ def _getter(self): raw = self.query(f"{command}?") return float(raw) def _setter(self, newval): 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(command if set_cmd is None else set_cmd, strval)) return rproperty( fget=_getter, fset=_setter, doc=doc, readonly=readonly, writeonly=writeonly ) 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 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. :param bool readonly: If `True`, the returned property does not have a setter. :param bool writeonly: If `True`, the returned property does not have a getter. Both readonly and writeonly cannot both be `True`. :param valid_set: Set of valid values for the property, or `None` if all `int` values are valid. :param str set_fmt: Specify the string format to use when sending a 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. """ def _getter(self): 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(command if set_cmd is None else set_cmd, strval) ) else: def _setter(self, newval): if newval not in valid_set: raise ValueError( "{} is not an allowed value for this property; " "must be one of {}.".format(newval, valid_set) ) strval = format_code.format(newval) 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 ) 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 and returns magnitudes without unit annotations, such that all unit information is provided by the ``units`` argument. This is not suitable for instruments where the units can change dynamically due to front-panel interaction or due to remote commands. :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 magnitude of values to the instrument. :param str doc: Docstring to be associated with the new property. :param callable input_decoration: Function called on responses from the instrument before passing to user code. :param callable output_decoration: Function called on commands to the instrument. :param bool readonly: If `True`, the returned property does not have a setter. :param bool writeonly: If `True`, the returned property does not have a getter. Both readonly and writeonly cannot both be `True`. :param str set_fmt: Specify the string format to use when sending a 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 valid_range: Tuple containing min & max values when setting the property. Index 0 is minimum value, index 1 is maximum value. Setting `None` in either disables bounds checking for that end of the range. The default of `(None, None)` has no min or max constraints. 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 elif hasattr(input_decoration, "__get__"): return input_decoration.__get__(None, object)(val) return input_decoration(val) def _out_decor_fcn(val): if output_decoration is None: return val elif hasattr(output_decoration, "__get__"): return output_decoration.__get__(None, object)(val) return output_decoration(val) def _getter(self): raw = _in_decor_fcn(self.query(f"{command}?")) return u.Quantity(*split_unit_str(raw, units)).to(units) def _setter(self, newval): newval = assume_units(newval, units).to(units) min_value, max_value = valid_range if min_value is not None: if callable(min_value): min_value = min_value(self) # pylint: disable=not-callable else: min_value = assume_units(min_value, units) if 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 callable(max_value): max_value = max_value(self) # pylint: disable=not-callable else: max_value = assume_units(max_value, units) if 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(newval.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 ) 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 `unitful_property` where all kwargs for this function are passed on to. See `unitful_property` documentation for information about additional parameters that will be passed on. Compared to `unitful_property`, this function will return 3 properties: the one created by `unitful_property`, one for the minimum value, and one for the maximum value. :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 minimum value query. The default is ``"{}:MIN?"`` which will place the property name in before the colon. Eg: ``"MOCK:MIN?"`` :param str max_fmt_str: Specify the string format to use when sending a maximum value query. The default is ``"{}:MAX?"`` which will place the property name in before the colon. Eg: ``"MOCK:MAX?"`` :param valid_range: Tuple containing min & max values when setting the property. Index 0 is minimum value, index 1 is maximum value. Setting `None` in either disables bounds checking for that end of the range. The default of ``("query", "query")`` will query the instrument for min and max parameter values. The valid set is inclusive of the values provided. :type valid_range: `list` or `tuple` of `int`, `float`, `None`, or the string ``"query"``. :param kwargs: All other keyword arguments are passed onto `unitful_property` :return: Returns a `tuple` of 3 properties: first is as returned by `unitful_property`, second is a property representing the minimum value, and third is a property representing the maximum value """ def _min_getter(self): if valid_range[0] == "query": 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 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, ) return ( 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, ) 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 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. :param bool writeonly: If `True`, the returned property does not have a getter. Both readonly and writeonly cannot both be `True`. :param str set_fmt: Specify the string format to use when sending a non-query to the instrument. The default is "{} {}{}{}" which places a space between the SCPI command the associated parameter, and places the bookmark symbols on either side of the parameter. :param str bookmark_symbol: The symbol that will flank both sides of the parameter to be sent to the instrument. By default this is ``"``. """ bookmark_length = len(bookmark_symbol) def _getter(self): 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( 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 ) # CLASSES ##################################################################### 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 instrument to have a single property through which all of its various identical input/ouput channels can be accessed. Search the code base of existing examples of how this is used for plenty of different examples. :param parent: The "parent" or "owner" of the of the proxy classes. In dev work, this is typically ``self``. :param proxy_cls: The child class that will be returned when the returned object is iterated through. These are usually objects that represent an entire channel/sensor/input/output, of which an instrument might have more than one but each are individually addressed. An example is an oscilloscope channel. :param valid_set: The set of valid keys by which the proxy class objects are accessed. Typically this is something like `range`, but can be any generator, list, enum, etc. """ def __init__(self, parent, proxy_cls, valid_set): self._parent = parent self._proxy_cls = proxy_cls self._valid_set = valid_set # FIXME: This only checks the next level up the chain! if hasattr(valid_set, "__bases__"): self._isenum = (Enum in valid_set.__bases__) or ( IntEnum in valid_set.__bases__ ) else: self._isenum = False def __iter__(self): for idx in self._valid_set: yield self._proxy_cls(self._parent, idx) def __getitem__(self, idx): # If we have an enum, try to normalize by using getitem. This will # allow for things like 'x' to be used instead of enum.x. if self._isenum: try: idx = self._valid_set[idx] except KeyError: try: idx = self._valid_set(idx) except ValueError: pass if not isinstance(idx, self._valid_set): 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) ) return self._proxy_cls(self._parent, idx) def __len__(self): return len(self._valid_set) ================================================ FILE: src/instruments/yokogawa/__init__.py ================================================ #!/usr/bin/env python """ Module containing Yokogawa instruments """ from .yokogawa6370 import Yokogawa6370 from .yokogawa7651 import Yokogawa7651 ================================================ FILE: src/instruments/yokogawa/yokogawa6370.py ================================================ #!/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.abstract_instruments.comm import SocketCommunicator from instruments.util_fns import ( enum_property, unitful_property, unitless_property, bounded_unitful_property, ProxyList, string_property, ) # CLASSES ##################################################################### class Yokogawa6370(OpticalSpectrumAnalyzer): """ The Yokogawa 6370 is an 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 Example usage with TCP/IP connection and user authentication: >>> import instruments as ik >>> auth = ("username", "password") >>> inst = ik.yokogawa.Yokogawa6370.open_tcpip("192.168.0.35", 10001, auth=auth) """ def __init__(self, filelike, auth=None): super().__init__(filelike) self._channel_count = len(self.Traces) if isinstance(self._file, SocketCommunicator): self.terminator = "\r\n" # TCP IP connection terminator # Authenticate with `auth` if auth is not None: self._authenticate(auth) # Set data Format to binary self.sendcmd(":FORMat:DATA REAL,64") # TODO: Find out where we want this def _authenticate(self, auth): """Authenticate with the instrument. :param auth: Authentication tuple of (username, password) """ username, password = auth _ = self.query(f'OPEN "{username}"') resp = self.query(f'"{password}"') if "ready" not in resp.lower(): raise ConnectionError("Could not authenticate with username / password") # 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, axis, limits=None, bin_format=True): """Get data of `axis`. :param axis: Axis to get the data of, "X" or "Y" :param limits: Range of samples to transfer as a tuple of min and max value, e.g. (5, 100) transfers data from the fifth to the 100th sample. The possible values are from 0 to 50000. """ if limits is None: cmd = f":TRAC:{axis}? {self._name}" elif isinstance(limits, (tuple, list)) and len(limits) == 2: cmd = f":TRAC:{axis}? {self._name},{limits[0]+1},{limits[1]+1}" else: raise ValueError("limits has to be a list or tuple with two members") 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) # Sweep 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().""", ) # Analysis # Traces 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, limits=None): """ Function to query the active Trace data of the OSA. :param limits: Range of samples to transfer as a tuple of min and max value, e.g. (5, 100) transfers data from the fifth to the 100th sample. The possible values are from 0 to 50000. """ return self.channel[self.active_trace].data(limits=limits) def wavelength(self, limits=None): """ Query the wavelength axis of the active trace. :param limits: Range of samples to transfer as a tuple of min and max value, e.g. (5, 100) transfers data from the fifth to the 100th sample. The possible values are from 0 to 50000. """ return self.channel[self.active_trace].wavelength(limits=limits) def analysis(self): """Get the analysis data.""" return [float(x) for x in self.query(":CALC:DATA?").split(",")] 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") def abort(self): """Abort a running sweep or calibration etc.""" self.sendcmd(":ABORT") def clear(self): """Clear status registers.""" self.sendcmd("*CLS") def query(self, cmd, size=-1): """todo: remove""" print(f"CMD: {cmd}") retval = super().query(cmd, size=size) print(f"RESP: {retval}") return retval ================================================ FILE: src/instruments/yokogawa/yokogawa7651.py ================================================ #!/usr/bin/env python """ Provides support for the Yokogawa 7651 power supply. """ # IMPORTS ##################################################################### from enum import IntEnum from instruments.units import ureg as u from instruments.abstract_instruments import PowerSupply from instruments.abstract_instruments import Instrument from instruments.util_fns import assume_units, ProxyList # CLASSES ##################################################################### class Yokogawa7651(PowerSupply, Instrument): """ The Yokogawa 7651 is a single channel DC power supply. Example usage: >>> import instruments as ik >>> import instruments.units as u >>> inst = ik.yokogawa.Yokogawa7651.open_gpibusb("/dev/ttyUSB0", 1) >>> inst.voltage = 10 * u.V """ # INNER CLASSES # class Channel(PowerSupply.Channel): """ Class representing the only channel on the Yokogawa 7651. 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. """ def __init__(self, parent, name): self._parent = parent self._name = name # PROPERTIES # @property def mode(self): """ Sets the output mode for the power supply channel. This is either constant voltage or constant current. Querying the mode is not supported by this instrument. :type: `Yokogawa7651.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"F{newval.value};") self._parent.trigger() @property def voltage(self): """ Sets the voltage of the specified channel. This device has a voltage range of 0V to +30V. Querying the voltage is not supported by this instrument. :units: As specified (if a `~pint.Quantity`) or assumed to be of units Volts. :type: `~pint.Quantity` with units Volt """ raise NotImplementedError( "This instrument does not support " "querying the output voltage setting." ) @voltage.setter def voltage(self, newval): newval = assume_units(newval, u.volt).to(u.volt).magnitude self.mode = self._parent.Mode.voltage self._parent.sendcmd(f"SA{newval};") self._parent.trigger() @property def current(self): """ Sets the current of the specified channel. This device has an max setting of 100mA. Querying the current is not supported by this instrument. :units: As specified (if a `~pint.Quantity`) or assumed to be of units Amps. :type: `~pint.Quantity` with units Amp """ raise NotImplementedError( "This instrument does not support " "querying the output current setting." ) @current.setter def current(self, newval): newval = assume_units(newval, u.amp).to(u.amp).magnitude self.mode = self._parent.Mode.current self._parent.sendcmd(f"SA{newval};") self._parent.trigger() @property def output(self): """ Sets the output status of the specified channel. This either enables or disables the output. Querying the output status is not supported by this instrument. :type: `bool` """ 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.trigger() else: self._parent.sendcmd("O0;") self._parent.trigger() # ENUMS # class Mode(IntEnum): """ Enum containing valid output modes for the Yokogawa 7651 """ voltage = 1 current = 5 # PROPERTIES # @property def channel(self): """ Gets the specific power supply channel object. Since the Yokogawa7651 is only equiped with a single channel, a list with a single element will be returned. This (single) channel is accessed as a list in the following manner:: >>> import instruments as ik >>> yoko = ik.yokogawa.Yokogawa7651.open_gpibusb('/dev/ttyUSB0', 10) >>> yoko.channel[0].voltage = 1 # Sets output voltage to 1V :rtype: `~Yokogawa7651.Channel` """ return ProxyList(self, Yokogawa7651.Channel, [0]) @property def voltage(self): """ Sets the voltage. This device has a voltage range of 0V to +30V. Querying the voltage is not supported by this instrument. :units: As specified (if a `~pint.Quantity`) or assumed to be of units Volts. :type: `~pint.Quantity` with units Volt """ raise NotImplementedError( "This instrument does not support querying " "the output voltage setting." ) @voltage.setter def voltage(self, newval): self.channel[0].voltage = newval @property def current(self): """ Sets the current. This device has an max setting of 100mA. Querying the current is not supported by this instrument. :units: As specified (if a `~pint.Quantity`) or assumed to be of units Amps. :type: `~pint.Quantity` with units Amp """ raise NotImplementedError( "This instrument does not support querying " "the output current setting." ) @current.setter def current(self, newval): self.channel[0].current = newval # METHODS # def trigger(self): """ Triggering function for the Yokogawa 7651. After changing any parameters of the instrument (for example, output voltage), the device needs to be triggered before it will update. """ self.sendcmd("E;") ================================================ FILE: tests/__init__.py ================================================ #!/usr/bin/env python """ Module containing InstrumentKit unit tests This file hosts a few utility functions to assist with creating and running unit tests. """ # IMPORTS #################################################################### import contextlib from io import BytesIO from unittest import mock import pytest 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", 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 according to that expectation. For an example of how to write tests using this context manager, see the ``make_name_test`` function below. :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. :type host_to_ins: ``str`` or ``list``; if ``list``, each line is concatenated with the separator given by ``sep``. :param ins_to_host: Data to be sent by the instrument; this is played back during the execution of this context manager, and should 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.decode("utf-8") # Normalize assertion and playback strings. if isinstance(ins_to_host, list): ins_to_host = [ 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"" ) 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"" ) 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: {} Got: {}""".format(repr(host_to_ins), repr(stdout.getvalue())) # current = stdin.tell() # stdin.seek(0, 2) # end = stdin.tell() # # assert current == end, \ # """Only read {} bytes out of {}""".format(current, end) def unit_eq(a, b, **kwargs): """ Asserts that two unitful quantites ``a`` and ``b`` are equal up to a small numerical threshold. Keyword arguments ``kwargs`` are passed on to ``pytest.approx()``. :param a: First quantity to compare to second. :type a: `~pint.Quantity` :param b: Second quantity to compare to first. :type b: `~pint.Quantity` :param kwargs: Keyword arguments, passed on to ``pytest.approx()``. """ assert a.magnitude == pytest.approx(b.magnitude, **kwargs) assert a.units == b.units, f"{a} and {b} have different units" 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: assert ins.name == "NAME" return test def iterable_eq(a, b, **kwargs): """ Asserts that the contents of two iterables are the same. Keyword arguments ``kwargs`` are passed on ``unit_eq``. :param a: First iterable to compare to second. :param b: Second iterable to compare to first. :param kwargs: Keyword arguments, passed on to ``pytest.approx()`` """ 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, **kwargs) else: assert a == b ================================================ FILE: tests/test_abstract_inst/__init__.py ================================================ ================================================ FILE: tests/test_abstract_inst/test_electrometer.py ================================================ #!/usr/bin/env python """ Module containing tests for the abstract electrometer class """ # IMPORTS #################################################################### import pytest import instruments as ik from 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() ================================================ FILE: tests/test_abstract_inst/test_function_generator.py ================================================ #!/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 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) ================================================ FILE: tests/test_abstract_inst/test_multimeter.py ================================================ #!/usr/bin/env python """ Module containing tests for the abstract multimeter class """ # IMPORTS #################################################################### import pytest import instruments as ik from 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") ================================================ FILE: tests/test_abstract_inst/test_optical_spectrum_analyzer.py ================================================ #!/usr/bin/env python """ Module containing tests for the abstract optical spectrum analyzer class """ # IMPORTS #################################################################### import pytest import instruments as ik from tests import expected_protocol # TESTS ###################################################################### @pytest.fixture def osa(monkeypatch): """Patch and return Optical Spectrum Analyzer class for access.""" inst = ik.abstract_instruments.OpticalSpectrumAnalyzer chan = ik.abstract_instruments.OpticalSpectrumAnalyzer.Channel monkeypatch.setattr(inst, "__abstractmethods__", set()) monkeypatch.setattr(chan, "__abstractmethods__", set()) return inst # OPTICAL SPECTRUM ANALYZER CLASS # def test_osa_channel(osa): """Get channel: ensure existence.""" with expected_protocol(osa, [], []) as inst: ch = inst.channel[0] assert isinstance(ch, ik.abstract_instruments.OpticalSpectrumAnalyzer.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 # @pytest.mark.parametrize("num_ch", [1, 5]) def test_osa_channel_wavelength(osa, num_ch): """Channel wavelength method: ensure existence.""" with expected_protocol(osa, [], []) as inst: inst._channel_count = num_ch ch = inst.channel[0] with pytest.raises(NotImplementedError): ch.wavelength() with pytest.raises(NotImplementedError): inst.wavelength() # single channel instrument @pytest.mark.parametrize("num_ch", [1, 5]) def test_osa_channel_data(osa, num_ch): """Channel data method: ensure existence.""" with expected_protocol(osa, [], []) as inst: inst._channel_count = num_ch ch = inst.channel[0] with pytest.raises(NotImplementedError): ch.data() with pytest.raises(NotImplementedError): inst.data() # single channel instrument ================================================ FILE: tests/test_abstract_inst/test_oscilloscope.py ================================================ #!/usr/bin/env python """ Module containing tests for the abstract oscilloscope class """ # IMPORTS #################################################################### import pytest import instruments as ik from 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() ================================================ FILE: tests/test_abstract_inst/test_power_supply.py ================================================ #!/usr/bin/env python """ Module containing tests for the abstract power supply class """ # IMPORTS #################################################################### import pytest import instruments as ik from 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 ================================================ FILE: tests/test_abstract_inst/test_signal_generator/test_channel.py ================================================ #!/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 ================================================ FILE: tests/test_abstract_inst/test_signal_generator/test_signal_generator.py ================================================ #!/usr/bin/env python """ Module containing tests for the abstract signal generator class """ # IMPORTS #################################################################### import pytest import instruments as ik from 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 ================================================ FILE: tests/test_abstract_inst/test_signal_generator/test_single_channel_sg.py ================================================ #!/usr/bin/env python """ Module containing tests for the abstract signal generator class """ # IMPORTS #################################################################### import pytest import instruments as ik from 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 ================================================ FILE: tests/test_agilent/__init__.py ================================================ ================================================ FILE: tests/test_agilent/test_agilent_33220a.py ================================================ #!/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 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 ================================================ FILE: tests/test_agilent/test_agilent_34410a.py ================================================ #!/usr/bin/env python """ Module containing tests for Agilent 34410a """ # IMPORTS #################################################################### import pytest import instruments as ik from instruments.optional_dep_finder import numpy from tests import expected_protocol, iterable_eq, make_name_test, unit_eq from instruments.units import ureg as u # TESTS ###################################################################### test_agilent_34410a_name = make_name_test(ik.agilent.Agilent34410a) def test_agilent34410a_read(): with expected_protocol( ik.agilent.Agilent34410a, ["CONF?", "READ?"], ["VOLT +1.000000E+01,+3.000000E-06", "+1.86850000E-03"], ) as dmm: unit_eq(dmm.read_meter(), +1.86850000e-03 * u.volt) def test_agilent34410a_data_point_count(): with expected_protocol( 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"], [ "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(1) iterable_eq(actual, expected) 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?", ], [ "VOLT +1.000000E+01,+3.000000E-06", ], ) 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() 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}"], [ f"{sample_count}", "VOLT +1.000000E+01,+3.000000E-06", "+4.27150000E-03,5.27150000E-03", ], ) as dmm: 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(): with expected_protocol( ik.agilent.Agilent34410a, [ "CONF?", "DATA:DATA? NVMEM", ], ["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 * u.volt) unit_eq(data[1], 5.27150000e-03 * u.volt) def test_agilent34410a_read_last_data(): with expected_protocol( 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: assert dmm.read_last_data() == float(na_value_str) ================================================ FILE: tests/test_aimtti/__init__.py ================================================ ================================================ FILE: tests/test_aimtti/test_aimttiel302p.py ================================================ #!/usr/bin/env python """ Unit tests for the Aim-TTI EL302P single output power supply """ # IMPORTS ##################################################################### import pytest import instruments as ik from instruments.units import ureg as u from tests import expected_protocol, unit_eq # TESTS ####################################################################### def test_channel(): with expected_protocol(ik.aimtti.AimTTiEL302P, [], [], sep="\n") as psu: assert psu.channel[0] == psu assert len(psu.channel) == 1 def test_current(): with expected_protocol( ik.aimtti.AimTTiEL302P, ["I 1.0", "I?"], ["I 1.00"], sep="\n" ) as psu: psu.current = 1.0 * u.amp assert psu.current == 1.0 * u.amp def test_current_sense(): with expected_protocol( ik.aimtti.AimTTiEL302P, ["IO?"], ["I 1.00"], sep="\n" ) as psu: assert psu.current_sense == 1.00 * u.amp def test_error(): with expected_protocol( ik.aimtti.AimTTiEL302P, ["ERR?"], ["ERR 0"], sep="\n" ) as psu: assert psu.error == ik.aimtti.AimTTiEL302P.Error.error_none def test_mode(): with expected_protocol(ik.aimtti.AimTTiEL302P, ["M?"], ["M CV"], sep="\n") as psu: assert psu.mode == ik.aimtti.AimTTiEL302P.Mode.voltage def test_name(): with expected_protocol( ik.aimtti.AimTTiEL302P, ["*IDN?"], ["Thurlby Thandar,EL302P,0,v2.00"], sep="\n" ) as psu: assert psu.name == "Thurlby Thandar EL302P" def test_off(): with expected_protocol( ik.aimtti.AimTTiEL302P, ["OFF", "OUT?"], ["OUT OFF"], sep="\n" ) as psu: psu.output = False assert not psu.output def test_on(): with expected_protocol( ik.aimtti.AimTTiEL302P, ["ON", "OUT?"], ["OUT ON"], sep="\n" ) as psu: psu.output = True assert psu.output def test_reset(): with expected_protocol(ik.aimtti.AimTTiEL302P, ["*RST"], [], sep="\n") as psu: psu.reset() def test_voltage(): with expected_protocol( ik.aimtti.AimTTiEL302P, ["V 10.0", "V?"], ["V 10.00"], sep="\n" ) as psu: psu.voltage = 10.0 * u.volt assert psu.voltage == 10.0 * u.volt def test_voltage_sense(): with expected_protocol( ik.aimtti.AimTTiEL302P, ["VO?"], ["V 24.00"], sep="\n" ) as psu: assert psu.voltage_sense == 24.00 * u.volt ================================================ FILE: tests/test_base_instrument.py ================================================ #!/usr/bin/env python """ Module containing tests for the base Instrument class """ # IMPORTS #################################################################### import socket import io import serial import usb.core from serial.tools.list_ports_common import ListPortInfo import pytest import instruments as ik from instruments.optional_dep_finder import numpy from tests import expected_protocol # pylint: disable=unused-import from instruments.abstract_instruments.comm import ( SocketCommunicator, USBCommunicator, VisaCommunicator, FileCommunicator, LoopbackCommunicator, GPIBCommunicator, AbstractCommunicator, USBTMCCommunicator, VXI11Communicator, SerialCommunicator, ) from instruments.errors import AcknowledgementError, PromptError from tests import iterable_eq from . import mock # TESTS ###################################################################### # pylint: disable=no-member,protected-access # BINBLOCKREAD TESTS def test_instrument_binblockread(): with expected_protocol( ik.Instrument, [], [ b"#210" + bytes.fromhex("00000001000200030004") + b"0", ], sep="\n", ) as inst: 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(): inst = ik.Instrument.open_test() data = bytes.fromhex("00000001000200030004") inst._file.read_raw = mock.MagicMock( side_effect=[b"#", b"2", b"10", data[:6], data[6:]] ) 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] iterable_eq(calls_actual, calls_expected) def test_instrument_binblockread_too_many_reads(): 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) def test_instrument_binblockread_bad_block_start(): with pytest.raises(IOError): inst = ik.Instrument.open_test() inst._file.read_raw = mock.MagicMock(return_value=b"@") _ = 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): mock_socket.socket.return_value.__class__ = socket.socket mock_socket_comm.return_value.__class__ = SocketCommunicator inst = ik.Instrument.open_tcpip("127.0.0.1", 1234) assert isinstance(inst._file, SocketCommunicator) is True # Check for call: SocketCommunicator(socket.socket()) mock_socket_comm.assert_called_with(mock_socket.socket.return_value) def test_instrument_open_tcpip_auth_not_implemented(): """Ensure `_authenticate` exists and raises NotImplemented error if hit here.""" inst = ik.Instrument.open_test() with pytest.raises(NotImplementedError): inst._authenticate(auth=("user", "pwd")) @pytest.mark.parametrize("auth", [None, ("user", "pwd")]) @mock.patch("instruments.abstract_instruments.instrument.SocketCommunicator") @mock.patch("instruments.abstract_instruments.instrument.socket") def test_instrument_open_tcpip_passing_on_auth( mock_socket, mock_socket_comm, auth, mocker ): """Ensure auth only passed on if not None, see issue #439.""" mock_socket.socket.return_value.__class__ = socket.socket mock_socket_comm.return_value.__class__ = SocketCommunicator # spy on the __init__ method of the Instrument class inst_spy = mocker.spy(ik.abstract_instruments.instrument.Instrument, "__init__") _ = ik.Instrument.open_tcpip("127.0.0.1", 1234, auth=auth) call_list = inst_spy.mock_calls auth_kwarg = {"auth": auth} if auth is not None: assert auth_kwarg in call_list[0] else: assert auth_kwarg not in call_list[0] @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 ) 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 ) 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 def isOpen(self): """ Pretends that the serial connection is open. """ return True # TEST OPEN_SERIAL WITH USB IDENTIFIERS ###################################### def fake_comports(): """ Generate a fake list of comports to compare against. """ fake_device = ListPortInfo(device="COM1") fake_device.vid = 0 fake_device.pid = 1000 fake_device.serial_number = "a1" fake_device2 = ListPortInfo(device="COM2") fake_device2.vid = 1 fake_device2.pid = 1010 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 ) 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 ) @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 ) 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 ) @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): 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(device="COM2") fake_device2.vid = 0 fake_device2.pid = 1000 fake_device2.serial_number = "b2" mock_comports.return_value = [fake_device, fake_device2] _ = ik.Instrument.open_serial(baud=1234, vid=0, pid=1000) @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): 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") @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): with pytest.raises(ValueError): mock_serial_manager.new_serial_connection.return_value.__class__ = ( SerialCommunicator ) _ = ik.Instrument.open_serial(baud=1234, vid=1234, pid=1000) @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): with pytest.raises(ValueError): mock_serial_manager.new_serial_connection.return_value.__class__ = ( SerialCommunicator ) _ = ik.Instrument.open_serial(baud=1234) @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): 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) @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): with pytest.raises(ValueError): mock_serial_manager.new_serial_connection.return_value.__class__ = ( SerialCommunicator ) _ = ik.Instrument.open_serial(baud=1234, vid=1234) @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): 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_gpib_comm.return_value.__class__ = GPIBCommunicator 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 ) mock_gpib_comm.assert_called_with( mock_serial_manager.new_serial_connection.return_value, 1, "gi" ) @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.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" visa_open_resource = mock_visa.ResourceManager.return_value.open_resource inst = ik.Instrument.open_visa("abc123") assert isinstance(inst._file, VisaCommunicator) is True visa_open_resource.assert_called_with("abc123") mock_visa_comm.assert_called_with(visa_open_resource("abc123")) @mock.patch("instruments.abstract_instruments.instrument.VisaCommunicator") @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" inst = ik.Instrument.open_visa("abc123") assert isinstance(inst._file, VisaCommunicator) is True mock_visa.instrument.assert_called_with("abc123") def test_instrument_open_test(): a = mock.MagicMock() b = mock.MagicMock() a.__class__ = io.BytesIO b.__class__ = io.BytesIO inst = ik.Instrument.open_test(stdin=a, stdout=b) assert isinstance(inst._file, LoopbackCommunicator) assert inst._file._stdin == a assert inst._file._stdout == b @mock.patch("instruments.abstract_instruments.instrument.VXI11Communicator") def test_instrument_open_vxi11(mock_vxi11_comm): mock_vxi11_comm.return_value.__class__ = VXI11Communicator inst = ik.Instrument.open_vxi11("string", 1, key1="value") assert isinstance(inst._file, VXI11Communicator) is True 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 inst = ik.Instrument.open_usbtmc("string", 1, key1="value") assert isinstance(inst._file, USBTMCCommunicator) is True mock_usbtmc_comm.assert_called_with("string", 1, key1="value") @mock.patch("instruments.abstract_instruments.instrument.FileCommunicator") def test_instrument_open_file(mock_file_comm): mock_file_comm.return_value.__class__ = FileCommunicator inst = ik.Instrument.open_file("filename") assert isinstance(inst._file, FileCommunicator) is True mock_file_comm.assert_called_with("filename") # 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") mock_open_conn.assert_called_with("/dev/foobar", baud=115200) @mock.patch("instruments.abstract_instruments.instrument.Instrument.open_serial") def test_instrument_open_from_uri_serial_with_baud(mock_open_conn): _ = ik.Instrument.open_from_uri("serial:///dev/foobar?baud=230400") mock_open_conn.assert_called_with("/dev/foobar", baud=230400) @mock.patch("instruments.abstract_instruments.instrument.Instrument.open_tcpip") def test_instrument_open_from_uri_tcpip(mock_open_conn): _ = ik.Instrument.open_from_uri("tcpip://192.169.0.1:8080") mock_open_conn.assert_called_with("192.169.0.1", 8080) @mock.patch("instruments.abstract_instruments.instrument.Instrument.open_gpibusb") def test_instrument_open_from_uri_gpibusb(mock_open_conn): _ = ik.Instrument.open_from_uri("gpib+usb:///dev/foobar/15") mock_open_conn.assert_called_with("/dev/foobar", 15) @mock.patch("instruments.abstract_instruments.instrument.Instrument.open_gpibusb") def test_instrument_open_from_uri_gpibserial(mock_open_conn): _ = ik.Instrument.open_from_uri("gpib+serial:///dev/foobar/7") mock_open_conn.assert_called_with("/dev/foobar", 7) @mock.patch("instruments.abstract_instruments.instrument.Instrument.open_visa") def test_instrument_open_from_uri_visa(mock_open_conn): _ = ik.Instrument.open_from_uri("visa://USB::0x1234::0xFF12::0x7421::0::INSTR") mock_open_conn.assert_called_with("USB::0x1234::0xFF12::0x7421::0::INSTR") @mock.patch("instruments.abstract_instruments.instrument.Instrument.open_usbtmc") def test_instrument_open_from_uri_usbtmc(mock_open_conn): _ = ik.Instrument.open_from_uri("usbtmc://USB::0x1234::0xFF12::0x7421::0::INSTR") mock_open_conn.assert_called_with("USB::0x1234::0xFF12::0x7421::0::INSTR") @mock.patch("instruments.abstract_instruments.instrument.Instrument.open_file") def test_instrument_open_from_uri_file(mock_open_conn): _ = ik.Instrument.open_from_uri("file:///dev/filename") mock_open_conn.assert_called_with("/dev/filename") @mock.patch("instruments.abstract_instruments.instrument.Instrument.open_vxi11") def test_instrument_open_from_uri_vxi11(mock_open_conn): _ = ik.Instrument.open_from_uri("vxi11://TCPIP::192.168.1.105::gpib,5::INSTR") mock_open_conn.assert_called_with("TCPIP::192.168.1.105::gpib,5::INSTR") def test_instrument_open_from_uri_invalid_scheme(): with pytest.raises(NotImplementedError): _ = ik.Instrument.open_from_uri("foo://bar") @mock.patch("instruments.abstract_instruments.comm.LoopbackCommunicator.close") def test_instrument_context_manager(mock_close: mock.Mock): with ik.Instrument.open_test(): pass mock_close.assert_called() # INIT TESTS def test_instrument_init_bad_filelike(): with pytest.raises(TypeError): _ = ik.Instrument(mock.MagicMock()) def test_instrument_init(): mock_filelike = mock.MagicMock() mock_filelike.__class__ = AbstractCommunicator inst = ik.Instrument(mock_filelike) assert inst._testing is False assert inst._prompt is None assert inst._terminator == "\n" assert inst._file == mock_filelike def test_instrument_init_loopbackcomm(): mock_filelike = mock.MagicMock() mock_filelike.__class__ = LoopbackCommunicator inst = ik.Instrument(mock_filelike) assert inst._testing is True # COMM TESTS def test_instrument_default_ack_expected(): mock_filelike = mock.MagicMock() mock_filelike.__class__ = AbstractCommunicator inst = ik.Instrument(mock_filelike) assert inst._ack_expected() is None assert inst._ack_expected("foobar") is None def test_instrument_sendcmd_noack_noprompt(): mock_filelike = mock.MagicMock() mock_filelike.__class__ = AbstractCommunicator inst = ik.Instrument(mock_filelike) inst.sendcmd("foobar") inst._file.sendcmd.assert_called_with("foobar") def test_instrument_sendcmd_noprompt(): mock_filelike = mock.MagicMock() mock_filelike.__class__ = AbstractCommunicator inst = ik.Instrument(mock_filelike) def new_ack(msg): return msg inst._ack_expected = new_ack inst.read = mock.MagicMock(return_value="foobar") inst.sendcmd("foobar") inst.read.assert_called_with() inst._file.sendcmd.assert_called_with("foobar") def test_instrument_sendcmd_noprompt_multiple_ack(): mock_filelike = mock.MagicMock() mock_filelike.__class__ = AbstractCommunicator inst = ik.Instrument(mock_filelike) def new_ack(msg): return [msg, "second ack"] inst._ack_expected = new_ack inst.read = mock.MagicMock(side_effect=["foobar", "second ack"]) inst.sendcmd("foobar") inst.read.assert_called_with() inst._file.sendcmd.assert_called_with("foobar") def test_instrument_sendcmd_bad_ack(): with pytest.raises(AcknowledgementError): mock_filelike = mock.MagicMock() mock_filelike.__class__ = AbstractCommunicator inst = ik.Instrument(mock_filelike) def new_ack(msg): return msg inst._ack_expected = new_ack inst.read = mock.MagicMock(return_value="derp") inst.sendcmd("foobar") def test_instrument_sendcmd_noack(): mock_filelike = mock.MagicMock() mock_filelike.__class__ = AbstractCommunicator inst = ik.Instrument(mock_filelike) inst.prompt = "> " inst.read = mock.MagicMock(return_value="> ") inst.sendcmd("foobar") inst.read.assert_called_with(2) inst._file.sendcmd.assert_called_with("foobar") def test_instrument_sendcmd_noack_bad_prompt(): 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.sendcmd("foobar") def test_instrument_sendcmd(): mock_filelike = mock.MagicMock() mock_filelike.__class__ = AbstractCommunicator inst = ik.Instrument(mock_filelike) def new_ack(msg): return msg inst._ack_expected = new_ack inst.prompt = "> " inst.read = mock.MagicMock(side_effect=["foobar", "> "]) inst.sendcmd("foobar") inst.read.assert_any_call() inst.read.assert_any_call(2) inst._file.sendcmd.assert_called_with("foobar") assert inst.read.call_count == 2 def test_instrument_query_noack_noprompt(): mock_filelike = mock.MagicMock() mock_filelike.__class__ = AbstractCommunicator inst = ik.Instrument(mock_filelike) inst._file.query.return_value = "datas" assert inst.query("foobar?") == "datas" inst._file.query.assert_called_with("foobar?", -1) def test_instrument_query_noprompt(): """ Expected order of operations: - IK sends command to instrument - Instrument sends ACK, commonly an echo of the command - ACK is verified with _ack_expected function - If ACK is good, do another read which contains our return data """ mock_filelike = mock.MagicMock() mock_filelike.__class__ = AbstractCommunicator inst = ik.Instrument(mock_filelike) inst.read = mock.MagicMock(side_effect=["foobar?", "datas"]) def new_ack(msg): return msg inst._ack_expected = new_ack assert inst.query("foobar?") == "datas" inst._file.query.assert_called_with("foobar?", size=0) inst.read.assert_called_with(-1) def test_instrument_query_noprompt_multiple_ack(): """ Expected order of operations: - IK sends command to instrument - Instrument sends ACK, commonly an echo of the command - ACK is verified with _ack_expected function - Loop through each ACK that is expected - If ACK is good, do another read which contains our return data """ mock_filelike = mock.MagicMock() mock_filelike.__class__ = AbstractCommunicator inst = ik.Instrument(mock_filelike) inst.read = mock.MagicMock(side_effect=["foobar?", "second ack", "datas"]) def new_ack(msg): return [msg, "second ack"] inst._ack_expected = new_ack assert inst.query("foobar?") == "datas" inst._file.query.assert_called_with("foobar?", size=0) inst.read.assert_called_with(-1) def test_instrument_query_bad_ack(): 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 inst._ack_expected = new_ack _ = inst.query("foobar?") def test_instrument_query_noack(): """ Expected order of operations: - IK sends command to instrument and gets responce containing our data - Another read is done to capture the prompt characters sent by the instrument. Read should be equal to the length of the expected prompt - Exception is raised is prompt is not correct """ 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="> ") assert inst.query("foobar?") == "datas" inst._file.query.assert_called_with("foobar?", -1) inst.read.assert_called_with(2) def test_instrument_query_noack_bad_prompt(): 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.query("foobar?") def test_instrument_query(): """ Expected order of operations: - IK sends command to instrument - Instrument sends ACK, commonly an echo of the command - ACK is verified with _ack_expected function - If ACK is good, do another read which contains our return data - Another read is done to capture the prompt characters sent by the instrument. Read should be equal to the length of the expected prompt - Exception is raised is prompt is not correct """ mock_filelike = mock.MagicMock() mock_filelike.__class__ = AbstractCommunicator inst = ik.Instrument(mock_filelike) inst._file.query.return_value = "foobar?" inst.read = mock.MagicMock(side_effect=["foobar?", "datas", "> "]) def new_ack(msg): return msg inst._ack_expected = new_ack inst.prompt = "> " assert inst.query("foobar?") == "datas" inst.read.assert_any_call(-1) inst.read.assert_any_call(2) inst._file.query.assert_called_with("foobar?", size=0) assert inst.read.call_count == 3 def test_instrument_read(): mock_filelike = mock.MagicMock() mock_filelike.__class__ = AbstractCommunicator inst = ik.Instrument(mock_filelike) inst._file.read.return_value = "foobar" assert inst.read() == "foobar" 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, "utf-8") def test_instrument_write(): mock_filelike = mock.MagicMock() mock_filelike.__class__ = AbstractCommunicator inst = ik.Instrument(mock_filelike) inst.write("foobar") inst._file.write.assert_called_with("foobar") # PROPERTIES # def test_instrument_timeout(): mock_filelike = mock.MagicMock() mock_filelike.__class__ = AbstractCommunicator inst = ik.Instrument(mock_filelike) timeout = mock.PropertyMock(return_value=1) type(inst._file).timeout = timeout assert inst.timeout == 1 timeout.assert_called_with() inst.timeout = 5 timeout.assert_called_with(5) def test_instrument_address(): mock_filelike = mock.MagicMock() mock_filelike.__class__ = AbstractCommunicator inst = ik.Instrument(mock_filelike) address = mock.PropertyMock(return_value="/dev/foobar") type(inst._file).address = address assert inst.address == "/dev/foobar" address.assert_called_with() inst.address = "COM1" address.assert_called_with("COM1") def test_instrument_terminator(): mock_filelike = mock.MagicMock() mock_filelike.__class__ = AbstractCommunicator inst = ik.Instrument(mock_filelike) terminator = mock.PropertyMock(return_value="\n") type(inst._file).terminator = terminator assert inst.terminator == "\n" terminator.assert_called_with() inst.terminator = "*" terminator.assert_called_with("*") def test_instrument_prompt(): mock_filelike = mock.MagicMock() mock_filelike.__class__ = AbstractCommunicator inst = ik.Instrument(mock_filelike) assert inst.prompt is None inst.prompt = "> " assert inst.prompt == "> " inst.prompt = None assert inst.prompt is None ================================================ FILE: tests/test_comet/__init__.py ================================================ ================================================ FILE: tests/test_comet/test_cito_plus_1310.py ================================================ #!/usr/bin/env python """Test the Comet Cito Plus 1310 instrument.""" from hypothesis import given, strategies as st import pytest import instruments as ik from instruments.comet.cito_plus_1310 import _crc16 as crc16 from instruments.units import ureg as u from tests import expected_protocol def add_checksum(data: bytes) -> bytes: """Add a CRC-16 checksum to the data.""" checksum = crc16(data) return data + checksum.to_bytes(2, "little") # TEST CLASS PROPERTIES # def test_name(): """Get the instrument label as string.""" label_exp = "Comet Cito Plus 1310" lbl_bytes = label_exp.encode("utf_8") cmd = bytes([0x0A, 0x41, 0x00, 0x0A, 0x00, 0x01]) cmd = add_checksum(cmd) answ = bytes([0x0A, 0x41, len(lbl_bytes)]) + lbl_bytes answ = add_checksum(answ) with expected_protocol( ik.comet.CitoPlus1310, [cmd], [answ], sep="", ) as cito: assert cito.name == label_exp def test_forward_power(): """Read forward power from instrument.""" cmd = bytes([0x0A, 0x41, 0x1F, 0x55, 0x00, 0x01]) cmd = add_checksum(cmd) answ = bytes([0x0A, 0x41, 0x04, 0x00, 0x00, 0x03, 0xE8]) # 1000 W answ = add_checksum(answ) with expected_protocol( ik.comet.CitoPlus1310, [cmd], [answ], sep="", ) as cito: assert cito.forward_power == 1000 * u.mW def test_load_power(): """Read forward power from instrument.""" cmd = bytes([0x0A, 0x41, 0x1F, 0x57, 0x00, 0x01]) cmd = add_checksum(cmd) answ = bytes([0x0A, 0x41, 0x04, 0x00, 0x00, 0x03, 0xE8]) # 1000 W answ = add_checksum(answ) with expected_protocol( ik.comet.CitoPlus1310, [cmd], [answ], sep="", ) as cito: assert cito.load_power == 1000 * u.mW def test_output_power(): """Get/set output power.""" cmd_set_1kW = bytes([0x0A, 0x42, 0x04, 0xB6, 0x00, 0x0F, 0x42, 0x40]) cmd_set_1kW = add_checksum(cmd_set_1kW) cmd_query = bytes([0x0A, 0x41, 0x04, 0xB6, 0x00, 0x01]) cmd_query = add_checksum(cmd_query) answ_1kW = bytes([0x0A, 0x41, 0x04, 0x00, 0x0F, 0x42, 0x40]) answ_1kW = add_checksum(answ_1kW) with expected_protocol( ik.comet.CitoPlus1310, [ cmd_set_1kW, cmd_query, ], [ cmd_set_1kW, answ_1kW, ], sep="", ) as cito: cito.output_power = 1 * u.kW assert cito.output_power == 1 * u.kW @given(pow=st.floats(min_value=0, max_value=1.0, exclude_max=True)) def test_output_power_smaller_one(pow): """Set output power values smaller than 1 W are set to zero.""" cmd_set_0W = bytes([0x0A, 0x42, 0x04, 0xB6, 0x00, 0x00, 0x00, 0x00]) cmd_set_0W = add_checksum(cmd_set_0W) with expected_protocol( ik.comet.CitoPlus1310, [cmd_set_0W], [cmd_set_0W], sep="", ) as cito: cito.output_power = pow * u.W def test_reflected_power(): """Read reflected power from instrument.""" cmd = bytes([0x0A, 0x41, 0x1F, 0x56, 0x00, 0x01]) cmd = add_checksum(cmd) answ = bytes([0x0A, 0x41, 0x04, 0x00, 0x00, 0x03, 0xE8]) # 1000 W answ = add_checksum(answ) with expected_protocol( ik.comet.CitoPlus1310, [cmd], [answ], sep="", ) as cito: assert cito.reflected_power == 1000 * u.mW def test_regulation_mode(): """Set/get the regulation mode.""" cmd_forward_power = bytes([0x0A, 0x42, 0x04, 0xB1, 0x00, 0x00, 0x00, 0x00]) cmd_forward_power = add_checksum(cmd_forward_power) cmd_load_power = bytes([0x0A, 0x42, 0x04, 0xB1, 0x00, 0x00, 0x00, 0x01]) cmd_load_power = add_checksum(cmd_load_power) cmd_read_mode = bytes([0x0A, 0x41, 0x04, 0xB1, 0x00, 0x01]) cmd_read_mode = add_checksum(cmd_read_mode) answ_forward_power = bytes([0x0A, 0x41, 0x04, 0x00, 0x00, 0x00, 0x00]) answ_forward_power = add_checksum(answ_forward_power) answ_load_power = bytes([0x0A, 0x41, 0x04, 0x00, 0x00, 0x00, 0x01]) answ_load_power = add_checksum(answ_load_power) with expected_protocol( ik.comet.CitoPlus1310, [ cmd_forward_power, cmd_read_mode, cmd_load_power, cmd_read_mode, ], [ cmd_forward_power, answ_forward_power, cmd_load_power, answ_load_power, ], sep="", ) as cito: cito.regulation_mode = cito.RegulationMode.ForwardPower assert cito.regulation_mode == cito.RegulationMode.ForwardPower cito.regulation_mode = cito.RegulationMode.LoadPower assert cito.regulation_mode == cito.RegulationMode.LoadPower def test_rf(): """Set/get the RF state.""" cmd_rf_on = bytes([0x0A, 0x42, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01]) cmd_rf_on = add_checksum(cmd_rf_on) cmd_rf_off = bytes([0x0A, 0x42, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x00]) cmd_rf_off = add_checksum(cmd_rf_off) query_rf = bytes([0x0A, 0x41, 0x1F, 0x40, 0x00, 0x01]) query_rf = add_checksum(query_rf) answ_rf_on = bytes([0x0A, 0x41, 0x04, 0x00, 0x00, 0x00, 0x00]) answ_rf_on = add_checksum(answ_rf_on) answ_rf_off = bytes([0x0A, 0x41, 0x04, 0x00, 0x00, 0x00, 0x01]) answ_rf_off = add_checksum(answ_rf_off) with expected_protocol( ik.comet.CitoPlus1310, [ cmd_rf_on, query_rf, cmd_rf_off, query_rf, ], [ cmd_rf_on, answ_rf_on, cmd_rf_off, answ_rf_off, ], sep="", ) as rf: rf.rf = True assert rf.rf rf.rf = False assert not rf.rf def test_checksum_error_return_package(): """Raise an OSError if the checksum of returned package is invalid.""" query_rf = bytes([0x0A, 0x41, 0x1F, 0x40, 0x00, 0x01]) query_rf = add_checksum(query_rf) answ_rf_on = bytes([0x0A, 0x41, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) with expected_protocol( ik.comet.CitoPlus1310, [query_rf], [answ_rf_on], sep="", ) as rf: with pytest.raises(OSError) as err: _ = rf.rf assert "CRC-16 checksum of returned package does not match" in err.value.args[0] def test_unknown_parameter(): """Raise an excpetion if illegal function code used.""" cmd = bytes([0x0A, 0x41, 0x00, 0x00]) cmd = add_checksum(cmd) answ = bytes([0x0A, 0xC1, 0x01]) answ = add_checksum(answ) with expected_protocol( ik.comet.CitoPlus1310, [cmd], [answ], sep="", ) as cito: with pytest.raises(OSError) as err: cito.query(cmd) assert "Unknown parameter or illegal function code" in err.value.args[0] def test_write_answer_package_no_match(): """Raise exception if answer package of a write command does not match.""" cmd_rf_on = bytes([0x0A, 0x42, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01]) cmd_rf_on = add_checksum(cmd_rf_on) wrong_return = bytes([0x0A, 0x42, 0x04, 0xE9, 0x00, 0x00, 0x00, 0x01]) wrong_return = add_checksum(wrong_return) with expected_protocol( ik.comet.CitoPlus1310, [cmd_rf_on], [wrong_return], sep="", ) as rf: with pytest.raises(OSError) as err: rf.rf = True assert "Received package does not match sent package" in err.value.args[0] ### TEST CHECKSUM FUNCTION ### @pytest.mark.parametrize( "inp_out", [[bytes([0x00]), 0x0000], [bytes([0x31, 0xAE]), 0x2C94]] ) def test_crc16(inp_out): """Test CRC16 calculation with some hand-calculated examples.""" input, expected = inp_out assert crc16(input) == expected ================================================ FILE: tests/test_comm/__init__.py ================================================ ================================================ FILE: tests/test_comm/test_file.py ================================================ #!/usr/bin/env python """ Unit tests for the file communication layer """ # IMPORTS #################################################################### import pytest from instruments.abstract_instruments.comm import FileCommunicator from .. import mock # TEST CASES ################################################################# # pylint: disable=protected-access,unused-argument patch_path = "instruments.abstract_instruments.comm.file_communicator.usbtmc" def test_filecomm_init(): mock_file = mock.MagicMock() comm = FileCommunicator(mock_file) assert comm._filelike is mock_file def test_filecomm_address_getter(): mock_file = mock.MagicMock() comm = FileCommunicator(mock_file) mock_name = mock.PropertyMock(return_value="/home/user/file") type(comm._filelike).name = mock_name assert comm.address == "/home/user/file" mock_name.assert_called_with() def test_filecomm_address_getter_no_name(): mock_file = mock.MagicMock() comm = FileCommunicator(mock_file) del comm._filelike.name assert comm.address is None def test_filecomm_address_setter(): with pytest.raises(NotImplementedError): comm = FileCommunicator(mock.MagicMock()) comm.address = "abc123" def test_filecomm_terminator(): comm = FileCommunicator(mock.MagicMock()) assert comm.terminator == "\n" comm.terminator = "*" assert comm._terminator == "*" comm.terminator = b"*" assert comm._terminator == "*" def test_filecomm_timeout_getter(): with pytest.raises(NotImplementedError): comm = FileCommunicator(mock.MagicMock()) _ = comm.timeout def test_filecomm_timeout_setter(): with pytest.raises(NotImplementedError): comm = FileCommunicator(mock.MagicMock()) comm.timeout = 1 def test_filecomm_close(): comm = FileCommunicator(mock.MagicMock()) comm.close() comm._filelike.close.assert_called_with() def test_filecomm_read_raw(): comm = FileCommunicator(mock.MagicMock()) comm._filelike.read = mock.MagicMock(side_effect=[b"a", b"b", b"c", b"\n"]) assert comm.read_raw() == b"abc" comm._filelike.read.assert_has_calls([mock.call(1)] * 4) assert comm._filelike.read.call_count == 4 comm._filelike.read = mock.MagicMock() comm.read_raw(10) comm._filelike.read.assert_called_with(10) def test_filecomm_write_raw(): comm = FileCommunicator(mock.MagicMock()) comm.write_raw(b"mock") comm._filelike.write.assert_called_with(b"mock") def test_filecomm_sendcmd(): comm = FileCommunicator(mock.MagicMock()) comm._sendcmd("mock") comm._filelike.write.assert_called_with(b"mock\n") def test_filecomm_query(): comm = FileCommunicator(mock.MagicMock()) 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"]) assert comm._query("mock") == "abc" def test_filecomm_seek(): comm = FileCommunicator(mock.MagicMock()) comm.seek(1) comm._filelike.seek.assert_called_with(1) def test_filecomm_tell(): comm = FileCommunicator(mock.MagicMock()) comm._filelike.tell.return_value = 5 assert comm.tell() == 5 comm._filelike.tell.assert_called_with() def test_filecomm_flush_input(): comm = FileCommunicator(mock.MagicMock()) comm.flush_input() comm._filelike.flush.assert_called_with() ================================================ FILE: tests/test_comm/test_gpibusb.py ================================================ #!/usr/bin/env python """ Unit tests for the GPIBUSB communication layer """ # IMPORTS #################################################################### import pytest import serial from instruments.units import ureg as u from instruments.abstract_instruments.comm import GPIBCommunicator, SerialCommunicator from tests import unit_eq from .. import mock # TEST CASES ################################################################# # pylint: disable=protected-access,unused-argument def test_gpibusbcomm_init(): serial_comm = SerialCommunicator(serial.Serial()) serial_comm._conn = mock.MagicMock() serial_comm._query = mock.MagicMock(return_value="1") comm = GPIBCommunicator(serial_comm, 1) assert isinstance(comm._file, SerialCommunicator) def test_gpibusbcomm_init_correct_values_new_firmware(): mock_gpib = mock.MagicMock() mock_gpib.query.return_value = "5" comm = GPIBCommunicator(mock_gpib, 1) 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(): # This test just has the differences between the new and old firmware mock_gpib = mock.MagicMock() mock_gpib.query.return_value = "4" comm = GPIBCommunicator(mock_gpib, 1) assert comm._eos == 10 def test_gpibusbcomm_address(): # Create our communicator comm = GPIBCommunicator(mock.MagicMock(), 1) port_name = mock.PropertyMock(return_value="/dev/address") type(comm._file).address = port_name # Check that our address function is working assert comm.address == (1, "/dev/address") port_name.assert_called_with() # Able to set GPIB address comm.address = 5 assert comm._gpib_address == 5 # Able to set address with a list comm.address = [6, "/dev/foobar"] assert comm._gpib_address == 6 port_name.assert_called_with("/dev/foobar") def test_gpibusbcomm_address_out_of_range(): with pytest.raises(ValueError): comm = GPIBCommunicator(mock.MagicMock(), 1) comm.address = 31 def test_gpibusbcomm_address_wrong_type(): with pytest.raises(TypeError): comm = GPIBCommunicator(mock.MagicMock(), 1) comm.address = "derp" def test_gpibusbcomm_eoi(): comm = GPIBCommunicator(mock.MagicMock(), 1) comm._version = 5 comm._file.sendcmd = mock.MagicMock() 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 assert comm.eoi is False assert comm._eoi is False comm._file.sendcmd.assert_called_with("++eoi 0") def test_gpibusbcomm_eoi_old_firmware(): comm = GPIBCommunicator(mock.MagicMock(), 1) comm._version = 4 comm._file.sendcmd = mock.MagicMock() 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 assert comm.eoi is False assert comm._eoi is False comm._file.sendcmd.assert_called_with("+eoi:0") def test_gpibusbcomm_eoi_bad_type(): with pytest.raises(TypeError): comm = GPIBCommunicator(mock.MagicMock(), 1) comm._version = 5 comm.eoi = "abc" def test_gpibusbcomm_eos_rn(): comm = GPIBCommunicator(mock.MagicMock(), 1) comm._version = 5 comm._file.sendcmd = mock.MagicMock() comm.eos = "\r\n" assert comm.eos == "\r\n" assert comm._eos == "\r\n" comm._file.sendcmd.assert_called_with("++eos 0") def test_gpibusbcomm_eos_r(): comm = GPIBCommunicator(mock.MagicMock(), 1) comm._version = 5 comm._file.sendcmd = mock.MagicMock() comm.eos = "\r" assert comm.eos == "\r" assert comm._eos == "\r" comm._file.sendcmd.assert_called_with("++eos 1") def test_gpibusbcomm_eos_n(): comm = GPIBCommunicator(mock.MagicMock(), 1) comm._version = 5 comm._file.sendcmd = mock.MagicMock() comm.eos = "\n" assert comm.eos == "\n" assert comm._eos == "\n" comm._file.sendcmd.assert_called_with("++eos 2") def test_gpibusbcomm_eos_invalid(): with pytest.raises(ValueError): comm = GPIBCommunicator(mock.MagicMock(), 1) comm._version = 5 comm.eos = "*" def test_gpibusbcomm_eos_old_firmware(): comm = GPIBCommunicator(mock.MagicMock(), 1) comm._version = 4 comm._file.sendcmd = mock.MagicMock() comm.eos = "\n" assert comm._eos == 10 comm._file.sendcmd.assert_called_with("+eos:10") def test_gpibusbcomm_terminator(): comm = GPIBCommunicator(mock.MagicMock(), 1) comm._version = 5 # Default terminator should be eoi assert comm.terminator == "eoi" assert comm._eoi is True comm.terminator = "\n" assert comm.terminator == "\n" assert comm._eoi is False comm.terminator = "eoi" 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 * u.millisecond) comm.timeout = 5000 * u.millisecond comm._file.sendcmd.assert_called_with("++read_tmo_ms 5000") def test_gpibusbcomm_close(): comm = GPIBCommunicator(mock.MagicMock(), 1) comm._version = 5 comm.close() comm._file.close.assert_called_with() def test_gpibusbcomm_read_raw(): comm = GPIBCommunicator(mock.MagicMock(), 1) comm._version = 5 comm._file.read_raw = mock.MagicMock(return_value=b"abc") assert comm.read_raw(3) == b"abc" comm._file.read_raw.assert_called_with(3) def test_gpibusbcomm_write_raw(): comm = GPIBCommunicator(mock.MagicMock(), 1) comm._version = 5 comm.write_raw(b"mock") comm._file.write_raw.assert_called_with(b"mock") def test_gpibusbcomm_sendcmd(): comm = GPIBCommunicator(mock.MagicMock(), 1) 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"), mock.call("++eos 2"), mock.call("mock"), ] ) def test_gpibusbcomm_sendcmd_empty_string(): comm = GPIBCommunicator(mock.MagicMock(), 1) comm._version = 5 comm._file.sendcmd = mock.MagicMock() # Refreshed because init makes calls comm._sendcmd("") comm._file.sendcmd.assert_not_called() def test_gpibusbcomm_query(): comm = GPIBCommunicator(mock.MagicMock(), 1) comm._version = 5 comm._file.read = mock.MagicMock(return_value="answer") comm.sendcmd = mock.MagicMock() assert comm._query("mock?") == "answer" comm.sendcmd.assert_called_with("mock?") comm._file.read.assert_called_with(-1) comm._query("mock?", size=10) comm._file.read.assert_called_with(10) def test_gpibusbcomm_query_no_question_mark(): comm = GPIBCommunicator(mock.MagicMock(), 1) comm._version = 5 comm._file.sendcmd = mock.MagicMock() # Refreshed because init makes calls comm._file.read = mock.MagicMock(return_value="answer") comm.sendcmd = mock.MagicMock() 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")]) def test_serialcomm_flush_input(): comm = GPIBCommunicator(mock.MagicMock(), 1) comm._version = 5 comm.flush_input() comm._file.flush_input.assert_called_with() ================================================ FILE: tests/test_comm/test_loopback.py ================================================ #!/usr/bin/env python """ Unit tests for the loopback communication layer """ # IMPORTS #################################################################### import pytest from instruments.abstract_instruments.comm import LoopbackCommunicator from .. import mock # TEST CASES ################################################################# # pylint: disable=protected-access,unused-argument def test_loopbackcomm_init(): var1 = "abc" var2 = "123" comm = LoopbackCommunicator(stdin=var1, stdout=var2) assert comm._stdin is var1 assert comm._stdout is var2 @mock.patch("instruments.abstract_instruments.comm.loopback_communicator.sys") def test_loopbackcomm_address(mock_sys): mock_name = mock.PropertyMock(return_value="address") type(mock_sys.stdin).name = mock_name comm = LoopbackCommunicator() comm._conn = mock.MagicMock() # Check that our address function is working assert comm.address == "address" mock_name.assert_called_with() def test_loopbackcomm_terminator(): comm = LoopbackCommunicator() # Default terminator should be \n assert comm.terminator == "\n" comm.terminator = b"*" assert comm.terminator == "*" assert comm._terminator == "*" comm.terminator = "\r" assert comm.terminator == "\r" assert comm._terminator == "\r" comm.terminator = "\r\n" assert comm.terminator == "\r\n" assert comm._terminator == "\r\n" def test_loopbackcomm_timeout(): comm = LoopbackCommunicator() assert comm.timeout == 0 comm.timeout = 10 assert comm.timeout == 0 # setting should be ignored def test_loopbackcomm_close(): mock_stdin = mock.MagicMock() comm = LoopbackCommunicator(stdin=mock_stdin) comm.close() mock_stdin.close.assert_called_with() def test_loopbackcomm_read_raw(): mock_stdin = mock.MagicMock() mock_stdin.read.side_effect = [b"a", b"b", b"c", b"\n"] comm = LoopbackCommunicator(stdin=mock_stdin) 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() comm.read_raw(10) mock_stdin.read.assert_called_with(10) def test_loopbackcomm_read_raw_2char_terminator(): mock_stdin = mock.MagicMock() mock_stdin.read.side_effect = [b"a", b"b", b"c", b"\r", b"\n"] comm = LoopbackCommunicator(stdin=mock_stdin) comm._terminator = "\r\n" 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) @mock.patch("builtins.input") def test_loopbackcomm_read_raw_stdin(mock_input): mock_input.return_value = "Returned string." comm = LoopbackCommunicator() assert comm.read_raw() == b"Returned string." def test_loopbackcomm_write_raw(): mock_stdout = mock.MagicMock() comm = LoopbackCommunicator(stdout=mock_stdout) comm.write_raw(b"mock") mock_stdout.write.assert_called_with(b"mock") def test_loopbackcomm_sendcmd(): mock_stdout = mock.MagicMock() comm = LoopbackCommunicator(stdout=mock_stdout) comm._sendcmd("mock") mock_stdout.write.assert_called_with(b"mock\n") comm.write = mock.MagicMock() comm._sendcmd("mock") comm.write.assert_called_with("mock\n") def test_loopbackcomm_query(): comm = LoopbackCommunicator() comm.read = mock.MagicMock(return_value="answer") comm.sendcmd = mock.MagicMock() assert comm._query("mock") == "answer" comm.sendcmd.assert_called_with("mock") comm.read.assert_called_with(-1) comm._query("mock", size=10) comm.read.assert_called_with(10) def test_loopbackcomm_seek(): with pytest.raises(NotImplementedError): comm = LoopbackCommunicator() comm.seek(1) def test_loopbackcomm_tell(): with pytest.raises(NotImplementedError): comm = LoopbackCommunicator() comm.tell() def test_loopbackcomm_flush_input(): comm = LoopbackCommunicator() comm.flush_input() ================================================ FILE: tests/test_comm/test_serial.py ================================================ #!/usr/bin/env python """ Unit tests for the serial communication layer """ # IMPORTS #################################################################### import pytest import serial from instruments.units import ureg as u from instruments.abstract_instruments.comm import SerialCommunicator from tests import unit_eq from .. import mock # TEST CASES ################################################################# # pylint: disable=protected-access,unused-argument def test_serialcomm_init(): comm = SerialCommunicator(serial.Serial()) assert isinstance(comm._conn, serial.Serial) is True def test_serialcomm_init_wrong_filelike(): with pytest.raises(TypeError): _ = SerialCommunicator("derp") def test_serialcomm_address(): # Create our communicator comm = SerialCommunicator(serial.Serial()) comm._conn = mock.MagicMock() port_name = mock.PropertyMock(return_value="/dev/address") type(comm._conn).port = port_name # Check that our address function is working assert comm.address == "/dev/address" port_name.assert_called_with() def test_serialcomm_terminator(): comm = SerialCommunicator(serial.Serial()) # Default terminator should be \n assert comm.terminator == "\n" comm.terminator = "*" assert comm.terminator == "*" comm.terminator = "\r\n" assert comm.terminator == "\r\n" assert comm._terminator == "\r\n" def test_serialcomm_timeout(): comm = SerialCommunicator(serial.Serial()) comm._conn = mock.MagicMock() timeout = mock.PropertyMock(return_value=30) type(comm._conn).timeout = timeout unit_eq(comm.timeout, 30 * u.second) timeout.assert_called_with() comm.timeout = 10 timeout.assert_called_with(10) comm.timeout = 1000 * u.millisecond timeout.assert_called_with(1) def test_serialcomm_parity(): comm = SerialCommunicator(serial.Serial()) # Default parity should be NONE assert comm.parity == serial.PARITY_NONE comm.parity = serial.PARITY_EVEN assert comm.parity == serial.PARITY_EVEN def test_serialcomm_close(): comm = SerialCommunicator(serial.Serial()) comm._conn = mock.MagicMock() comm.close() comm._conn.shutdown.assert_called_with() comm._conn.close.assert_called_with() def test_serialcomm_read_raw(): comm = SerialCommunicator(serial.Serial()) comm._conn = mock.MagicMock() comm._conn.read = mock.MagicMock(side_effect=[b"a", b"b", b"c", b"\n"]) 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() comm.read_raw(10) comm._conn.read.assert_called_with(10) def test_loopbackcomm_read_raw_2char_terminator(): comm = SerialCommunicator(serial.Serial()) comm._conn = mock.MagicMock() comm._conn.read = mock.MagicMock(side_effect=[b"a", b"b", b"c", b"\r", b"\n"]) comm._terminator = "\r\n" assert comm.read_raw() == b"abc" comm._conn.read.assert_has_calls([mock.call(1)] * 5) assert comm._conn.read.call_count == 5 def test_serialcomm_read_raw_timeout(): 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) def test_serialcomm_write_raw(): comm = SerialCommunicator(serial.Serial()) comm._conn = mock.MagicMock() comm.write_raw(b"mock") comm._conn.write.assert_called_with(b"mock") def test_serialcomm_sendcmd(): comm = SerialCommunicator(serial.Serial()) comm._conn = mock.MagicMock() comm._sendcmd("mock") comm._conn.write.assert_called_with(b"mock\n") def test_serialcomm_query(): comm = SerialCommunicator(serial.Serial()) comm._conn = mock.MagicMock() comm.read = mock.MagicMock(return_value="answer") comm.sendcmd = mock.MagicMock() assert comm._query("mock") == "answer" comm.sendcmd.assert_called_with("mock") comm.read.assert_called_with(-1) comm._query("mock", size=10) comm.read.assert_called_with(10) def test_serialcomm_seek(): with pytest.raises(NotImplementedError): comm = SerialCommunicator(serial.Serial()) comm.seek(1) def test_serialcomm_tell(): with pytest.raises(NotImplementedError): comm = SerialCommunicator(serial.Serial()) comm.tell() def test_serialcomm_flush_input(): comm = SerialCommunicator(serial.Serial()) comm._conn = mock.MagicMock() comm.flush_input() comm._conn.flushInput.assert_called_with() ================================================ FILE: tests/test_comm/test_socket.py ================================================ #!/usr/bin/env python """ Unit tests for the socket communication layer """ # IMPORTS #################################################################### import socket import pytest from instruments.units import ureg as u from instruments.abstract_instruments.comm import SocketCommunicator from tests import unit_eq from .. import mock # TEST CASES ################################################################# # pylint: disable=protected-access,unused-argument def test_socketcomm_init(): socket_object = socket.socket() comm = SocketCommunicator(socket_object) assert isinstance(comm._conn, socket.socket) is True assert comm._conn == socket_object def test_socketcomm_init_wrong_filelike(): with pytest.raises(TypeError): _ = SocketCommunicator("derp") def test_socketcomm_address(): comm = SocketCommunicator(socket.socket()) comm._conn = mock.MagicMock() comm._conn.getpeername.return_value = "127.0.0.1", 1234 assert comm.address == ("127.0.0.1", 1234) comm._conn.getpeername.assert_called_with() def test_socketcomm_address_setting(): with pytest.raises(NotImplementedError): comm = SocketCommunicator(socket.socket()) comm.address = "foobar" def test_socketcomm_terminator(): comm = SocketCommunicator(socket.socket()) # Default terminator should be \n assert comm.terminator == "\n" comm.terminator = b"*" assert comm.terminator == "*" assert comm._terminator == "*" comm.terminator = "\r" assert comm.terminator == "\r" assert comm._terminator == "\r" comm.terminator = "\r\n" assert comm.terminator == "\r\n" assert comm._terminator == "\r\n" def test_socketcomm_timeout(): comm = SocketCommunicator(socket.socket()) comm._conn = mock.MagicMock() comm._conn.gettimeout.return_value = 1.234 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 * u.millisecond comm._conn.settimeout.assert_called_with(1) def test_socketcomm_close(): comm = SocketCommunicator(socket.socket()) comm._conn = mock.MagicMock() comm.close() comm._conn.shutdown.assert_called_with(socket.SHUT_RDWR) comm._conn.close.assert_called_with() def test_socketcomm_read_raw(): comm = SocketCommunicator(socket.socket()) comm._conn = mock.MagicMock() comm._conn.recv = mock.MagicMock(side_effect=[b"a", b"b", b"c", b"\n"]) 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() comm.read_raw(10) comm._conn.recv.assert_called_with(10) def test_loopbackcomm_read_raw_2char_terminator(): comm = SocketCommunicator(socket.socket()) comm._conn = mock.MagicMock() comm._conn.recv = mock.MagicMock(side_effect=[b"a", b"b", b"c", b"\r", b"\n"]) comm._terminator = "\r\n" assert comm.read_raw() == b"abc" comm._conn.recv.assert_has_calls([mock.call(1)] * 5) assert comm._conn.recv.call_count == 5 def test_serialcomm_read_raw_timeout(): 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) def test_socketcomm_write_raw(): comm = SocketCommunicator(socket.socket()) comm._conn = mock.MagicMock() comm.write_raw(b"mock") comm._conn.sendall.assert_called_with(b"mock") def test_socketcomm_sendcmd(): comm = SocketCommunicator(socket.socket()) comm._conn = mock.MagicMock() comm._sendcmd("mock") comm._conn.sendall.assert_called_with(b"mock\n") def test_socketcomm_query(): comm = SocketCommunicator(socket.socket()) comm._conn = mock.MagicMock() comm.read = mock.MagicMock(return_value="answer") comm.sendcmd = mock.MagicMock() assert comm._query("mock") == "answer" comm.sendcmd.assert_called_with("mock") comm.read.assert_called_with(-1) comm._query("mock", size=10) comm.read.assert_called_with(10) def test_socketcomm_seek(): with pytest.raises(NotImplementedError): comm = SocketCommunicator(socket.socket()) comm.seek(1) def test_socketcomm_tell(): with pytest.raises(NotImplementedError): comm = SocketCommunicator(socket.socket()) comm.tell() def test_socketcomm_flush_input(): comm = SocketCommunicator(socket.socket()) comm._conn = mock.MagicMock() comm.read = mock.MagicMock() comm.flush_input() comm.read.assert_called_with(-1) ================================================ FILE: tests/test_comm/test_usb_communicator.py ================================================ #!/usr/bin/env python """ Unit tests for the USB communicator. """ # IMPORTS #################################################################### import math 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." ) @pytest.mark.parametrize("val", [1, 1000, math.inf]) 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) ================================================ FILE: tests/test_comm/test_usbtmc.py ================================================ #!/usr/bin/env python """ Unit tests for the USBTMC communication layer """ # IMPORTS #################################################################### import pytest from instruments.abstract_instruments.comm import USBTMCCommunicator from tests import unit_eq from instruments.units import ureg as u from .. import mock # TEST CASES ################################################################# # pylint: disable=protected-access,unused-argument,no-member patch_path = "instruments.abstract_instruments.comm.usbtmc_communicator.usbtmc" @mock.patch(patch_path) def test_usbtmccomm_init(mock_usbtmc): _ = USBTMCCommunicator("foobar", var1=123) mock_usbtmc.Instrument.assert_called_with("foobar", var1=123) @mock.patch(patch_path, new=None) def test_usbtmccomm_init_missing_module(): with pytest.raises(ImportError): _ = USBTMCCommunicator() @mock.patch(patch_path) def test_usbtmccomm_terminator_getter(mock_usbtmc): comm = USBTMCCommunicator() term_char = mock.PropertyMock(return_value=10) type(comm._filelike).term_char = term_char assert comm.terminator == "\n" term_char.assert_called_with() @mock.patch(patch_path) def test_usbtmccomm_terminator_setter(mock_usbtmc): comm = USBTMCCommunicator() term_char = mock.PropertyMock(return_value="\n") type(comm._filelike).term_char = term_char comm.terminator = "*" assert comm._terminator == "*" term_char.assert_called_with(42) comm.terminator = b"*" assert comm._terminator == "*" term_char.assert_called_with(42) @mock.patch(patch_path) def test_usbtmccomm_timeout(mock_usbtmc): comm = USBTMCCommunicator() timeout = mock.PropertyMock(return_value=1) type(comm._filelike).timeout = timeout unit_eq(comm.timeout, 1 * u.second) timeout.assert_called_with() comm.timeout = 10 timeout.assert_called_with(10.0) comm.timeout = 1000 * u.millisecond timeout.assert_called_with(1.0) @mock.patch(patch_path) def test_usbtmccomm_close(mock_usbtmc): comm = USBTMCCommunicator() comm.close() comm._filelike.close.assert_called_with() @mock.patch(patch_path) def test_usbtmccomm_read_raw(mock_usbtmc): comm = USBTMCCommunicator() comm._filelike.read_raw = mock.MagicMock(return_value=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 comm._filelike.read_raw = mock.MagicMock() comm.read_raw(10) comm._filelike.read_raw.assert_called_with(num=10) @mock.patch(patch_path) def test_usbtmccomm_write_raw(mock_usbtmc): comm = USBTMCCommunicator() comm.write_raw(b"mock") comm._filelike.write_raw.assert_called_with(b"mock") @mock.patch(patch_path) def test_usbtmccomm_sendcmd(mock_usbtmc): comm = USBTMCCommunicator() comm.write = mock.MagicMock() comm._sendcmd("mock") comm.write.assert_called_with("mock") @mock.patch(patch_path) def test_usbtmccomm_query(mock_usbtmc): comm = USBTMCCommunicator() comm._filelike.ask = mock.MagicMock(return_value="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") @mock.patch(patch_path) def test_usbtmccomm_seek(mock_usbtmc): with pytest.raises(NotImplementedError): comm = USBTMCCommunicator() comm.seek(1) @mock.patch(patch_path) def test_usbtmccomm_tell(mock_usbtmc): with pytest.raises(NotImplementedError): comm = USBTMCCommunicator() comm.tell() @mock.patch(patch_path) def test_usbtmccomm_flush_input(mock_usbtmc): comm = USBTMCCommunicator() comm.flush_input() ================================================ FILE: tests/test_comm/test_visa_communicator.py ================================================ #!/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_read_termination_not_string(visa_inst): """Raise TypeError if read termination is set with non-string character.""" comm = VisaCommunicator(visa_inst) with pytest.raises(TypeError): comm.read_termination = 42 def test_visacomm_terminator(visa_inst): """Get / Set terminator and ensure pyvisa takes the right communicator.""" comm = VisaCommunicator(visa_inst) comm.terminator = "\r" assert comm.terminator == "\r" assert comm.read_termination == "\r" assert comm.write_termination == "\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 string.") 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_write_termination_not_string(visa_inst): """Raise TypeError if write termination is set with non-string character.""" comm = VisaCommunicator(visa_inst) with pytest.raises(TypeError): comm.write_termination = 42 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) 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) ================================================ FILE: tests/test_comm/test_vxi11.py ================================================ #!/usr/bin/env python """ Unit tests for the VXI11 communication layer """ # IMPORTS #################################################################### import pytest from instruments.abstract_instruments.comm import VXI11Communicator from .. import mock # TEST CASES ################################################################# # pylint: disable=protected-access,unused-argument,no-member import_base = "instruments.abstract_instruments.comm.vxi11_communicator.vxi11" @mock.patch(import_base) def test_vxi11comm_init(mock_vxi11): _ = VXI11Communicator("host") mock_vxi11.Instrument.assert_called_with("host") @mock.patch(import_base, new=None) def test_vxi11comm_init_no_vxi11(): with pytest.raises(ImportError): _ = VXI11Communicator("host") @mock.patch(import_base) def test_vxi11comm_address(mock_vxi11): # Create our communicator comm = VXI11Communicator() # Add in the host and name properties which are usually # done in vxi11.Instrument.__init__ host = mock.PropertyMock(return_value="host") name = mock.PropertyMock(return_value="name") type(comm._inst).host = host type(comm._inst).name = name # Check that our address function is working assert comm.address == ["host", "name"] host.assert_called_with() name.assert_called_with() @mock.patch(import_base) def test_vxi11comm_terminator(mock_vxi11): comm = VXI11Communicator() term_char = mock.PropertyMock(return_value="\n") type(comm._inst).term_char = term_char assert comm.terminator == "\n" term_char.assert_called_with() comm.terminator = "*" term_char.assert_called_with("*") @mock.patch(import_base) def test_vxi11comm_timeout(mock_vxi11): comm = VXI11Communicator() timeout = mock.PropertyMock(return_value=30) type(comm._inst).timeout = timeout assert comm.timeout == 30 timeout.assert_called_with() comm.timeout = 10 timeout.assert_called_with(10) @mock.patch(import_base) def test_vxi11comm_close(mock_vxi11): comm = VXI11Communicator() comm.close() comm._inst.close.assert_called_with() @mock.patch(import_base) def test_vxi11comm_close_fail(mock_vxi11): comm = VXI11Communicator() comm._inst.close.return_value = Exception comm.close() comm._inst.close.assert_called_once_with() @mock.patch(import_base) def test_vxi11comm_read(mock_vxi11): comm = VXI11Communicator() comm._inst.read_raw.return_value = b"mock" assert comm.read_raw() == b"mock" comm._inst.read_raw.assert_called_with(num=-1) comm.read(10) comm._inst.read_raw.assert_called_with(num=10) @mock.patch(import_base) def test_vxi11comm_write(mock_vxi11): comm = VXI11Communicator() comm.write_raw(b"mock") comm._inst.write_raw.assert_called_with(b"mock") @mock.patch(import_base) def test_vxi11comm_sendcmd(mock_vxi11): comm = VXI11Communicator() comm._sendcmd("mock") comm._inst.write_raw.assert_called_with(b"mock") @mock.patch(import_base) def test_vxi11comm_query(mock_vxi11): comm = VXI11Communicator() comm._inst.ask.return_value = "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) @mock.patch(import_base) def test_vxi11comm_seek(mock_vxi11): with pytest.raises(NotImplementedError): comm = VXI11Communicator() comm.seek(1) @mock.patch(import_base) def test_vxi11comm_tell(mock_vxi11): with pytest.raises(NotImplementedError): comm = VXI11Communicator() comm.tell() @mock.patch(import_base) def test_vxi11comm_flush(mock_vxi11): with pytest.raises(NotImplementedError): comm = VXI11Communicator() comm.flush_input() ================================================ FILE: tests/test_config.py ================================================ #!/usr/bin/env python """ Module containing tests for util_fns.py """ # IMPORTS #################################################################### from io import StringIO import pytest from instruments.units import ureg as u import instruments as ik 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_from_file(tmp_path): """Load an instrument from a `.yml` file with filename as string.""" conf_file = tmp_path.joinpath("config.yml") conf_file.write_text(""" test: class: !!python/name:instruments.Instrument uri: test:// """) insts = load_instruments(str(conf_file.absolute())) 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) 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") def test_load_test_instrument_oserror(mocker): """Raise warning and continue in case loading test instrument fails with OSError.""" config_data = StringIO(""" test: class: !!python/name:instruments.Instrument uri: test:// """) mocker.patch.object(Instrument, "open_from_uri", side_effect=OSError) with pytest.warns(RuntimeWarning): _ = load_instruments(config_data) ================================================ FILE: tests/test_delta_elektronika/__init__.py ================================================ ================================================ FILE: tests/test_delta_elektronika/test_psc_eth.py ================================================ #!/usr/bin/env python """Tests for the Delta Elektronika PSC-ETH interface.""" from hypothesis import given, strategies as st import pytest import instruments as ik from instruments.units import ureg as u from tests import expected_protocol, make_name_test, unit_eq # TEST CLASS PROPERTIES # test_psc_eth_device_name = make_name_test(ik.delta_elektronika.PscEth) def test_current_limit(): """Get the current limit of the instrument.""" with expected_protocol( ik.delta_elektronika.PscEth, ["SYST:LIM:CUR?", "SYST:LIM:CUR?"], ["0.0,OFF", "0.2,ON"], sep="\n", ) as rf: status, value = rf.current_limit assert status == ik.delta_elektronika.PscEth.LimitStatus.OFF unit_eq(value, 0.0 * u.A) status, value = rf.current_limit assert status == ik.delta_elektronika.PscEth.LimitStatus.ON unit_eq(value, 0.2 * u.A) def test_voltage_limit(): """Get the voltage limit of the instrument.""" with expected_protocol( ik.delta_elektronika.PscEth, ["SYST:LIM:VOL?", "SYST:LIM:VOL?"], ["0.0,OFF", "20.0,ON"], sep="\n", ) as rf: status, value = rf.voltage_limit assert status == ik.delta_elektronika.PscEth.LimitStatus.OFF unit_eq(value, 0.0 * u.V) status, value = rf.voltage_limit assert status == ik.delta_elektronika.PscEth.LimitStatus.ON unit_eq(value, 20.0 * u.V) def test_current(): """Get/set the output current of the instrument.""" with expected_protocol( ik.delta_elektronika.PscEth, ["SOUR:CURR?", f"SOUR:CURR {0.1:.15f}", "SOUR:CURR?"], ["0.0", "0.1"], sep="\n", ) as rf: unit_eq(rf.current, 0.0 * u.A) rf.current = 0.1 * u.A unit_eq(rf.current, 0.1 * u.A) def test_current_max(): """Get/set the maximum output current of the instrument.""" with expected_protocol( ik.delta_elektronika.PscEth, ["SOUR:CURR:MAX?", f"SOUR:CURR:MAX {0.2:.15f}", "SOUR:CURR:MAX?"], ["0.1", "0.2"], sep="\n", ) as rf: unit_eq(rf.current_max, 0.1 * u.A) rf.current_max = 0.2 * u.A unit_eq(rf.current_max, 0.2 * u.A) def test_current_measure(): """Get the measured output current of the instrument.""" with expected_protocol( ik.delta_elektronika.PscEth, ["MEAS:CURR?", "MEAS:CURR?"], ["0.0", "0.1"], sep="\n", ) as rf: unit_eq(rf.current_measure, 0.0 * u.A) unit_eq(rf.current_measure, 0.1 * u.A) def test_current_stepsize(): """Get the current stepsize of the instrument.""" with expected_protocol( ik.delta_elektronika.PscEth, ["SOUR:CUR:STE?", "SOUR:CUR:STE?"], ["0.001", "0.01"], sep="\n", ) as rf: unit_eq(rf.current_stepsize, 0.001 * u.A) unit_eq(rf.current_stepsize, 0.01 * u.A) def test_voltage(): """Get/set the output voltage of the instrument.""" with expected_protocol( ik.delta_elektronika.PscEth, ["SOUR:VOL?", f"SOUR:VOL {10.0:.15f}", "SOUR:VOL?"], ["0.0", "10.0"], sep="\n", ) as rf: unit_eq(rf.voltage, 0.0 * u.V) rf.voltage = 10.0 * u.V unit_eq(rf.voltage, 10.0 * u.V) def test_voltage_max(): """Get/set the maximum output voltage of the instrument.""" with expected_protocol( ik.delta_elektronika.PscEth, ["SOUR:VOLT:MAX?", f"SOUR:VOLT:MAX {20.0:.15f}", "SOUR:VOLT:MAX?"], ["10.0", "20.0"], sep="\n", ) as rf: unit_eq(rf.voltage_max, 10.0 * u.V) rf.voltage_max = 20.0 * u.V unit_eq(rf.voltage_max, 20.0 * u.V) def test_voltage_measure(): """Get the measured output voltage of the instrument.""" with expected_protocol( ik.delta_elektronika.PscEth, ["MEAS:VOLT?", "MEAS:VOLT?"], ["0.0", "10.0"], sep="\n", ) as rf: unit_eq(rf.voltage_measure, 0.0 * u.V) unit_eq(rf.voltage_measure, 10.0 * u.V) def test_voltage_stepsize(): """Get the voltage stepsize of the instrument.""" with expected_protocol( ik.delta_elektronika.PscEth, ["SOUR:VOL:STE?", "SOUR:VOL:STE?"], ["0.01", "0.1"], sep="\n", ) as rf: unit_eq(rf.voltage_stepsize, 0.01 * u.V) unit_eq(rf.voltage_stepsize, 0.1 * u.V) # TEST CLASS METHODS # def test_recall(): """Recall a stored setting from non-volatile memory.""" with expected_protocol( ik.delta_elektronika.PscEth, ["*RCL"], [], sep="\n", ) as rf: rf.recall() def test_reset(): """Reset the instrument to default settings.""" with expected_protocol( ik.delta_elektronika.PscEth, ["*RST"], [], sep="\n", ) as rf: rf.reset() def test_save(): """Save the current settings to non-volatile memory.""" with expected_protocol( ik.delta_elektronika.PscEth, ["*SAV"], [], sep="\n", ) as rf: rf.save() def test_set_current_limit(): """Set the current limit of the instrument.""" with expected_protocol( ik.delta_elektronika.PscEth, [f"SYST:LIM:CUR {0.0:.15f},OFF", f"SYST:LIM:CUR {0.2:.15f},ON"], [], sep="\n", ) as rf: rf.set_current_limit(ik.delta_elektronika.PscEth.LimitStatus.OFF) rf.set_current_limit(ik.delta_elektronika.PscEth.LimitStatus.ON, 0.2 * u.A) def test_set_current_limit_invalid_type(): """Setting current limit with invalid type raises TypeError.""" with expected_protocol( ik.delta_elektronika.PscEth, [], [], sep="\n", ) as rf: with pytest.raises(TypeError): rf.set_current_limit("ON", 0.2 * u.A) def test_set_voltage_limit(): """Set the voltage limit of the instrument.""" with expected_protocol( ik.delta_elektronika.PscEth, [f"SYST:LIM:VOL {0.0:.15f},OFF", f"SYST:LIM:VOL {20.0:.15f},ON"], [], sep="\n", ) as rf: rf.set_voltage_limit(ik.delta_elektronika.PscEth.LimitStatus.OFF) rf.set_voltage_limit(ik.delta_elektronika.PscEth.LimitStatus.ON, 20.0 * u.V) def test_set_voltage_limit_invalid_type(): """Setting voltage limit with invalid type raises TypeError.""" with expected_protocol( ik.delta_elektronika.PscEth, [], [], sep="\n", ) as rf: with pytest.raises(TypeError): rf.set_voltage_limit("ON", 20.0 * u.V) ================================================ FILE: tests/test_dressler/__init__.py ================================================ ================================================ FILE: tests/test_dressler/test_cesar_1312.py ================================================ #!/usr/bin/env python """Tests for the Dressler Cesar 1312 RF generator.""" from hypothesis import given, strategies as st import pytest import instruments as ik from instruments.units import ureg as u from tests import expected_protocol # CONSTANTS # ACK = bytes([0x06]) NAK = bytes([0x15]) # TEST CLASS PROPERTIES # def test_address(): """Set/get the address of the instrument.""" with expected_protocol( ik.dressler.Cesar1312, [], [], sep="", ) as rf: assert rf.address == 0x01 rf.address = 5 assert rf.address == 5 for addr in [-1, 32]: with pytest.raises(ValueError): rf.address = addr def test_retries(): """Set/get the number of retries.""" with expected_protocol( ik.dressler.Cesar1312, [], [], sep="", ) as rf: assert rf.retries == 3 rf.retries = 5 assert rf.retries == 5 with pytest.raises(ValueError): rf.retries = -1 # TEST INSTRUMENT PROPERTIES # def test_control_mode(): """Get/set the control model.""" read_mode = bytes([0x08, 0x9B, 0x93]) read_mode_answ_front_panel = bytes([0x09, 0x9B, 0x06, 0x94]) set_mode_host = bytes([0x09, 0x0E, 0x02, 0x05]) set_mode_answ = bytes([0x09, 0x0E, 0x00, 0x07]) read_mode_answ_host = bytes([0x09, 0x9B, 0x02, 0x90]) with expected_protocol( ik.dressler.Cesar1312, [ read_mode, ACK, set_mode_host, ACK, read_mode, ACK, ], [ ACK, read_mode_answ_front_panel, ACK, set_mode_answ, ACK, read_mode_answ_host, ], sep="", ) as rf: assert rf.control_mode == rf.ControlMode.FrontPanel rf.control_mode = rf.ControlMode.Host assert rf.control_mode == rf.ControlMode.Host def test_name(): """Get the supply type and size.""" cmd_type = bytes([0x08, 0x80, 0x88]) ascii_type = b"CESAR" ans_type = bytes([0x0D, 0x80]) + ascii_type ans_type += checksum(ans_type) cmd_size = bytes([0x08, 0x81, 0x89]) ascii_size = b"_1312" ans_size = bytes([0x0D, 0x81]) + ascii_size ans_size += checksum(ans_size) with expected_protocol( ik.dressler.Cesar1312, [ cmd_type, ACK, cmd_size, ACK, ], [ ACK, ans_type, ACK, ans_size, ], sep="", ) as rf: assert rf.name == "CESAR_1312" def test_output_power(): """Get/set the output power of the RF generator.""" set_power_1kW = bytes([0x0A, 0x08, 0xE8, 0x03, 0xE9]) set_power_answ = bytes([0x09, 0x08, 0x00, 0x01]) read_power = bytes([0x08, 0xA4, 0xAC]) read_answ = bytes([0x0B, 0xA4, 0xE8, 0x03, 0x06, 0x42]) with expected_protocol( ik.dressler.Cesar1312, [ set_power_1kW, ACK, set_power_1kW, ACK, read_power, ACK, ], [ ACK, set_power_answ, ACK, set_power_answ, ACK, read_answ, ], sep="", ) as rf: rf.output_power = 1000 rf.output_power = u.Quantity(1, u.kW) assert rf.output_power == u.Quantity(1000, u.W) def test_regulation_mode(): """Get/set regulation mode.""" read_mode = bytes([0x08, 0x9A, 0x92]) read_answ_load = bytes([0x09, 0x9A, 0x08, 0x9B]) set_mode_fwd = bytes([0x09, 0x03, 0x06, 0x0C]) set_mode_answ = bytes([0x09, 0x03, 0x00, 0x0A]) read_answ_fwd = bytes([0x09, 0x9A, 0x06, 0x95]) with expected_protocol( ik.dressler.Cesar1312, [ read_mode, ACK, set_mode_fwd, ACK, read_mode, ACK, ], [ ACK, read_answ_load, ACK, set_mode_answ, ACK, read_answ_fwd, ], sep="", ) as rf: assert rf.regulation_mode == rf.RegulationMode.ExternalPower rf.regulation_mode = rf.RegulationMode.ForwardPower assert rf.regulation_mode == rf.RegulationMode.ForwardPower def test_reflected_power(): """Get the reflected power.""" read_send = bytes([0x08, 0xA6, 0xAE]) read_answ = bytes([0x0A, 0xA6, 0x01, 0x00, 0xAD]) with expected_protocol( ik.dressler.Cesar1312, [ read_send, ACK, ], [ACK, read_answ], sep="", ) as rf: assert rf.reflected_power == u.Quantity(1, u.W) def test_rf(): """Set/get the RF output state.""" rf_read = bytes([0x08, 0xA2, 0xAA]) rf_read_answ_on = bytes([0x0C, 0xA2, 0x20, 0x00, 0x00, 0x00, 0x8E]) rf_read_answ_off = bytes([0x0C, 0xA2, 0x00, 0x00, 0x00, 0x00, 0xAE]) rf_on = bytes([0x08, 0x02, 0x0A]) rf_on_answ = bytes([0x09, 0x02, 0x00, 0x0B]) rf_off = bytes([0x08, 0x01, 0x09]) rf_off_answ = bytes([0x09, 0x01, 0x00, 0x08]) with expected_protocol( ik.dressler.Cesar1312, [ rf_read, ACK, rf_off, ACK, rf_on, ACK, rf_read, ACK, ], [ ACK, rf_read_answ_off, ACK, rf_off_answ, ACK, rf_on_answ, ACK, rf_read_answ_on, ], sep="", ) as rf: assert not rf.rf rf.rf = False rf.rf = True assert rf.rf def test_rf_cmd_invalid(): """Raise OSError if acknowledgement of cmd fails after retries.""" rf_read = bytes([0x08, 0xA2, 0xAA]) with expected_protocol( ik.dressler.Cesar1312, [ rf_read, rf_read, ], [ NAK, NAK, ], sep="", ) as rf: rf.retries = 1 with pytest.raises(OSError): rf.rf def test_rf_reply_invalid(): """Raise OSError if acknowledgement of reply fails after retries.""" rf_read = bytes([0x08, 0xA2, 0xAA]) rf_read_answ_on = bytes([0x0C, 0xA2, 0x20, 0x00, 0x00, 0x00, 0xFF]) # bad checksum with expected_protocol( ik.dressler.Cesar1312, [ rf_read, NAK, NAK, ], [ ACK, rf_read_answ_on, rf_read_answ_on, rf_read_answ_on, ], sep="", ) as rf: rf.retries = 2 with pytest.raises(OSError): rf.rf def test_unknown_command(): """Raise OSError if an unknown command is sent.""" pkg_send = bytes([0x08, 0x00, 0x08]) pkg_rec = bytes([0x09, 0x80, 0x63, 0xEA]) with expected_protocol( ik.dressler.Cesar1312, [ pkg_send, ACK, ], [ ACK, pkg_rec, ], sep="", ) as rf: with pytest.raises(OSError) as err: rf.sendcmd(rf._make_pkg(0)) assert "Command not implemented" in err.value.args[0] def test_device_returns_no_data(): """Raise ValueError if device returned no data.""" pkg_send = bytes([0x08, 0x00, 0x08]) pkg_rec = bytes([0x08, 0x80, 0x88]) with expected_protocol( ik.dressler.Cesar1312, [ pkg_send, ACK, ], [ ACK, pkg_rec, ], sep="", ) as rf: with pytest.raises(ValueError) as err: rf.sendcmd(rf._make_pkg(0)) assert "No data received from the device" in err.value.args[0] def test_answer_longer_six_bytes(): """Ensure that answers with data >6 bytes are handled correctly. Note: While the protocol describes this scenario and it is implemented into the query, the whole command book does not offer a single command where this case occurs. However, this might be different for other models. """ pkg_send = bytes([0x08, 0x00, 0x08]) rec_data = bytes([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]) pkg_rec = bytes([0x0F, 0x00, 0x08]) + rec_data pkg_rec += checksum(pkg_rec) with expected_protocol( ik.dressler.Cesar1312, [ pkg_send, ACK, ], [ ACK, pkg_rec, ], sep="", ) as rf: data = rf.query(rf._make_pkg(0)) assert data == rec_data # TEST PRIVATE METHODS # def test_make_pkg(): """Create a package that can be sent to the instrument.""" with expected_protocol( ik.dressler.Cesar1312, [], [], sep="", ) as rf: # simple query package with command 1 pkg_exp = bytes([0b00001000, 0x01]) pkg_exp += rf._calculate_checksum(pkg_exp) pkg = rf._make_pkg(1, None) assert pkg == pkg_exp pkg = rf._make_pkg(1) # should be same as above assert pkg == pkg_exp # change address to 3 and send 4 bytes of 1024 to command 2 pkg_exp = bytes([0b00011100, 0x02, 0x00, 0x04, 0x00, 0x00]) pkg_exp += rf._calculate_checksum(pkg_exp) rf.address = 3 pkg = rf._make_pkg(2, (1024).to_bytes(4, "little")) assert pkg == pkg_exp # some long command with lots of data, using the make data pkg rf.address = 1 pkg_exp = bytes( [ 0b00001111, 0x01, 0x09, 0x01, 0x02, 0x03, 0x04, 0x05, 0x00, 0x04, 0x00, 0x00, ] ) data = rf._make_data([1, 1, 1, 1, 1, 4], [1, 2, 3, 4, 5, 1024]) pkg_exp += rf._calculate_checksum(pkg_exp) pkg = rf._make_pkg(1, data) assert pkg == pkg_exp def test_make_pkg_error(): """Raise error if cmd is too large or data is too long.""" with expected_protocol( ik.dressler.Cesar1312, [], [], sep="", ) as rf: with pytest.raises(ValueError): rf._make_pkg(256, None) with pytest.raises(ValueError): rf._make_pkg(1, (1).to_bytes(256, "little")) @given(values=st.lists(st.integers(min_value=0, max_value=255), min_size=1)) def test_checksum(values): """Assure that exclusive or of all bytes plus checksum is 0.""" bts = bytes(values) with expected_protocol( ik.dressler.Cesar1312, [], [], sep="", ) as rf: checksum = rf._calculate_checksum(bts)[0] for val in bts: checksum ^= val assert checksum == 0 @given( addr=st.integers(min_value=0, max_value=31), data_length=st.integers(min_value=0, max_value=255), ) def test_pack_header(addr, data_length): """Pack the header of the package.""" with expected_protocol( ik.dressler.Cesar1312, [], [], sep="", ) as rf: rf.address = addr header = rf._pack_header(data_length) dl = 7 if data_length > 6 else data_length header_exp = (addr << 3) + dl assert header == header_exp @given(hdr_int=st.integers(min_value=0, max_value=255)) def test_unpack_header(hdr_int): """Unpack a header to return address and data length.""" hdr = hdr_int.to_bytes(1, "little") with expected_protocol( ik.dressler.Cesar1312, [], [], sep="", ) as rf: addr, dl = rf._unpack_header(hdr) assert addr == hdr_int >> 3 assert dl == hdr_int & 0b00000111 def checksum(values: bytes) -> bytes: """Calculate the checksum of the given values.""" checksum = 0x00 for val in values: checksum ^= val return bytes([checksum]) ================================================ FILE: tests/test_fluke/__init__.py ================================================ ================================================ FILE: tests/test_fluke/test_fluke3000.py ================================================ #!/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 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}" ================================================ FILE: tests/test_generic_scpi/__init__.py ================================================ ================================================ FILE: tests/test_generic_scpi/test_scpi_function_generator.py ================================================ #!/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 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 ================================================ FILE: tests/test_generic_scpi/test_scpi_instrument.py ================================================ #!/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 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." ================================================ FILE: tests/test_generic_scpi/test_scpi_multimeter.py ================================================ #!/usr/bin/env python """ Module containing tests for generic SCPI multimeter instruments """ # IMPORTS #################################################################### import pytest from instruments.units import ureg as u import instruments as ik from tests import expected_protocol, make_name_test, unit_eq # TESTS ###################################################################### test_scpi_multimeter_name = make_name_test(ik.generic_scpi.SCPIMultimeter) def test_scpi_multimeter_mode(): with expected_protocol( ik.generic_scpi.SCPIMultimeter, ["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 def test_scpi_multimeter_trigger_mode(): with expected_protocol( 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 def test_scpi_multimeter_input_range(): with expected_protocol( ik.generic_scpi.SCPIMultimeter, [ "CONF?", # 1 "CONF?", # 2 "CONF?", # 3.1 "CONF:FRES MIN", # 3.2 "CONF?", # 4.1 "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 ], ) as dmm: 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 * u.amp def test_scpi_multimeter_resolution(): with expected_protocol( ik.generic_scpi.SCPIMultimeter, [ "CONF?", # 1 "CONF?", # 2 "CONF?", # 3.1 "CONF:FRES +1.000000E+01,MIN", # 3.2 "CONF?", # 4.1 "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 ], ) as dmm: assert dmm.resolution == 3e-06 assert dmm.resolution == dmm.Resolution.maximum dmm.resolution = dmm.Resolution.minimum 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"], [ "+10", "INF", ], ) as dmm: assert dmm.trigger_count == 10 assert dmm.trigger_count == dmm.TriggerCount.infinity dmm.trigger_count = dmm.TriggerCount.minimum 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"], [ "+10", "MAX", ], ) as dmm: assert dmm.sample_count == 10 assert dmm.sample_count == dmm.SampleCount.maximum dmm.sample_count = dmm.SampleCount.minimum 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?", f"TRIG:DEL {1:e}", ], [ "+1", ], ) as dmm: unit_eq(dmm.trigger_delay, 1 * u.second) dmm.trigger_delay = 1000 * u.millisecond def test_scpi_multimeter_sample_source(): with expected_protocol( ik.generic_scpi.SCPIMultimeter, [ "SAMP:SOUR?", "SAMP:SOUR TIM", ], [ "IMM", ], ) as dmm: assert dmm.sample_source == dmm.SampleSource.immediate dmm.sample_source = dmm.SampleSource.timer def test_scpi_multimeter_sample_timer(): with expected_protocol( ik.generic_scpi.SCPIMultimeter, [ "SAMP:TIM?", f"SAMP:TIM {1:e}", ], [ "+1", ], ) as dmm: 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(): with expected_protocol( 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(), 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." ) ================================================ FILE: tests/test_gentec_eo/__init__.py ================================================ ================================================ FILE: tests/test_gentec_eo/test_blu.py ================================================ #!/usr/bin/env python """ Module containing tests for the Gentec-eo Blu """ # IMPORTS #################################################################### from hypothesis import given from hypothesis import strategies as st import pytest import instruments as ik from instruments.units import ureg as u from tests import expected_protocol # 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", "*OFF00000042"], # 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", "*OFF00000042"], # 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, ["*OFF00000042"], ["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) if value > 0: assert value == pytest.approx(float(value_read), rel=0.01) else: assert value == pytest.approx(float(value_read), rel=0.05) assert len(value_read) == 8 ================================================ FILE: tests/test_glassman/__init__.py ================================================ ================================================ FILE: tests/test_glassman/test_glassmanfr.py ================================================ #!/usr/bin/env python """ Module containing tests for the Glassman FR power supply """ # IMPORTS #################################################################### import pytest import instruments as ik from 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}" ================================================ FILE: tests/test_hcp/__init__.py ================================================ ================================================ FILE: tests/test_hcp/test_tc038.py ================================================ #!/usr/bin/env python """ Unit tests for the HCP TC038 """ # IMPORTS ##################################################################### from tests import expected_protocol, unit_eq, pytest from instruments.units import ureg as u from instruments.hcp import TC038 def test_sendcmd(): with expected_protocol(TC038, ["\x0201010x\x03"], [], sep="\r") as inst: inst.sendcmd("x") def test_query(): with expected_protocol(TC038, ["\x0201010x\x03"], ["y"], sep="\r") as inst: assert inst.query("x") == "y" def test_setpoint(): with expected_protocol( TC038, ["\x0201010WRDD0120,01\x03"], ["\x020101OK00C8\x03"], sep="\r" ) as inst: value = inst.setpoint unit_eq(value, u.Quantity(20, u.degC)) def test_setpoint_setter(): # Communication from manual. with expected_protocol( TC038, ["\x0201010WWRD0120,01,00C8\x03"], ["\x020101OK\x03"], sep="\r" ) as inst: inst.setpoint = 20 def test_temperature(): # Communication from manual. with expected_protocol( TC038, ["\x0201010WRDD0002,01\x03"], ["\x020101OK00C8\x03"], sep="\r" ) as inst: value = inst.temperature unit_eq(value, u.Quantity(20, u.degC)) def test_monitored(): # Communication from manual. with expected_protocol( TC038, ["\x0201010WRM\x03"], ["\x020101OK00C8\x03"], sep="\r" ) as inst: value = inst.monitored_value unit_eq(value, u.Quantity(20, u.degC)) def test_set_monitored(): # Communication from manual. with expected_protocol( TC038, ["\x0201010WRS01D0002\x03"], ["\x020101OK\x03"], sep="\r" ) as inst: inst.monitored_quantity = "temperature" assert inst.monitored_quantity == "temperature" def test_set_monitored_wrong_input(): with expected_protocol(TC038, [], [], sep="\r") as inst: with pytest.raises(AssertionError): inst.monitored_quantity = "temper" def test_information(): # Communication from manual. with expected_protocol( TC038, ["\x0201010INF6\x03"], ["\x020101OKUT150333 V01.R001111222233334444\x03"], sep="\r", ) as inst: value = inst.information assert value == "UT150333 V01.R001111222233334444" ================================================ FILE: tests/test_hcp/test_tc038d.py ================================================ #!/usr/bin/env python """ Unit tests for the HCP TC038D """ # IMPORTS ##################################################################### from tests import expected_protocol, unit_eq, pytest from instruments.units import ureg as u from instruments.hcp import TC038D def test_write_multiple(): # Communication from manual. with expected_protocol( TC038D, [b"\x01\x10\x01\x0a\x00\x04\x08\x00\x00\x03\xe8\xff\xff\xfc\x18\x8d\xe9"], [b"\x01\x10\x01\x0a\x00\x04\xe0\x34"], sep="", ) as inst: inst.writeMultiple(0x010A, [1000, -1000]) def test_write_multiple_CRC_error(): with expected_protocol( TC038D, [b"\x01\x10\x01\x06\x00\x02\x04\x00\x00\x01A\xbf\xb5"], [b"\x01\x10\x01\x06\x00\x02\x01\x02"], sep="", ) as inst: with pytest.raises(ConnectionError): inst.setpoint = u.Quantity(32.1, u.degC) def test_write_multiple_wrong_values(): with expected_protocol( TC038D, [], [], sep="", ) as inst: with pytest.raises(ValueError): inst.writeMultiple(0x010A, 5.5) def test_write_multiple_Value_error(): with expected_protocol( TC038D, [b"\x01\x10\x01\x06\x00\x02\x04\x00\x00\x01A\xbf\xb5"], [b"\x01\x90\x02\x06\x00"], sep="", ) as inst: with pytest.raises(ValueError) as exc: inst.setpoint = u.Quantity(32.1, u.degC) assert str(exc) == "Wrong start address" def test_read_CRC_error(): with expected_protocol( TC038D, [b"\x01\x03\x00\x00\x00\x02\xc4\x0b"], [b"\x01\x03\x04\x00\x00\x03\xe8\x01\x02"], sep="", ) as inst: with pytest.raises(ConnectionError): inst.temperature def test_read_address_error(): with expected_protocol( TC038D, [b"\x01\x03\x00\x00\x00\x02\xc4\x0b"], [b"\x01\x83\x02\01\02"], sep="", ) as inst: with pytest.raises(ValueError): inst.temperature def test_read_elements_error(): with expected_protocol( TC038D, [b"\x01\x03\x00\x00\x00\x02\xc4\x0b"], [b"\x01\x83\x03\01\02"], sep="", ) as inst: with pytest.raises(ValueError): inst.temperature def test_read_any_error(): with expected_protocol( TC038D, [b"\x01\x03\x00\x00\x00\x02\xc4\x0b"], [b"\x01\x43\x05\01\02"], sep="", ) as inst: with pytest.raises(ConnectionError): inst.temperature def test_setpoint(): with expected_protocol( TC038D, [b"\x01\x03\x01\x06\x00\x02\x25\xf6"], [b"\x01\x03\x04\x00\x00\x00\x99:Y"], sep="", ) as inst: value = inst.setpoint unit_eq(value, u.Quantity(15.3, u.degC)) def test_setpoint_setter(): with expected_protocol( TC038D, [b"\x01\x10\x01\x06\x00\x02\x04\x00\x00\x01A\xbf\xb5"], [b"\x01\x10\x01\x06\x00\x02\xa0\x35"], sep="", ) as inst: inst.setpoint = u.Quantity(32.1, u.degC) def test_temperature(): # Communication from manual. # Tests readRegister as well. with expected_protocol( TC038D, [b"\x01\x03\x00\x00\x00\x02\xc4\x0b"], [b"\x01\x03\x04\x00\x00\x03\xe8\xfa\x8d"], sep="", ) as inst: value = inst.temperature unit_eq(value, u.Quantity(100, u.degC)) ================================================ FILE: tests/test_holzworth/__init__.py ================================================ ================================================ FILE: tests/test_holzworth/test_holzworth_hs9000.py ================================================ #!/usr/bin/env python """ Unit tests for the Holzworth HS9000 """ # IMPORTS ##################################################################### from instruments.units import ureg as u import instruments as ik from tests import expected_protocol from .. import mock # TEST CLASSES ################################################################ # pylint: disable=protected-access def test_hs9000_name(): with expected_protocol( ik.holzworth.HS9000, [":ATTACH?", ":CH1:IDN?"], [":CH1:CH2:FOO", "Foobar name"], sep="\n", ) as hs: assert hs.name == "Foobar name" def test_channel_idx_list(): with expected_protocol( ik.holzworth.HS9000, [ ":ATTACH?", ], [":CH1:CH2:FOO"], sep="\n", ) as hs: assert hs._channel_idxs() == [0, 1, "FOO"] def test_channel_returns_inner_class(): with expected_protocol( ik.holzworth.HS9000, [ ":ATTACH?", ], [":CH1:CH2:FOO"], sep="\n", ) as hs: channel = hs.channel[0] assert isinstance(channel, hs.Channel) is True assert channel._ch_name == "CH1" def test_channel_sendcmd(): channel = ik.holzworth.HS9000.Channel(mock.MagicMock(), 0) channel.sendcmd("FOO") channel._hs.sendcmd.assert_called_with(":CH1:FOO") def test_channel_query(): channel = ik.holzworth.HS9000.Channel(mock.MagicMock(), 0) channel._hs.query.return_value = "FOO" value = channel.query("BAR") channel._hs.query.assert_called_with(":CH1:BAR") assert value == "FOO" def test_channel_reset(): channel = ik.holzworth.HS9000.Channel(mock.MagicMock(), 0) channel.reset() channel._hs.sendcmd.assert_called_with(":CH1:*RST") def test_channel_recall_state(): channel = ik.holzworth.HS9000.Channel(mock.MagicMock(), 0) channel.recall_state() channel._hs.sendcmd.assert_called_with(":CH1:*RCL") def test_channel_save_state(): channel = ik.holzworth.HS9000.Channel(mock.MagicMock(), 0) channel.save_state() channel._hs.sendcmd.assert_called_with(":CH1:*SAV") def test_channel_temperature(): with expected_protocol( ik.holzworth.HS9000, [":ATTACH?", ":CH1:TEMP?"], [":CH1:CH2:FOO", "10 C"], sep="\n", ) as hs: channel = hs.channel[0] 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", ) as hs: channel = hs.channel[0] 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?", f":CH1:FREQ {1:e}"], [":CH1:CH2:FOO", "100 MHz", "10 GHz"], sep="\n", ) as hs: channel = hs.channel[0] 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", ) as hs: channel = hs.channel[0] 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?", f":CH1:PWR {0:e}"], [":CH1:CH2:FOO", "-100", "20"], sep="\n", ) as hs: channel = hs.channel[0] 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", ) as hs: channel = hs.channel[0] 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?", f":CH1:PHASE {0:e}"], [":CH1:CH2:FOO", "-180", "+180"], sep="\n", ) as hs: channel = hs.channel[0] 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", ) as hs: channel = hs.channel[0] assert channel.output is False channel.output = True channel.output = False def test_hs9000_is_ready(): with expected_protocol( ik.holzworth.HS9000, [":COMM:READY?", ":COMM:READY?"], ["Ready", "DANGER DANGER"], sep="\n", ) as hs: assert hs.ready is True assert hs.ready is False ================================================ FILE: tests/test_hp/__init__.py ================================================ ================================================ FILE: tests/test_hp/test_hp3325a.py ================================================ #!/usr/bin/env python """ Unit tests for the HP 3325a function generator """ # IMPORTS ##################################################################### import time import pytest from instruments.units import ureg as u import instruments as ik from tests import expected_protocol # 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_hp3325a_high_voltage(): with expected_protocol( ik.hp.hp3325a.HP3325a, [ "IHV", ], ["HV0"], sep="\r\n", ) as fcngen: assert not fcngen.high_voltage with expected_protocol( ik.hp.hp3325a.HP3325a, [ "IHV", ], ["HV1"], sep="\r\n", ) as fcngen: assert fcngen.high_voltage def test_hp3325a_phase(): with expected_protocol( ik.hp.hp3325a.HP3325a, [ "IPH", ], ["PH10DE"], sep="\r\n", ) as fcngen: assert fcngen.phase == u.Quantity(10, "deg") def test_hp3325a_amplitude(): with expected_protocol( ik.hp.hp3325a.HP3325a, [ "IAM", ], ["AM1.2VO"], sep="\r\n", ) as fcngen: assert fcngen.amplitude == u.Quantity(1.2, "V") def test_hp3325a_frequency(): with expected_protocol( ik.hp.hp3325a.HP3325a, [ "IFR", ], ["FR1000.0HZ"], sep="\r\n", ) as fcngen: assert fcngen.frequency == u.Quantity(1000, "Hz") def test_hp3325a_offset(): with expected_protocol( ik.hp.hp3325a.HP3325a, [ "IOF", ], ["OF0.123VO"], sep="\r\n", ) as fcngen: assert fcngen.offset == u.Quantity(0.123, "V") def test_hp3325a_commands(): with expected_protocol( ik.hp.hp3325a.HP3325a, ["AC", "AP", "IER"], ["ER0"], sep="\r\n", ) as fcngen: fcngen.amplitude_calibration() fcngen.assign_zero_phase() fcngen.query_error() ================================================ FILE: tests/test_hp/test_hp3456a.py ================================================ #!/usr/bin/env python """ Unit tests for the HP 3456a digital voltmeter """ # IMPORTS ##################################################################### import time import pytest import instruments as ik from 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", ) as dmm: dmm.trigger_mode = dmm.TriggerMode.hold def test_hp3456a_number_of_digits(): 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" ) as dmm: dmm.number_of_digits = 6 assert dmm.number_of_digits == 6 def test_hp3456a_auto_range(): with expected_protocol( ik.hp.HP3456a, [ "HO0T4SO1", "R1W", ], [""], 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" ) as dmm: dmm.number_of_readings = 10 assert dmm.number_of_readings == 10 def test_hp3456a_nplc(): with expected_protocol( ik.hp.HP3456a, ["HO0T4SO1", "W1STI", "REI"], ["+1.00000E+0"], sep="\r" ) as dmm: dmm.nplc = 1 assert dmm.nplc == 1 def test_hp3456a_nplc_invalid(): with pytest.raises(ValueError), expected_protocol( ik.hp.HP3456a, ["HO0T4SO1", "W1STI", "REI"], ["+1.00000E+0"], sep="\r" ) as dmm: dmm.nplc = 0 def test_hp3456a_mode(): with expected_protocol( ik.hp.HP3456a, [ "HO0T4SO1", "S0F4", ], [""], sep="\r", ) as dmm: dmm.mode = dmm.Mode.resistance_2wire def test_hp3456a_math_mode(): with expected_protocol( ik.hp.HP3456a, [ "HO0T4SO1", "M2", ], [""], sep="\r", ) as dmm: dmm.math_mode = dmm.MathMode.statistic def test_hp3456a_trigger(): with expected_protocol( ik.hp.HP3456a, [ "HO0T4SO1", "T3", ], [""], sep="\r", ) as dmm: dmm.trigger() def test_hp3456a_fetch(): with expected_protocol( ik.hp.HP3456a, ["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", ], sep="\r", ) as dmm: v = dmm.fetch(dmm.Mode.resistance_2wire) assert v == [0.1055 * u.ohm, 0.1043 * u.ohm, 0.1005 * u.ohm, 0.1014 * u.ohm] v = dmm.fetch() assert v == [0.1055, 0.1043, 0.1005, 0.1014] def test_hp3456a_variance(): with expected_protocol( ik.hp.HP3456a, [ "HO0T4SO1", "REV", ], ["+04.93111E-6"], sep="\r", ) as dmm: assert dmm.variance == +04.93111e-6 def test_hp3456a_count(): with expected_protocol( ik.hp.HP3456a, [ "HO0T4SO1", "REC", ], ["+10.00000E+0"], sep="\r", ) as dmm: assert dmm.count == +10 def test_hp3456a_mean(): with expected_protocol( ik.hp.HP3456a, [ "HO0T4SO1", "REM", ], ["+102.1000E-3"], sep="\r", ) as dmm: assert dmm.mean == +102.1000e-3 def test_hp3456a_delay(): with expected_protocol( ik.hp.HP3456a, ["HO0T4SO1", "RED", "W1STD"], ["-000.0000E+0"], sep="\r" ) as dmm: assert dmm.delay == 0 dmm.delay = 1 * u.sec def test_hp3456a_lower(): with expected_protocol( 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 def test_hp3456a_upper(): with expected_protocol( 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 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", ) as dmm: 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", ) as dmm: assert dmm.measure(dmm.Mode.ratio_dcv_dcv) == 0 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" ) as dmm: dmm.input_range = 10**-1 * u.volt dmm.input_range = 1e3 * u.ohm with pytest.raises(NotImplementedError): _ = dmm.input_range def test_hp3456a_input_range_invalid_str(): with pytest.raises(ValueError), expected_protocol( ik.hp.HP3456a, [], [], sep="\r" ) as dmm: dmm.input_range = "derp" def test_hp3456a_input_range_invalid_range(): with pytest.raises(ValueError), expected_protocol( ik.hp.HP3456a, [], [], sep="\r" ) as dmm: dmm.input_range = 1 * u.ohm def test_hp3456a_input_range_bad_type(): with pytest.raises(TypeError), expected_protocol( ik.hp.HP3456a, [], [], sep="\r" ) as dmm: dmm.input_range = True def test_hp3456a_input_range_bad_units(): with pytest.raises(ValueError), expected_protocol( ik.hp.HP3456a, [], [], sep="\r" ) as dmm: dmm.input_range = 1 * u.amp def test_hp3456a_relative(): with expected_protocol( ik.hp.HP3456a, [ "HO0T4SO1", "M0", "M3", ], [ "", ], sep="\r", ) as dmm: dmm.relative = False dmm.relative = True assert dmm.relative is True def test_hp3456a_relative_bad_type(): with pytest.raises(TypeError), expected_protocol( ik.hp.HP3456a, [], [], sep="\r" ) as dmm: dmm.relative = "derp" def test_hp3456a_auto_zero(): with expected_protocol( ik.hp.HP3456a, [ "HO0T4SO1", "Z0", "Z1", ], [ "", ], sep="\r", ) as dmm: dmm.autozero = False dmm.autozero = True def test_hp3456a_filter(): with expected_protocol( ik.hp.HP3456a, [ "HO0T4SO1", "FL0", "FL1", ], [ "", ], sep="\r", ) as dmm: dmm.filter = False dmm.filter = True def test_hp3456a_register_read_bad_name(): with pytest.raises(TypeError), expected_protocol( ik.hp.HP3456a, [], [], sep="\r" ) as dmm: dmm._register_read("foobar") def test_hp3456a_register_write_bad_name(): with pytest.raises(TypeError), expected_protocol( ik.hp.HP3456a, [], [], sep="\r" ) as dmm: dmm._register_write("foobar", 1) def test_hp3456a_register_write_bad_register(): with pytest.raises(ValueError), expected_protocol( ik.hp.HP3456a, [], [], sep="\r" ) as dmm: dmm._register_write(dmm.Register.mean, 1) ================================================ FILE: tests/test_hp/test_hp6624a.py ================================================ #!/usr/bin/env python """ Unit tests for the HP 6624a power supply """ # IMPORTS ##################################################################### import pytest import instruments as ik from tests import ( expected_protocol, iterable_eq, ) from instruments.units import ureg as u from .. import mock # TESTS ####################################################################### # pylint: disable=protected-access def test_channel_returns_inner_class(): 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 def test_channel_sendcmd(): channel = ik.hp.HP6624a.Channel(mock.MagicMock(), 0) channel.sendcmd("FOO") channel._hp.sendcmd.assert_called_with("FOO 1") def test_channel_sendcmd_2(): channel = ik.hp.HP6624a.Channel(mock.MagicMock(), 0) channel.sendcmd("FOO 5") channel._hp.sendcmd.assert_called_with("FOO 1,5") def test_channel_query(): channel = ik.hp.HP6624a.Channel(mock.MagicMock(), 0) channel._hp.query.return_value = "FOO" value = channel.query("BAR?") channel._hp.query.assert_called_with("BAR? 1") 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", f"VSET 1,{5:.1f}"], ["2"], sep="\n" ) as hp: 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", f"ISET 1,{5:.1f}"], ["2"], sep="\n" ) as hp: 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 * u.V def test_channel_current_sense(): with expected_protocol( ik.hp.HP6624a, [ "IOUT? 1", ], ["2"], sep="\n", ) as hp: assert hp.channel[0].current_sense == 2 * u.A def test_channel_overvoltage(): with expected_protocol( ik.hp.HP6624a, ["OVSET? 1", f"OVSET 1,{5:.1f}"], ["2"], sep="\n" ) as hp: 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: 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: assert hp.channel[0].output is True hp.channel[0].output = True def test_channel_reset(): channel = ik.hp.HP6624a.Channel(mock.MagicMock(), 0) channel.reset() calls = [mock.call("OVRST 1"), mock.call("OCRST 1")] channel._hp.sendcmd.assert_has_calls(calls) def test_all_voltage(): with expected_protocol( ik.hp.HP6624a, [ "VSET? 1", "VSET? 2", "VSET? 3", "VSET? 4", 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}", ], ["2", "3", "4", "5"], sep="\n", ) as hp: 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) def test_all_voltage_wrong_length(): with pytest.raises(ValueError), expected_protocol( ik.hp.HP6624a, [], [], sep="\n" ) as hp: hp.voltage = (1 * u.volt, 2 * u.volt) def test_all_current(): with expected_protocol( ik.hp.HP6624a, [ "ISET? 1", "ISET? 2", "ISET? 3", "ISET? 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", ) as hp: 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) def test_all_current_wrong_length(): with pytest.raises(ValueError), expected_protocol( ik.hp.HP6624a, [], [], sep="\n" ) as hp: 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", ) as hp: 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", ) as hp: 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: hp.clear() def test_channel_count(): with expected_protocol(ik.hp.HP6624a, [], [], sep="\n") as hp: assert hp.channel_count == 4 hp.channel_count = 3 def test_channel_count_wrong_type(): with pytest.raises(TypeError), expected_protocol( ik.hp.HP6624a, [], [], sep="\n" ) as hp: hp.channel_count = "foobar" def test_channel_count_too_small(): with pytest.raises(ValueError), expected_protocol( ik.hp.HP6624a, [], [], sep="\n" ) as hp: hp.channel_count = 0 ================================================ FILE: tests/test_hp/test_hp6632b.py ================================================ #!/usr/bin/env python """ Unit tests for the HP 6632b power supply """ # IMPORTS ##################################################################### import pytest from instruments.units import ureg as u import instruments as ik from tests import expected_protocol, make_name_test, unit_eq # TESTS ####################################################################### test_scpi_multimeter_name = make_name_test(ik.hp.HP6632b) def test_hp6632b_display_textmode(): with expected_protocol( ik.hp.HP6632b, ["DISP:MODE?", "DISP:MODE TEXT"], ["NORM"] ) as psu: assert psu.display_textmode is False psu.display_textmode = True def test_hp6632b_display_text(): with expected_protocol( 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: assert psu.output is False psu.output = True def test_hp6632b_voltage(): 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(): with expected_protocol( ik.hp.HP6632b, [ "MEAS:VOLT?", ], ["10.0"], ) as psu: unit_eq(psu.voltage_sense, 10 * u.volt) def test_hp6632b_overvoltage(): with expected_protocol( ik.hp.HP6632b, ["VOLT:PROT?", f"VOLT:PROT {1:e}"], ["10.0"] ) as psu: unit_eq(psu.overvoltage, 10 * u.volt) psu.overvoltage = 1.0 * u.volt def test_hp6632b_current(): 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(): with expected_protocol( ik.hp.HP6632b, [ "MEAS:CURR?", ], ["10.0"], ) as psu: 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"] ) as psu: assert psu.overcurrent is False psu.overcurrent = True def test_hp6632b_current_sense_range(): with expected_protocol( ik.hp.HP6632b, ["SENS:CURR:RANGE?", f"SENS:CURR:RANGE {1:e}"], ["0.05"] ) as psu: 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"] ) as psu: assert psu.output_dfi_source == psu.DFISource.operation psu.output_dfi_source = psu.DFISource.questionable def test_hp6632b_output_remote_inhibit(): with expected_protocol( 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 def test_hp6632b_digital_function(): with expected_protocol( 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: assert psu.digital_data == 5 psu.digital_data = 1 def test_hp6632b_sense_sweep_points(): with expected_protocol( 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 def test_hp6632b_sense_sweep_interval(): with expected_protocol( ik.hp.HP6632b, ["SENS:SWE:TINT?", f"SENS:SWE:TINT {1e-05:e}"], ["1.56e-05"], ) as psu: 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"] ) as psu: assert psu.sense_window == psu.SenseWindow.hanning psu.sense_window = psu.SenseWindow.rectangular def test_hp6632b_output_protection_delay(): with expected_protocol( 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 * u.second) psu.output_protection_delay = 5e-02 * u.second def test_hp6632b_voltage_alc_bandwidth(): with expected_protocol( ik.hp.HP6632b, [ "VOLT:ALC:BAND?", ], ["6e4"], ) as psu: assert psu.voltage_alc_bandwidth == psu.ALCBandwidth.fast def test_hp6632b_voltage_trigger(): with expected_protocol( ik.hp.HP6632b, ["VOLT:TRIG?", f"VOLT:TRIG {1:e}"], ["1e+0"] ) as psu: 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?", f"CURR:TRIG {0.1:e}"], ["1e-01"] ) as psu: unit_eq(psu.current_trigger, 0.1 * u.amp) psu.current_trigger = 0.1 * u.amp def test_hp6632b_init_output_trigger(): with expected_protocol( ik.hp.HP6632b, [ "INIT:NAME TRAN", ], [], ) as psu: psu.init_output_trigger() def test_hp6632b_abort_output_trigger(): with expected_protocol( ik.hp.HP6632b, [ "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"'], ) as psu: err_queue = psu.check_error_queue() assert err_queue == [psu.ErrorCodes.data_out_of_range], f"got {err_queue}" ================================================ FILE: tests/test_hp/test_hp6652a.py ================================================ #!/usr/bin/env python """ Unit tests for the HP 6652a single output power supply """ # IMPORTS ##################################################################### import pytest import instruments as ik from tests import expected_protocol # TESTS ####################################################################### def test_name(): with expected_protocol( 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: hp.reset() def test_display_text(): with expected_protocol( 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: assert hp.channel[0] == hp assert len(hp.channel) == 1 ================================================ FILE: tests/test_hp/test_hpe3631a.py ================================================ #!/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 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 ================================================ FILE: tests/test_keithley/__init__.py ================================================ ================================================ FILE: tests/test_keithley/test_keithley195.py ================================================ #!/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 tests import expected_protocol from instruments.units import ureg as u # TESTS ###################################################################### # pylint: disable=redefined-outer-name # PYTEST FIXTURES FOR INITIALIZATION # @pytest.fixture(scope="session") def init(): """Returns the initialization command that is sent to instrument.""" return "YX\nG1DX" @pytest.fixture(scope="session") 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() ================================================ FILE: tests/test_keithley/test_keithley2182.py ================================================ #!/usr/bin/env python """ Unit tests for the Keithley 2182 nano-voltmeter """ # IMPORTS ##################################################################### import pytest import instruments as ik from instruments.optional_dep_finder import numpy from tests import ( expected_protocol, iterable_eq, ) from instruments.units import ureg as u # TESTS ####################################################################### def test_channel(): inst = ik.keithley.Keithley2182.open_test() assert isinstance(inst.channel[0], inst.Channel) is True def test_channel_mode(): with expected_protocol( ik.keithley.Keithley2182, [ "SENS:FUNC?", ], [ "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?"], [ "1.234", "VOLT", ], ) as inst: channel = inst.channel[0] 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"], ) as inst: channel = inst.channel[0] assert channel.measure() == u.Quantity(1.234, u.degC) def test_channel_measure_unknown_temperature_units(): with pytest.raises(ValueError), expected_protocol( ik.keithley.Keithley2182, ["SENS:CHAN 1", "SENS:DATA:FRES?", "SENS:FUNC?", "UNIT:TEMP?"], ["1.234", "TEMP", "Z"], ) as inst: inst.channel[0].measure() def test_units(): with expected_protocol( ik.keithley.Keithley2182, [ "SENS:FUNC?", "UNIT:TEMP?", "SENS:FUNC?", "UNIT:TEMP?", "SENS:FUNC?", "UNIT:TEMP?", "SENS:FUNC?", ], ["TEMP", "C", "TEMP", "F", "TEMP", "K", "VOLT"], ) as inst: 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?"], [ "1.234,1,5.678", "VOLT", ], ) as inst: 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(): with expected_protocol( ik.keithley.Keithley2182, [ "SENS:FUNC?", "MEAS:VOLT?", "SENS:FUNC?", ], ["VOLT", "1.234", "VOLT"], ) as inst: assert inst.measure() == 1.234 * u.volt def test_measure_invalid_mode(): with pytest.raises(TypeError), expected_protocol( ik.keithley.Keithley2182, [], [] ) as inst: inst.measure("derp") def test_relative_get(): with expected_protocol( ik.keithley.Keithley2182, ["SENS:FUNC?", "SENS:VOLT:CHAN1:REF:STAT?"], ["VOLT", "ON"], ) as inst: assert inst.relative is True def test_relative_set_already_enabled(): with expected_protocol( ik.keithley.Keithley2182, [ "SENS:FUNC?", "SENS:FUNC?", "SENS:VOLT:CHAN1:REF:STAT?", "SENS:VOLT:CHAN1:REF:ACQ", ], [ "VOLT", "VOLT", "ON", ], ) as inst: inst.relative = True def test_relative_set_start_disabled(): with expected_protocol( ik.keithley.Keithley2182, [ "SENS:FUNC?", "SENS:FUNC?", "SENS:VOLT:CHAN1:REF:STAT?", "SENS:VOLT:CHAN1:REF:STAT ON", ], [ "VOLT", "VOLT", "OFF", ], ) as inst: inst.relative = True def test_relative_set_wrong_type(): 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 ================================================ FILE: tests/test_keithley/test_keithley485.py ================================================ #!/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 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}" ================================================ FILE: tests/test_keithley/test_keithley580.py ================================================ #!/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 tests import expected_protocol from instruments.units import ureg as u # TESTS ###################################################################### # pylint: disable=redefined-outer-name # PYTEST FIXTURES FOR INITIALIZATION # @pytest.fixture(scope="session") def init(): """Returns the initialization command that is sent to instrument.""" return "Y:X:" @pytest.fixture(scope="session") 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(scope="session") 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:.6f}", "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-3) 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 ================================================ FILE: tests/test_keithley/test_keithley6220.py ================================================ #!/usr/bin/env python """ Unit tests for the Keithley 6220 constant current supply """ # IMPORTS ##################################################################### import pytest from instruments.units import ureg as u import instruments as ik from tests import expected_protocol # TESTS ####################################################################### def test_channel(): inst = ik.keithley.Keithley6220.open_test() 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?", f"SOUR:CURR {0.05:e}"], [ "0.1", ], ) as inst: 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: inst.disable() ================================================ FILE: tests/test_keithley/test_keithley6514.py ================================================ #!/usr/bin/env python """ Unit tests for the Keithley 6514 electrometer """ # IMPORTS ##################################################################### import pytest import instruments as ik from tests import expected_protocol from instruments.units import ureg as u # TESTS ####################################################################### # pylint: disable=protected-access 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.charge) == inst.ValidRange.charge def test_valid_range_invalid(): with pytest.raises(ValueError): inst = ik.keithley.Keithley6514.open_test() inst._valid_range(inst.TriggerMode.immediate) def test_parse_measurement(): with expected_protocol( ik.keithley.Keithley6514, [ "FUNCTION?", ], ['"VOLT:DC"'], ) as inst: reading, timestamp, status = inst._parse_measurement("1.0,1234,5678") 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"'] ) as inst: assert inst.mode == inst.Mode.voltage inst.mode = inst.Mode.voltage def test_trigger_source(): with expected_protocol( ik.keithley.Keithley6514, ["TRIGGER:SOURCE?", "TRIGGER:SOURCE IMM"], ["TLINK"] ) as inst: assert inst.trigger_mode == inst.TriggerMode.tlink inst.trigger_mode = inst.TriggerMode.immediate def test_arm_source(): with expected_protocol( ik.keithley.Keithley6514, ["ARM:SOURCE?", "ARM:SOURCE IMM"], ["TIM"] ) as inst: assert inst.arm_source == inst.ArmSource.timer inst.arm_source = inst.ArmSource.immediate def test_zero_check(): with expected_protocol( ik.keithley.Keithley6514, ["SYST:ZCH?", "SYST:ZCH ON"], ["OFF"] ) as inst: assert inst.zero_check is False inst.zero_check = True def test_zero_correct(): with expected_protocol( ik.keithley.Keithley6514, ["SYST:ZCOR?", "SYST:ZCOR ON"], ["OFF"] ) as inst: assert inst.zero_correct is False inst.zero_correct = True def test_unit(): with expected_protocol( ik.keithley.Keithley6514, [ "FUNCTION?", ], ['"VOLT:DC"'], ) as inst: 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"'], ) as inst: assert inst.auto_range is False inst.auto_range = True def test_input_range(): with expected_protocol( ik.keithley.Keithley6514, [ "FUNCTION?", "VOLT:DC:RANGE:UPPER?", "FUNCTION?", f"VOLT:DC:RANGE:UPPER {20:e}", ], ['"VOLT:DC"', "10", '"VOLT:DC"'], ) as inst: assert inst.input_range == 10 * u.volt inst.input_range = 20 * u.volt def test_input_range_invalid(): with pytest.raises(ValueError), expected_protocol( ik.keithley.Keithley6514, ["FUNCTION?"], ['"VOLT:DC"'] ) as inst: inst.input_range = 10 * u.volt def test_auto_config(): with expected_protocol( ik.keithley.Keithley6514, [ "CONF:VOLT:DC", ], [], ) as inst: inst.auto_config(inst.Mode.voltage) def test_fetch(): with expected_protocol( ik.keithley.Keithley6514, [ "FETC?", "FUNCTION?", ], ["1.0,1234,5678", '"VOLT:DC"'], ) as inst: reading, timestamp = inst.fetch() assert reading == 1.0 * u.volt assert timestamp == 1234 def test_read(): with expected_protocol( ik.keithley.Keithley6514, [ "READ?", "FUNCTION?", ], ["1.0,1234,5678", '"VOLT:DC"'], ) as inst: reading, timestamp = inst.read_measurements() assert reading == 1.0 * u.volt assert timestamp == 1234 ================================================ FILE: tests/test_lakeshore/__init__.py ================================================ ================================================ FILE: tests/test_lakeshore/test_lakeshore336.py ================================================ #!/usr/bin/env python """ Module containing tests for the Lakeshore 336 """ # IMPORTS #################################################################### import pytest import instruments as ik from instruments.units import ureg as u from tests import expected_protocol # TESTS ###################################################################### # pylint: disable=protected-access # TEST SENSOR CLASS # def test_lakeshore336_sensor_init(): """ Test initialization of sensor class. """ with expected_protocol( ik.lakeshore.Lakeshore336, [], [], ) as cryo: sensor = cryo.sensor[0] assert sensor._parent is cryo assert sensor._idx == "A" @pytest.mark.parametrize("idx_ch", [(0, "A"), (1, "B"), (2, "C"), (3, "D")]) def test_lakeshore336_sensor_temperature(idx_ch): """ Receive a unitful temperature from a sensor. """ idx, ch = idx_ch with expected_protocol( ik.lakeshore.Lakeshore336, [f"KRDG?{ch}"], ["77"], ) as cryo: assert cryo.sensor[idx].temperature == u.Quantity(77, u.K) ================================================ FILE: tests/test_lakeshore/test_lakeshore340.py ================================================ #!/usr/bin/env python """ Module containing tests for the Lakeshore 340 """ # IMPORTS #################################################################### import instruments as ik from instruments.units import ureg as u from 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) ================================================ FILE: tests/test_lakeshore/test_lakeshore370.py ================================================ #!/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 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) ================================================ FILE: tests/test_lakeshore/test_lakeshore475.py ================================================ #!/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 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 {int} 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 {int} 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 {int} 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 {int} 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." ================================================ FILE: tests/test_mettler_toledo/__init__.py ================================================ ================================================ FILE: tests/test_mettler_toledo/test_mt_sics.py ================================================ #!/usr/bin/env python """ Tests for the Mettler Toledo Standard Interface Command Set (SICS). """ # IMPORTS ##################################################################### import pytest import instruments as ik from instruments.units import ureg as u from tests import expected_protocol # TESTS ####################################################################### def test_clear_tare(): """Clear the tare value.""" with expected_protocol( ik.mettler_toledo.MTSICS, ["TAC"], ["TAC A"], "\r\n" ) as inst: inst.clear_tare() def test_reset(): """Reset the balance.""" with expected_protocol( ik.mettler_toledo.MTSICS, ["@"], ['@ A "123456789"'], "\r\n" ) as inst: inst.reset() @pytest.mark.parametrize("mode", ik.mettler_toledo.MTSICS.WeightMode) def test_tare(mode): """Tare the balance.""" msg = "TI" if mode.value else "T" with expected_protocol( ik.mettler_toledo.MTSICS, [f"{msg}", "T", "TI"], [f"{msg} A 2.486 g", "T A 2.486 g", "TI A 2.486 g"], "\r\n", ) as inst: inst.weight_mode = mode inst.tare() inst.tare(immediately=False) inst.tare(immediately=True) @pytest.mark.parametrize("mode", ik.mettler_toledo.MTSICS.WeightMode) def test_zero(mode): """Zero the balance.""" msg = "ZI" if mode.value else "Z" with expected_protocol( ik.mettler_toledo.MTSICS, [f"{msg}", "Z", "ZI"], [f"{msg} A", "Z A", "ZI A"], "\r\n", ) as inst: inst.weight_mode = mode inst.zero() inst.zero(immediately=False) inst.zero(immediately=True) @pytest.mark.parametrize("err", ["I", "L", "+", "-"]) def test_command_error_checking(err): """Raise OSError if command encounters an error.""" with expected_protocol( ik.mettler_toledo.MTSICS, ["S"], [f"S {err}"], "\r\n" ) as inst: with pytest.raises(OSError): inst.weight @pytest.mark.parametrize("err", ["ES", "ET", "EL"]) def test_general_error_checking(err): """Raise OSError if general error encountered.""" with expected_protocol(ik.mettler_toledo.MTSICS, ["S"], [f"{err}"], "\r\n") as inst: with pytest.raises(OSError): inst.weight # PROPERTIES # def test_mt_sics(): """Get MT-SICS level and MT-SICS versions.""" with expected_protocol( ik.mettler_toledo.MTSICS, ["I1"], ["I1 A 01 2.00 2.00 2.00 1.0"], "\r\n" ) as inst: assert inst.mt_sics == ["01", "2.00", "2.00", "2.00", "1.0"] def test_mt_sics_commands(): """Get all available MT-SICS implemented commands.""" with expected_protocol( ik.mettler_toledo.MTSICS, ["I0"], ['0 B 0 "I0"\r\nI0 B 1 "D"'], "\r\n" ) as inst: assert inst.mt_sics_commands == [["0", "I0"], ["1", "D"]] def test_mt_sics_commands_timeout(mocker): """Ensure that timeout error is caught appropriately.""" inst_class = ik.mettler_toledo.MTSICS # mock reading raises timeout error os_error_mock = mocker.Mock() os_error_mock.side_effect = OSError mocker.patch.object(inst_class, "read", os_error_mock) with expected_protocol(inst_class, ["I0"], [], "\r\n") as inst: timeout = inst.timeout assert inst.mt_sics_commands == [] assert inst.timeout == timeout def test_name(): """Get / Set balance name.""" with expected_protocol( ik.mettler_toledo.MTSICS, ['I10 "My Balance"', "I10"], ['I10 A "Balance"', 'I10 A "My Balance"'], "\r\n", ) as inst: inst.name = "My Balance" assert inst.name == "My Balance" def test_name_too_long(): """Raise ValueError if name is too long.""" with expected_protocol(ik.mettler_toledo.MTSICS, [], [], "\r\n") as inst: with pytest.raises(ValueError): inst.name = "My Balance is too long" def test_serial_number(): """Get the serial number of the instrument.""" with expected_protocol( ik.mettler_toledo.MTSICS, ["I4"], ["I4 A 0123456789"], "\r\n" ) as inst: assert inst.serial_number == "0123456789" def test_tare_value(): """Set / get the tare value.""" with expected_protocol( ik.mettler_toledo.MTSICS, ["TA 2.486 g", "TA"], ["TA A 2.486 g", "TA A 2.486 g"], "\r\n", ) as inst: inst.tare_value = u.Quantity(2.486, u.gram) assert inst.tare_value == u.Quantity(2.486, u.gram) @pytest.mark.parametrize("mode", ik.mettler_toledo.MTSICS.WeightMode) def test_weight(mode): """Get the stable weight.""" msg = "SI" if mode.value else "S" with expected_protocol( ik.mettler_toledo.MTSICS, [f"{msg}"], [f"{msg} A 1.234 g"], "\r\n" ) as inst: inst.weight_mode = mode assert inst.weight == u.Quantity(1.234, u.gram) def test_weight_immediately_dynamic_mode(): """Raise UserWarning if balance is in dynamic mode.""" with expected_protocol( ik.mettler_toledo.MTSICS, ["SI"], ["S D 1.234 g"], "\r\n" ) as inst: inst.weight_mode = inst.WeightMode.immediately with pytest.warns(UserWarning): _ = inst.weight def test_weight_mode_type_error(): """Raise TypeError if weight mode is set with wrong type.""" with expected_protocol(ik.mettler_toledo.MTSICS, [], [], "\r\n") as inst: with pytest.raises(TypeError): inst.weight_mode = True ================================================ FILE: tests/test_minghe/test_minghe_mhs5200a.py ================================================ #!/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 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) ================================================ FILE: tests/test_named_struct.py ================================================ #!/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 ================================================ FILE: tests/test_newport/__init__.py ================================================ ================================================ FILE: tests/test_newport/test_agilis.py ================================================ #!/usr/bin/env python """ Module containing tests for the Agilis Controller """ # IMPORTS ##################################################################### import time import pytest import instruments as ik from 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." ) ================================================ FILE: tests/test_newport/test_errors.py ================================================ #!/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 ================================================ FILE: tests/test_newport/test_newport_pmc8742.py ================================================ #!/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 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") ================================================ FILE: tests/test_newport/test_newportesp301.py ================================================ #!/usr/bin/env python """ Unit tests for the Newport ESP 301 axis controller """ # IMPORTS ##################################################################### import time from unittest import mock from hypothesis import given, strategies as st import pytest import instruments as ik from instruments.units import ureg as u from tests import expected_protocol # TESTS ####################################################################### # 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(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: with mock.patch.object(inst, "_newport_cmd") as mock_cmd: with inst.define_program(prg_id): pass calls = ( mock.call("XX", target=prg_id), mock.call("EP", target=prg_id), mock.call("QP"), ) mock_cmd.assert_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, [ f"{ax+1}SN?", "TB?", # error check query ], ["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(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: with mock.patch.object(inst, "_newport_cmd") as mock_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(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] with mock.patch.object(axis, "_newport_cmd"), mock.patch.object( axis, "read_setup", return_value=True ), 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(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] with mock.patch.object(axis, "_newport_cmd"), mock.patch.object( axis, "read_setup", return_value=True ), 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] 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) ================================================ FILE: tests/test_ondax/test_lm.py ================================================ #!/usr/bin/env python """ Unit tests for the Ondax Laser Module """ # IMPORTS ##################################################################### import pytest from instruments import ondax from 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 * u.mA def test_acc_enable(): 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: lm.acc.enabled = False assert not lm.acc.enabled def test_acc_enable_not_boolean(): 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: lm.acc.on() def test_acc_off(): 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 * u.mW def test_apc_enable(): 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: lm.apc.enabled = False assert not lm.apc.enabled def test_apc_enable_not_boolean(): 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: lm.apc.start() def test_apc_stop(): 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"], ["10", "OK"], sep="\r" ) as lm: 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"], ["10", "OK"], sep="\r" ) as lm: 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: lm.modulation.enabled = True assert lm.modulation.enabled def test_modulation_disabled(): with expected_protocol(ondax.LM, ["ctm"], ["OK"], sep="\r") as lm: lm.modulation.enabled = False assert not lm.modulation.enabled def test_modulation_enable_not_boolean(): 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 * u.mA def test_tec_target(): 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: lm.tec.enabled = True assert lm.tec.enabled def test_tec_disable(): with expected_protocol(ondax.LM, ["tecoff"], ["OK"], sep="\r") as lm: lm.tec.enabled = False assert not lm.tec.enabled def test_tec_enable_not_boolean(): 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: assert lm.firmware == "3.27" def test_current(): with expected_protocol( ondax.LM, ["rli?", "slc:100"], ["120", "OK"], sep="\r" ) as lm: assert lm.current == 120 * u.mA lm.current = 100 * u.mA def test_maximum_current(): with expected_protocol( ondax.LM, ["rlcm?", "smlc:100"], ["120", "OK"], sep="\r" ) as lm: assert lm.maximum_current == 120 * u.mA lm.maximum_current = 100 * u.mA def test_power(): with expected_protocol( ondax.LM, ["rlp?", "slp:100"], ["120", "OK"], sep="\r" ) as lm: 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: assert lm.serial_number == "B099999" def test_status(): 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"], ["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: lm.enabled = True assert lm.enabled def test_disable(): with expected_protocol(ondax.LM, ["loff"], ["OK"], sep="\r") as lm: lm.enabled = False assert not lm.enabled def test_enable_not_boolean(): 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: lm.save() def test_reset(): with expected_protocol(ondax.LM, ["reset"], ["OK"], sep="\r") as lm: lm.reset() ================================================ FILE: tests/test_oxford/__init__.py ================================================ ================================================ FILE: tests/test_oxford/test_oxforditc503.py ================================================ #!/usr/bin/env python """ Unit tests for the Oxford ITC 503 temperature controller """ # IMPORTS ##################################################################### import instruments as ik from 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: 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" ) as inst: sensor = inst.sensor[0] assert sensor.temperature == u.Quantity(123, u.kelvin) ================================================ FILE: tests/test_package.py ================================================ """ 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") ================================================ FILE: tests/test_pfeiffer/__init__.py ================================================ ================================================ FILE: tests/test_pfeiffer/test_tpg36x.py ================================================ #!/usr/bin/env python """ Module containing tests for the TPG 36x gauge controller """ # IMPORTS #################################################################### from ipaddress import ip_address import pytest import instruments as ik from instruments.units import ureg as u from tests import expected_protocol, unit_eq # TESTS ###################################################################### # pylint: disable=no-member,protected-access SEP = "\r\n" ENQ = chr(5) ACK = chr(6) NAK = chr(21) # CHANNELS # def test_tpg36x_channel_init(): """Ensure an error is raised when not coming from correct class.""" with pytest.raises(TypeError): ik.pfeiffer.TPG36x.Channel(42, 0) def test_tpg36x_channel_pressure(): """Get the pressure from the device.""" with expected_protocol( ik.pfeiffer.TPG36x, ["PR1", SEP, ENQ, "UNI", SEP, ENQ], [ACK, SEP, "0,2.0000E-2", SEP, ACK, SEP, "0", SEP], sep="", ) as tpg: ch = tpg.channel[0] assert ch.pressure == 0.02 * u.mbar @pytest.mark.parametrize( "status", [ [1, "Underrange"], [2, "Overrange"], [3, "Sensor error"], [4, "Sensor off"], [5, "No sensor"], [6, "Identification error"], [42, "Unknown error"], ], ) def test_tpg36x_channel_pressure_error(status): """Raise correct error if statos is not zero.""" err_code, err_meaning = status with expected_protocol( ik.pfeiffer.TPG36x, ["PR1", SEP, ENQ], [ACK, SEP, f"{err_code},2.0000E-2", SEP], sep="", ) as tpg: ch = tpg.channel[0] with pytest.raises(OSError) as err: ch.pressure assert err.value.args[0].contains(err_meaning) def test_tpg36x_channel_status(): """Set/get the status of a channel.""" with expected_protocol( ik.pfeiffer.TPG36x, ["SEN", SEP, ENQ, "SEN,0,2", SEP, "SEN", SEP, ENQ], [ACK, SEP, "0,1", SEP, ACK, SEP, ACK, SEP, "0,2", SEP], sep="", ) as tpg: ch0 = tpg.channel[0] assert ch0.status == ch0.SensorStatus.CANNOT_TURN_ON_OFF ch1 = tpg.channel[1] ch1.status = ch1.SensorStatus.ON assert ch1.status == ch1.SensorStatus.ON def test_tpg36x_channel_status_error(): """Raise ValueErrors if bad statuses are given.""" with expected_protocol( ik.pfeiffer.TPG36x, [], [], sep="", ) as tpg: ch = tpg.channel[0] with pytest.raises(ValueError): ch.status = 42 with pytest.raises(ValueError): ch.status = ch.SensorStatus.CANNOT_TURN_ON_OFF # TPG36x #1 @pytest.mark.parametrize( "addrs", [ ["192.168.1.10", "255.255.255.0", "192.168.1.1"], [ ip_address("192.168.1.10"), ip_address("255.255.255.0"), ip_address("192.168.1.1"), ], ], ) def test_tpg36x_ethernet_configuration_static(addrs): """Set/get the ethernet configuration (static).""" ip, subnet, gateway = addrs with expected_protocol( ik.pfeiffer.TPG36x, [f"ETH,0,{ip},{subnet},{gateway}", SEP, "ETH", SEP, ENQ], [ACK, SEP, ACK, SEP, f"0,{ip},{subnet},{gateway}", SEP], sep="", ) as tpg: tpg.ethernet_configuration = [ tpg.EthernetMode.STATIC, "192.168.1.10", "255.255.255.0", "192.168.1.1", ] ret_val = tpg.ethernet_configuration assert isinstance(ret_val, list) mode, ip_rec, subnet_rec, gateway_rec = ret_val assert mode == tpg.EthernetMode.STATIC assert str(ip) == ip_rec assert str(subnet) == subnet_rec assert str(gateway) == gateway_rec def test_tpg36x_ethernet_configuration_dhcp(): """Set/get the ethernet configuration (dhcp).""" with expected_protocol( ik.pfeiffer.TPG36x, ["ETH,1", SEP], [ACK, SEP], sep="", ) as tpg: tpg.ethernet_configuration = tpg.EthernetMode.DHCP def test_tpg36x_ethernet_configuration_errors(): """Raise appropriate errors for bad settings.""" good_addr = "192.168.1.1" with expected_protocol( ik.pfeiffer.TPG36x, [], [], sep="", ) as tpg: with pytest.raises(ValueError): # invalid list tpg.ethernet_configuration = [tpg.EthernetMode.STATIC, 42] with pytest.raises(ValueError): # first value not an EthernetMode tpg.ethernet_configuration = [42, good_addr, good_addr, good_addr] def test_tpg36x_language(): """Set/get the language of the TPG36x.""" with expected_protocol( ik.pfeiffer.TPG36x, ["LNG,0", SEP, "LNG", SEP, ENQ], [ACK, SEP, ACK, SEP, "0", SEP], sep="", ) as tpg: tpg.language = tpg.Language.ENGLISH assert tpg.language == tpg.Language.ENGLISH def test_tpg36x_mac_address(): """Get the MAC address of the TPG36x.""" with expected_protocol( ik.pfeiffer.TPG36x, ["MAC", SEP, ENQ], [ACK, SEP, "00:00:00:00:00:00", SEP], sep="", ) as tpg: assert tpg.mac_address == "00:00:00:00:00:00" def test_tpg36x_mac_address_name(): """Get the name from the TPG36x.""" with expected_protocol( ik.pfeiffer.TPG36x, ["AYT", SEP, ENQ], [ACK, SEP, "TPG 362,PTG28290,44990000,010100,010100", SEP], sep="", ) as tpg: assert tpg.name == "TPG 362" def test_tpg36x_number_channels(): """Set/get the number of channels.""" with expected_protocol( ik.pfeiffer.TPG36x, [], [], sep="", ) as tpg: tpg.number_channels = 2 assert tpg.number_channels == 2 with expected_protocol( ik.pfeiffer.TPG36x, [], [], sep="", ) as tpg: tpg.number_channels = 1 assert tpg.number_channels == 1 @pytest.mark.parametrize("bad_num", [3, 0]) def test_tpg36x_number_channels_error(bad_num): """Raise ValueErrors if bad number of channels are given.""" with expected_protocol( ik.pfeiffer.TPG36x, [], [], sep="", ) as tpg: with pytest.raises(ValueError): tpg.number_channels = bad_num def test_tpg36x_pressure(): """Get the pressure from a one-channel device.""" with expected_protocol( ik.pfeiffer.TPG36x, ["PR1", SEP, ENQ, "UNI", SEP, ENQ], [ACK, SEP, "0,2.0000E-2", SEP, ACK, SEP, "0", SEP], sep="", ) as tpg: assert tpg.pressure == 0.02 * u.mbar @pytest.mark.parametrize("ret_val", [0, 1, 2, 3, 4, 5]) def test_tpg36x_unit(ret_val): """Get the unit from the device.""" with expected_protocol( ik.pfeiffer.TPG36x, ["UNI", SEP, ENQ], [ACK, SEP, f"{int(ret_val)}", SEP], sep="", ) as tpg: assert tpg.unit == tpg.Unit(ret_val) def test_tpg36x_unit_string(): """Set a unit from a string.""" with expected_protocol( ik.pfeiffer.TPG36x, ["UNI,0", SEP, "UNI", SEP, ENQ], [ACK, SEP, ACK, SEP, "0", SEP], sep="", ) as tpg: tpg.unit = "mbar" assert tpg.unit == tpg.Unit.MBAR ================================================ FILE: tests/test_phasematrix/__init__.py ================================================ ================================================ FILE: tests/test_phasematrix/test_phasematrix_fsw0020.py ================================================ #!/usr/bin/env python """ Unit tests for the Phasematrix FSW0020 """ # IMPORTS ##################################################################### import pytest from instruments.units import ureg as u import instruments as ik from tests import expected_protocol # TESTS ####################################################################### def test_reset(): with expected_protocol(ik.phasematrix.PhaseMatrixFSW0020, ["0E."], []) as inst: inst.reset() def test_frequency(): with expected_protocol( ik.phasematrix.PhaseMatrixFSW0020, ["04.", f"0C{int((10 * u.GHz).to(u.mHz).magnitude):012X}."], ["00E8D4A51000"], ) as inst: assert inst.frequency == 1.0000000000000002 * u.GHz inst.frequency = 10 * u.GHz def test_power(): with expected_protocol( ik.phasematrix.PhaseMatrixFSW0020, ["0D.", f"03{int(u.Quantity(10, u.dBm).to(u.cBm).magnitude):04X}."], ["-064"], ) as inst: 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, [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, [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, [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, [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, [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 ================================================ FILE: tests/test_picowatt/__init__.py ================================================ ================================================ FILE: tests/test_picowatt/test_picowatt_avs47.py ================================================ #!/usr/bin/env python """ Unit tests for the Picowatt AVS47 """ # IMPORTS ##################################################################### from instruments.units import ureg as u import instruments as ik from tests import expected_protocol # TESTS ####################################################################### def test_sensor_is_sensor_class(): inst = ik.picowatt.PicowattAVS47.open_test() assert isinstance(inst.sensor[0], inst.Sensor) is True def test_init(): 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"] ) as inst: 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"], ) as inst: 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"], ) as inst: assert inst.remote is False assert inst.remote is True inst.remote = True inst.remote = False def test_input_source(): with expected_protocol( ik.picowatt.PicowattAVS47, ["HDR 0", "INP?", "INP 1"], [ "0", ], ) as inst: assert inst.input_source == inst.InputSource.ground inst.input_source = inst.InputSource.actual def test_mux_channel(): with expected_protocol( ik.picowatt.PicowattAVS47, ["HDR 0", "MUX?", "MUX 1"], [ "3", ], ) as inst: assert inst.mux_channel == 3 inst.mux_channel = 1 def test_excitation(): with expected_protocol( ik.picowatt.PicowattAVS47, ["HDR 0", "EXC?", "EXC 1"], [ "3", ], ) as inst: assert inst.excitation == 3 inst.excitation = 1 def test_display(): with expected_protocol( ik.picowatt.PicowattAVS47, ["HDR 0", "DIS?", "DIS 1"], [ "3", ], ) as inst: assert inst.display == 3 inst.display = 1 ================================================ FILE: tests/test_property_factories/__init__.py ================================================ #!/usr/bin/env python """ Module containing common code for testing the property factories """ # IMPORTS #################################################################### from io import StringIO # CLASSES #################################################################### # pylint: disable=missing-docstring class MockInstrument: """ Mock class that admits sendcmd/query but little else such that property factories can be tested by deriving from the class. """ def __init__(self, responses=None): self._buf = StringIO() self._responses = responses if responses is not None else {} @property def value(self): return self._buf.getvalue() def sendcmd(self, cmd): self._buf.write(f"{cmd}\n") def query(self, cmd): self.sendcmd(cmd) return self._responses[cmd.strip()] ================================================ FILE: tests/test_property_factories/test_bool_property.py ================================================ #!/usr/bin/env python """ Module containing tests for the bool property factories """ # IMPORTS #################################################################### import pytest from instruments.util_fns import bool_property from . import MockInstrument # TEST CASES ################################################################# # pylint: disable=missing-docstring def test_bool_property_basics(): class BoolMock(MockInstrument): mock1 = bool_property("MOCK1") mock2 = bool_property("MOCK2", inst_true="YES", inst_false="NO") mock_inst = BoolMock({"MOCK1?": "OFF", "MOCK2?": "YES"}) assert mock_inst.mock1 is False assert mock_inst.mock2 is True mock_inst.mock1 = True mock_inst.mock2 = False assert mock_inst.value == "MOCK1?\nMOCK2?\nMOCK1 ON\nMOCK2 NO\n" def test_bool_property_set_fmt(): class BoolMock(MockInstrument): mock1 = bool_property("MOCK1", set_fmt="{}={}") mock_instrument = BoolMock({"MOCK1?": "OFF"}) mock_instrument.mock1 = True assert mock_instrument.value == "MOCK1=ON\n" def test_bool_property_readonly_writing_fails(): with pytest.raises(AttributeError): class BoolMock(MockInstrument): mock1 = bool_property("MOCK1", readonly=True) mock_instrument = BoolMock({"MOCK1?": "OFF"}) mock_instrument.mock1 = True def test_bool_property_readonly_reading_passes(): class BoolMock(MockInstrument): mock1 = bool_property("MOCK1", readonly=True) mock_instrument = BoolMock({"MOCK1?": "OFF"}) assert mock_instrument.mock1 is False def test_bool_property_writeonly_reading_fails(): with pytest.raises(AttributeError): class BoolMock(MockInstrument): mock1 = bool_property("MOCK1", writeonly=True) mock_instrument = BoolMock({"MOCK1?": "OFF"}) _ = mock_instrument.mock1 def test_bool_property_writeonly_writing_passes(): class BoolMock(MockInstrument): mock1 = bool_property("MOCK1", writeonly=True) 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" ================================================ FILE: tests/test_property_factories/test_bounded_unitful_property.py ================================================ #!/usr/bin/env python """ Module containing tests for the bounded unitful property factories """ # IMPORTS #################################################################### 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 ################################################################# # pylint: disable=missing-docstring def test_bounded_unitful_property_basics(): 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"} ) 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 * u.hertz def test_bounded_unitful_property_set_outside_max(): with pytest.raises(ValueError): 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 = 10000 * u.hertz # Should raise ValueError def test_bounded_unitful_property_set_outside_min(): with pytest.raises(ValueError): 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 * 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=u.hertz, min_fmt_str="{} MIN?" ) mock_inst = BoundedUnitfulMock({"MOCK MIN?": "10"}) 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=u.hertz, max_fmt_str="{} MAX?" ) mock_inst = BoundedUnitfulMock({"MOCK MAX?": "9999"}) 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=u.hertz, valid_range=(10, 9999) ) mock_inst = BoundedUnitfulMock() 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=u.hertz, valid_range=(10 * u.kilohertz, 9999 * u.kilohertz) ) mock_inst = BoundedUnitfulMock() 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(command="MOCK", units=u.Hz, derp="foobar") mock_unitful_property.assert_called_with( "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(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=u.hertz, valid_range=(None, None) ) mock_inst = BoundedUnitfulMock() assert mock_inst.property_min is None assert mock_inst.property_max is None ================================================ FILE: tests/test_property_factories/test_enum_property.py ================================================ #!/usr/bin/env python """ Module containing tests for the enum property factories """ # IMPORTS #################################################################### from enum import Enum, IntEnum import pytest from instruments.util_fns import enum_property from . import MockInstrument # TEST CASES ################################################################# # pylint: disable=missing-docstring def test_enum_property(): class SillyEnum(Enum): a = "aa" b = "bb" class EnumMock(MockInstrument): a = enum_property("MOCK:A", SillyEnum) b = enum_property("MOCK:B", SillyEnum) mock_inst = EnumMock({"MOCK:A?": "aa", "MOCK:B?": "bb"}) 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" assert mock_inst.value == "MOCK:A?\nMOCK:B?\nMOCK:A bb\nMOCK:B aa\nMOCK:B bb\n" def test_enum_property_invalid(): with pytest.raises(ValueError): 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.a = "c" def test_enum_property_set_fmt(): class SillyEnum(Enum): a = "aa" class EnumMock(MockInstrument): a = enum_property("MOCK:A", SillyEnum, set_fmt="{}={}") mock_instrument = EnumMock() mock_instrument.a = "aa" assert mock_instrument.value == "MOCK:A=aa\n" def test_enum_property_input_decoration(): class SillyEnum(Enum): a = "aa" class EnumMock(MockInstrument): @staticmethod def _input_decorator(_): return "aa" a = enum_property("MOCK:A", SillyEnum, input_decoration=_input_decorator) mock_instrument = EnumMock({"MOCK:A?": "garbage"}) assert mock_instrument.a == SillyEnum.a def test_enum_property_input_decoration_not_a_function(): class SillyEnum(IntEnum): a = 1 class EnumMock(MockInstrument): a = enum_property("MOCK:A", SillyEnum, input_decoration=int) mock_instrument = EnumMock({"MOCK:A?": "1"}) assert mock_instrument.a == SillyEnum.a def test_enum_property_output_decoration(): class SillyEnum(Enum): a = "aa" class EnumMock(MockInstrument): @staticmethod def _output_decorator(_): return "foobar" a = enum_property("MOCK:A", SillyEnum, output_decoration=_output_decorator) mock_instrument = EnumMock() mock_instrument.a = SillyEnum.a assert mock_instrument.value == "MOCK:A foobar\n" def test_enum_property_output_decoration_not_a_function(): class SillyEnum(Enum): a = ".23" class EnumMock(MockInstrument): a = enum_property("MOCK:A", SillyEnum, output_decoration=float) mock_instrument = EnumMock() mock_instrument.a = SillyEnum.a assert mock_instrument.value == "MOCK:A 0.23\n" def test_enum_property_writeonly_reading_fails(): with pytest.raises(AttributeError): class SillyEnum(Enum): a = "aa" class EnumMock(MockInstrument): a = enum_property("MOCK:A", SillyEnum, writeonly=True) mock_instrument = EnumMock() _ = mock_instrument.a def test_enum_property_writeonly_writing_passes(): class SillyEnum(Enum): a = "aa" class EnumMock(MockInstrument): a = enum_property("MOCK:A", SillyEnum, writeonly=True) mock_instrument = EnumMock() mock_instrument.a = SillyEnum.a assert mock_instrument.value == "MOCK:A aa\n" 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" class EnumMock(MockInstrument): a = enum_property("MOCK:A", SillyEnum, readonly=True) mock_instrument = EnumMock({"MOCK:A?": "aa"}) assert mock_instrument.a == SillyEnum.a assert mock_instrument.value == "MOCK:A?\n" def test_enum_property_set_cmd(): class SillyEnum(Enum): a = "aa" class EnumMock(MockInstrument): a = enum_property("MOCK:A", SillyEnum, set_cmd="FOOBAR:A") mock_inst = EnumMock({"MOCK:A?": "aa"}) assert mock_inst.a == SillyEnum.a mock_inst.a = SillyEnum.a assert mock_inst.value == "MOCK:A?\nFOOBAR:A aa\n" ================================================ FILE: tests/test_property_factories/test_int_property.py ================================================ #!/usr/bin/env python """ Module containing tests for the int property factories """ # IMPORTS #################################################################### import pytest from instruments.util_fns import int_property from . import MockInstrument # TEST CASES ################################################################# # pylint: disable=missing-docstring def test_int_property_outside_valid_set(): with pytest.raises(ValueError): 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={1, 2}) mock_inst = IntMock({"MOCK?": "1"}) assert mock_inst.int_property == 1 mock_inst.int_property = 2 assert mock_inst.value == "MOCK?\nMOCK 2\n" def test_int_property_no_set(): class IntMock(MockInstrument): int_property = int_property("MOCK") mock_inst = IntMock() mock_inst.int_property = 1 assert mock_inst.value == "MOCK 1\n" def test_int_property_writeonly_reading_fails(): with pytest.raises(AttributeError): class IntMock(MockInstrument): int_property = int_property("MOCK", writeonly=True) mock_inst = IntMock() _ = mock_inst.int_property def test_int_property_writeonly_writing_passes(): class IntMock(MockInstrument): int_property = int_property("MOCK", writeonly=True) mock_inst = IntMock() mock_inst.int_property = 1 assert mock_inst.value == f"MOCK {1:d}\n" def test_int_property_readonly_writing_fails(): with pytest.raises(AttributeError): class IntMock(MockInstrument): int_property = int_property("MOCK", readonly=True) 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) mock_inst = IntMock({"MOCK?": "1"}) assert mock_inst.int_property == 1 def test_int_property_format_code(): class IntMock(MockInstrument): int_property = int_property("MOCK", format_code="{:e}") mock_inst = IntMock() mock_inst.int_property = 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" ================================================ FILE: tests/test_property_factories/test_rproperty.py ================================================ #!/usr/bin/env python """ Module containing tests for the property factories """ # IMPORTS #################################################################### import pytest from instruments.util_fns import rproperty from . import MockInstrument # TEST CASES ################################################################# # pylint: disable=missing-docstring def test_rproperty_basic(): class Mock(MockInstrument): def __init__(self): super().__init__() self._value = 0 def mockget(self): return self._value def mockset(self, newval): self._value = newval mockproperty = rproperty(fget=mockget, fset=mockset) mock_inst = Mock() mock_inst.mockproperty = 1 assert mock_inst.mockproperty == 1 def test_rproperty_readonly_writing_fails(): with pytest.raises(AttributeError): 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) mock_inst = Mock() mock_inst.mockproperty = 1 def test_rproperty_readonly_reading_passes(): class Mock(MockInstrument): def __init__(self): super().__init__() self._value = 0 def mockget(self): return self._value mockproperty = rproperty(fget=mockget, fset=None, readonly=True) mock_inst = Mock() assert mock_inst.mockproperty == 0 def test_rproperty_writeonly_reading_fails(): with pytest.raises(AttributeError): 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) mock_inst = Mock() assert mock_inst.mockproperty == 0 def test_rproperty_writeonly_writing_passes(): class Mock(MockInstrument): def __init__(self): 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 def test_rproperty_readonly_and_writeonly(): with pytest.raises(ValueError): _ = rproperty(readonly=True, writeonly=True) ================================================ FILE: tests/test_property_factories/test_string_property.py ================================================ #!/usr/bin/env python """ Module containing tests for the string property factories """ # IMPORTS #################################################################### from instruments.util_fns import string_property from . import MockInstrument # TEST CASES ################################################################# # pylint: disable=missing-docstring def test_string_property_basics(): class StringMock(MockInstrument): mock_property = string_property("MOCK") 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_different_bookmark_symbol(): class StringMock(MockInstrument): 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_no_bookmark_symbol(): class StringMock(MockInstrument): 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?": '"derp"'}) assert mock_inst.mock_property == "derp" mock_inst.mock_property = "qwerty" assert mock_inst.value == 'MOCK?\nFOOBAR "qwerty"\n' ================================================ FILE: tests/test_property_factories/test_unitful_property.py ================================================ #!/usr/bin/env python """ Module containing tests for the unitful property factories """ # IMPORTS #################################################################### import pytest import pint from instruments.util_fns import unitful_property from instruments.units import ureg as u from . import MockInstrument # TEST CASES ################################################################# # pylint: disable=missing-docstring,no-self-use def test_unitful_property_basics(): class UnitfulMock(MockInstrument): unitful_property = unitful_property("MOCK", units=u.hertz) mock_inst = UnitfulMock({"MOCK?": "1000"}) assert mock_inst.unitful_property == 1000 * u.hertz 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", u.hertz, format_code="{:f}") mock_inst = UnitfulMock() 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", u.hertz) mock_inst = UnitfulMock() 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", u.hertz) mock_inst = UnitfulMock() mock_inst.unitful_property = 1000 assert mock_inst.value == f"MOCK {1000:e}\n" def test_unitful_property_wrong_units(): with pytest.raises(pint.errors.DimensionalityError): class UnitfulMock(MockInstrument): unitful_property = unitful_property("MOCK", u.hertz) mock_inst = UnitfulMock() mock_inst.unitful_property = 1 * u.volt def test_unitful_property_writeonly_reading_fails(): with pytest.raises(AttributeError): class UnitfulMock(MockInstrument): unitful_property = unitful_property("MOCK", u.hertz, writeonly=True) mock_inst = UnitfulMock() _ = mock_inst.unitful_property def test_unitful_property_writeonly_writing_passes(): class UnitfulMock(MockInstrument): unitful_property = unitful_property("MOCK", u.hertz, writeonly=True) mock_inst = UnitfulMock() mock_inst.unitful_property = 1 * u.hertz assert mock_inst.value == f"MOCK {1:e}\n" def test_unitful_property_readonly_writing_fails(): with pytest.raises(AttributeError): class UnitfulMock(MockInstrument): unitful_property = unitful_property("MOCK", u.hertz, readonly=True) mock_inst = UnitfulMock({"MOCK?": "1"}) mock_inst.unitful_property = 1 * u.hertz def test_unitful_property_readonly_reading_passes(): class UnitfulMock(MockInstrument): unitful_property = unitful_property("MOCK", u.hertz, readonly=True) mock_inst = UnitfulMock({"MOCK?": "1"}) assert mock_inst.unitful_property == 1 * u.hertz def test_unitful_property_valid_range(): class UnitfulMock(MockInstrument): unitful_property = unitful_property("MOCK", u.hertz, valid_range=(0, 10)) mock_inst = UnitfulMock() mock_inst.unitful_property = 0 mock_inst.unitful_property = 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 * u.Hz def max_value(self): return 10 * u.Hz unitful_property = unitful_property( "MOCK", u.hertz, valid_range=(min_value, max_value) ) mock_inst = UnitfulMock() mock_inst.unitful_property = 0 mock_inst.unitful_property = 10 assert mock_inst.value == f"MOCK {0:e}\nMOCK {10:e}\n" def test_unitful_property_minimum_value(): with pytest.raises(ValueError): class UnitfulMock(MockInstrument): unitful_property = unitful_property("MOCK", u.hertz, valid_range=(0, 10)) mock_inst = UnitfulMock() mock_inst.unitful_property = -1 def test_unitful_property_maximum_value(): with pytest.raises(ValueError): class UnitfulMock(MockInstrument): unitful_property = unitful_property("MOCK", u.hertz, valid_range=(0, 10)) mock_inst = UnitfulMock() 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", u.hertz, input_decoration=_input_decorator) 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", u.hertz, input_decoration=float) mock_instrument = UnitfulMock({"MOCK:A?": ".123"}) 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", u.hertz, output_decoration=_output_decorator) mock_instrument = UnitfulMock() mock_instrument.a = 345 * u.hertz 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", u.hertz, output_decoration=bool) mock_instrument = UnitfulMock() mock_instrument.a = 1 * u.hertz assert mock_instrument.value == "MOCK:A True\n" def test_unitful_property_split_str(): class UnitfulMock(MockInstrument): 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 == 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" ================================================ FILE: tests/test_property_factories/test_unitless_property.py ================================================ #!/usr/bin/env python """ Module containing tests for the unitless property factory """ # IMPORTS #################################################################### import pytest from instruments.units import ureg as u from instruments.util_fns import unitless_property from . import MockInstrument # TEST CASES ################################################################# # pylint: disable=missing-docstring def test_unitless_property_basics(): class UnitlessMock(MockInstrument): mock_property = unitless_property("MOCK") mock_inst = UnitlessMock({"MOCK?": "1"}) assert mock_inst.mock_property == 1 mock_inst.mock_property = 1 assert mock_inst.value == f"MOCK?\nMOCK {1:e}\n" def test_unitless_property_units(): with pytest.raises(ValueError): class UnitlessMock(MockInstrument): mock_property = unitless_property("MOCK") mock_inst = UnitlessMock({"MOCK?": "1"}) 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_inst = UnitlessMock() mock_inst.mock_property = 1 assert mock_inst.value == f"MOCK {1:f}\n" def test_unitless_property_writeonly_reading_fails(): with pytest.raises(AttributeError): class UnitlessMock(MockInstrument): mock_property = unitless_property("MOCK", writeonly=True) 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_inst = UnitlessMock() mock_inst.mock_property = 1 assert mock_inst.value == f"MOCK {1:e}\n" def test_unitless_property_readonly_writing_fails(): with pytest.raises(AttributeError): class UnitlessMock(MockInstrument): mock_property = unitless_property("MOCK", readonly=True) 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_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 assert mock_inst.value == f"MOCK?\nFOOBAR {1:e}\n" ================================================ FILE: tests/test_qubitekk/__init__.py ================================================ ================================================ FILE: tests/test_qubitekk/test_qubitekk_cc1.py ================================================ #!/usr/bin/env python """ Module containing tests for the Qubitekk CC1 """ # IMPORTS #################################################################### from io import BytesIO import pytest from instruments.units import ureg as u import instruments as ik from tests import expected_protocol, unit_eq # 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", ) 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"], [ "", "Firmware v2.010", "2", ], sep="\n", ) as cc: unit_eq(cc.window, u.Quantity(2, "ns")) cc.window = 7 def test_cc1_window_error(): with pytest.raises(ValueError), expected_protocol( ik.qubitekk.CC1, [":ACKN OF", "FIRM?", ":WIND 10"], ["", "Firmware v2.010"], sep="\n", ) as cc: cc.window = 10 def test_cc1_delay(): with expected_protocol( ik.qubitekk.CC1, [":ACKN OF", "FIRM?", "DELA?", ":DELA 2"], ["", "Firmware v2.010", "8", ""], sep="\n", ) as cc: unit_eq(cc.delay, u.Quantity(8, "ns")) cc.delay = 2 def test_cc1_delay_error1(): with pytest.raises(ValueError), expected_protocol( ik.qubitekk.CC1, [":ACKN OF", "FIRM?", ":DELA -1"], ["", "Firmware v2.010"], sep="\n", ) as cc: cc.delay = -1 def test_cc1_delay_error2(): with pytest.raises(ValueError), expected_protocol( ik.qubitekk.CC1, [":ACKN OF", "FIRM?", ":DELA 1"], ["", "Firmware v2.010"], sep="\n", ) as cc: cc.delay = 1 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", ) as cc: 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", ) as cc: unit_eq(cc.dwell_time, u.Quantity(8, "s")) cc.dwell_time = 2 def test_cc1_dwell_time_error(): with pytest.raises(ValueError), expected_protocol( ik.qubitekk.CC1, [":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" ) as cc: assert cc.firmware == (2, 10, 0) def test_cc1_firmware_2(): with expected_protocol( ik.qubitekk.CC1, [":ACKN OF", "FIRM?"], ["Unknown Command", "Firmware v2"], sep="\n", ) as cc: assert cc.firmware == (2, 0, 0) def test_cc1_firmware_3(): with expected_protocol( ik.qubitekk.CC1, [":ACKN OF", "FIRM?"], ["Unknown Command", "Firmware v2.010.1"], sep="\n", ) as cc: assert cc.firmware == (2, 10, 1) def test_cc1_firmware_repeat_query(): with expected_protocol( ik.qubitekk.CC1, [":ACKN OF", "FIRM?", "FIRM?"], ["Unknown Command", "Unknown", "Firmware v2.010"], sep="\n", ) as cc: assert cc.firmware == (2, 10, 0) def test_cc1_gate_new_firmware(): with expected_protocol( ik.qubitekk.CC1, [":ACKN OF", "FIRM?", "GATE?", ":GATE:ON", ":GATE:OFF"], ["", "Firmware v2.010", "ON"], ) as cc: assert cc.gate is True cc.gate = True cc.gate = False 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", ) as cc: assert cc.gate is True cc.gate = True cc.gate = False def test_cc1_gate_error(): with pytest.raises(TypeError), expected_protocol( ik.qubitekk.CC1, [":ACKN OF", "FIRM?", ":GATE blo"], ["", "Firmware v2.010"], sep="\n", ) as cc: cc.gate = "blo" 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", ) as cc: assert cc.subtract is True cc.subtract = True cc.subtract = False def test_cc1_subtract_error(): with pytest.raises(TypeError), expected_protocol( ik.qubitekk.CC1, [":ACKN OF", "FIRM?", ":SUBT blo"], ["", "Firmware v2.010"], sep="\n", ) as cc: cc.subtract = "blo" 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", ) 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(): with expected_protocol( ik.qubitekk.CC1, [":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 def test_cc1_trigger_mode_error(): with pytest.raises(ValueError), expected_protocol( ik.qubitekk.CC1, [":ACKN OF", "FIRM?"], ["", "Firmware v2.010"], sep="\n" ) as cc: cc.trigger_mode = "blo" def test_cc1_clear(): with expected_protocol( ik.qubitekk.CC1, [":ACKN OF", "FIRM?", "CLEA"], ["", "Firmware v2.010"], sep="\n", ) as cc: cc.clear_counts() 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", ) as cc: assert not cc.acknowledge cc.acknowledge = True assert cc.acknowledge cc.clear_counts() cc.acknowledge = False assert not cc.acknowledge cc.clear_counts() def test_acknowledge_notimplementederror(): with pytest.raises(NotImplementedError), expected_protocol( ik.qubitekk.CC1, [":ACKN OF", "FIRM?"], ["Unknown Command", "Firmware v2.001"], sep="\n", ) as cc: cc.acknowledge = True def test_acknowledge_not_implemented_error(): # pylint: disable=protected-access with pytest.raises(NotImplementedError), expected_protocol( ik.qubitekk.CC1, [":ACKN OF", "FIRM?"], ["Unknown Command", "Firmware v2.001"], sep="\n", ) as cc: cc.acknowledge = True ================================================ FILE: tests/test_qubitekk/test_qubitekk_mc1.py ================================================ #!/usr/bin/env python """ Module containing tests for the Qubitekk MC1 """ # IMPORTS #################################################################### import pytest from instruments.units import ureg as u import instruments as ik from tests import expected_protocol # 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" ) as mc: assert mc.setting == 1 mc.setting = 0 def test_mc1_internal_position(): with expected_protocol( ik.qubitekk.MC1, ["POSI?", "STEP?"], ["-100", "1"], sep="\r" ) as mc: 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 * u.mm def test_mc1_direction(): 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: 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 * u.ms def test_mc1_step(): 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: assert mc.controller == mc.MotorType.radio def test_mc1_move_timeout(): with expected_protocol( ik.qubitekk.MC1, ["TIME?", "STEP?"], ["200", "1"], sep="\r" ) as mc: assert mc.move_timeout == 200 * u.ms def test_mc1_is_centering(): 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: assert mc.is_centering() is False def test_mc1_center(): 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: mc.reset() def test_mc1_move(): with expected_protocol( ik.qubitekk.MC1, ["STEP?", ":MOVE 0"], ["1"], sep="\r" ) as mc: mc.move(0) def test_mc1_move_value_error(): 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" ================================================ FILE: tests/test_rigol/test_rigolds1000.py ================================================ #!/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 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() ================================================ FILE: tests/test_split_str.py ================================================ #!/usr/bin/env python """ Module containing tests for the util_fns.split_unit_str utility function """ # IMPORTS #################################################################### import pytest from instruments.units import ureg as u from instruments.util_fns import split_unit_str # TEST CASES ################################################################# def test_split_unit_str_magnitude_and_units(): """ split_unit_str: Given the input "42 foobars" I expect the output to be (42, "foobars"). This checks that "[val] [units]" works where val is a non-scientific number """ mag, units = split_unit_str("42 foobars") assert mag == 42 assert units == "foobars" def test_split_unit_str_magnitude_and_default_units(): """ split_unit_str: Given the input "42" and default_units="foobars" I expect output to be (42, "foobars"). This checks that when given a string without units, the function returns default_units as the units. """ mag, units = split_unit_str("42", default_units="foobars") assert mag == 42 assert units == "foobars" def test_split_unit_str_ignore_default_units(): """ split_unit_str: Given the input "42 snafus" and default_units="foobars" I expect the output to be (42, "snafus"). This verifies that if the input has units, then any specified default_units are ignored. """ mag, units = split_unit_str("42 snafus", default_units="foobars") assert mag == 42 assert units == "snafus" def test_split_unit_str_lookups(): """ split_unit_str: Given the input "42 FOO" and a dictionary for our units lookup, I expect the output to be (42, "foobars"). 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"} mag, units = split_unit_str("42 FOO", lookup=unit_dict.__getitem__) assert mag == 42 assert units == "foobars" def test_split_unit_str_scientific_notation(): """ split_unit_str: Given inputs of scientific notation, I expect the output to correctly represent the inputted magnitude. This checks that inputs with scientific notation are correctly converted to floats. """ # No signs, no units mag, units = split_unit_str("123E1") assert mag == 1230 assert units == u.dimensionless # Negative exponential, no units mag, units = split_unit_str("123E-1") assert mag == 12.3 assert units == u.dimensionless # Negative magnitude, no units mag, units = split_unit_str("-123E1") assert mag == -1230 assert units == u.dimensionless # No signs, with units mag, units = split_unit_str("123E1 foobars") assert mag == 1230 assert units == "foobars" # Signs everywhere, with units mag, units = split_unit_str("-123E-1 foobars") assert mag == -12.3 assert units == "foobars" # Lower case e mag, units = split_unit_str("123e1") assert mag == 1230 assert units == u.dimensionless def test_split_unit_str_empty_string(): """ split_unit_str: Given an empty string, I expect the function to raise a ValueError. """ with pytest.raises(ValueError): _ = split_unit_str("") 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. """ with pytest.raises(ValueError): _ = split_unit_str("E3") def test_split_unit_str_magnitude_with_decimal(): """ split_unit_str: Given a string with magnitude containing a decimal, I expect the function to correctly parse the magnitude. """ # Decimal and units mag, units = split_unit_str("123.4 foobars") assert mag == 123.4 assert units == "foobars" # Decimal, units, and exponential mag, units = split_unit_str("123.4E1 foobars") assert mag == 1234 assert units == "foobars" 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. """ with pytest.raises(ValueError): _ = split_unit_str("foobars") ================================================ FILE: tests/test_srs/__init__.py ================================================ ================================================ FILE: tests/test_srs/test_srs345.py ================================================ #!/usr/bin/env python """ Unit tests for the SRS 345 function generator """ # IMPORTS ##################################################################### import instruments as ik from tests import ( expected_protocol, iterable_eq, ) from instruments.units import ureg as u # TESTS ####################################################################### def test_amplitude(): with expected_protocol( ik.srs.SRS345, ["AMPL?", "AMPL 0.1VP", "AMPL 0.1VR"], [ "1.234VP", ], ) as inst: 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(): with expected_protocol( ik.srs.SRS345, [ "FREQ?", f"FREQ {0.1:e}", ], [ "1.234", ], ) as inst: 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"], [ "1", ], ) as inst: assert inst.function == inst.Function.square inst.function = inst.Function.sinusoid def test_offset(): with expected_protocol( ik.srs.SRS345, [ "OFFS?", f"OFFS {0.1:e}", ], [ "1.234", ], ) as inst: assert inst.offset == 1.234 * u.V inst.offset = 0.1 * u.V def test_phase(): with expected_protocol( ik.srs.SRS345, [ "PHSE?", f"PHSE {0.1:e}", ], [ "1.234", ], ) as inst: assert inst.phase == 1.234 * u.degree inst.phase = 0.1 * u.degree ================================================ FILE: tests/test_srs/test_srs830.py ================================================ #!/usr/bin/env python """ Unit tests for the SRS 830 lock-in amplifier """ # IMPORTS ##################################################################### import time import pytest import serial import instruments as ik from instruments.abstract_instruments.comm import ( FileCommunicator, GPIBCommunicator, LoopbackCommunicator, SerialCommunicator, ) from instruments.optional_dep_finder import numpy from 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"], [ "1", ], ) as inst: assert inst.frequency_source == inst.FreqSource.internal inst.frequency_source = inst.FreqSource.external def test_frequency(): with expected_protocol( ik.srs.SRS830, ["FREQ?", f"FREQ {1000:e}"], [ "12.34", ], ) as inst: assert inst.frequency == 12.34 * u.Hz inst.frequency = 1 * u.kHz def test_phase(): with expected_protocol( ik.srs.SRS830, ["PHAS?", f"PHAS {10:e}"], [ "-45", ], ) as inst: assert inst.phase == -45 * u.degrees inst.phase = 10 * u.degrees def test_amplitude(): with expected_protocol( ik.srs.SRS830, ["SLVL?", f"SLVL {1:e}"], [ "0.1", ], ) as inst: 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"], [ "0", ], ) as inst: assert inst.input_shield_ground is False inst.input_shield_ground = True def test_coupling(): with expected_protocol( ik.srs.SRS830, ["ICPL?", "ICPL 0"], [ "1", ], ) as inst: assert inst.coupling == inst.Coupling.dc inst.coupling = inst.Coupling.ac def test_sample_rate(): # sends index of VALID_SAMPLE_RATES with expected_protocol( ik.srs.SRS830, ["SRAT?", "SRAT?", f"SRAT {5:d}", "SRAT 14"], ["8", "14"] ) as inst: assert inst.sample_rate == 16 * u.Hz assert inst.sample_rate == "trigger" inst.sample_rate = 2 inst.sample_rate = "trigger" def test_sample_rate_invalid(): 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"], [ "0", ], ) as inst: assert inst.buffer_mode == inst.BufferMode.one_shot inst.buffer_mode = inst.BufferMode.loop def test_num_data_points(): with expected_protocol( ik.srs.SRS830, ["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"], [ "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: inst.auto_offset(inst.Mode.x) inst.auto_offset("x") def test_auto_offset_invalid(): 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: inst.auto_phase() def test_init(): 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: inst.start_data_transfer() def test_take_measurement(): with expected_protocol( ik.srs.SRS830, [ "REST", "SRAT 4", "SEND 0", "FAST 2", "STRD", "PAUS", "SPTS?", "SPTS?", "TRCA?1,0,2", "SPTS?", "TRCA?2,0,2", ], ["2", "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_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: 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: inst.set_offset_expand(mode="x", offset=0, expand=1) def test_set_offset_expand_invalid_mode(): with pytest.raises(ValueError), expected_protocol(ik.srs.SRS830, [], []) as inst: inst.set_offset_expand(mode=inst.Mode.theta, offset=0, expand=1) def test_set_offset_expand_invalid_offset(): with pytest.raises(ValueError), expected_protocol(ik.srs.SRS830, [], []) as inst: inst.set_offset_expand(mode=inst.Mode.x, offset=106, expand=1) def test_set_offset_expand_invalid_expand(): with pytest.raises(ValueError), expected_protocol(ik.srs.SRS830, [], []) as inst: inst.set_offset_expand(mode=inst.Mode.x, offset=0, expand=5) def test_set_offset_expand_invalid_type_offset(): with pytest.raises(TypeError), expected_protocol(ik.srs.SRS830, [], []) as inst: inst.set_offset_expand(mode=inst.Mode.x, offset="derp", expand=1) def test_set_offset_expand_invalid_type_expand(): 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: inst.start_scan() def test_pause(): 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: data = inst.data_snap(mode1=inst.Mode.x, mode2=inst.Mode.y) expected = [1.234, 9.876] 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") expected = [1.234, 9.876] iterable_eq(data, expected) def test_data_snap_invalid_snap_mode1(): with pytest.raises(ValueError), expected_protocol(ik.srs.SRS830, [], []) as inst: _ = inst.data_snap(mode1=inst.Mode.xnoise, mode2=inst.Mode.y) def test_data_snap_invalid_snap_mode2(): with pytest.raises(ValueError), expected_protocol(ik.srs.SRS830, [], []) as inst: _ = inst.data_snap(mode1=inst.Mode.x, mode2=inst.Mode.ynoise) def test_data_snap_identical_modes(): 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"] ) as inst: data = inst.read_data_buffer(channel=inst.Mode.ch1) 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"] ) as inst: data = inst.read_data_buffer(channel="ch1") expected = (1.234, 9.876) if numpy: expected = numpy.array(expected) iterable_eq(data, expected) def test_read_data_buffer_invalid_mode(): 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: inst.clear_data_buffer() def test_set_channel_display(): 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 ) 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") def test_set_channel_display_invalid_channel(): 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 ) def test_set_channel_display_invalid_display(): 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, ) def test_set_channel_display_invalid_ratio(): 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 ) ================================================ FILE: tests/test_srs/test_srsctc100.py ================================================ #!/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 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() ================================================ FILE: tests/test_srs/test_srsdg645.py ================================================ #!/usr/bin/env python """ Module containing tests for the SRS DG645 """ # IMPORTS #################################################################### import pytest import instruments as ik from instruments.abstract_instruments.comm import GPIBCommunicator from instruments.units import ureg as u from 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 output level. """ with expected_protocol( ik.srs.SRSDG645, [ "LAMP? 1", "LAMP 1,4.0", ], ["3.2"], ) as ddg: unit_eq(ddg.output["AB"].level_amplitude, u.Quantity(3.2, "V")) ddg.output["AB"].level_amplitude = 4.0 def test_srsdg645_output_offset(): """ SRSDG645: Checks getting/setting unitful output offset. """ with expected_protocol( ik.srs.SRSDG645, [ "LOFF? 1", "LOFF 1,2.0", ], ["1.2"], ) as ddg: 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(): """ 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 ================================================ FILE: tests/test_sunpower/__init__.py ================================================ ================================================ FILE: tests/test_sunpower/test_cryotel_gt.py ================================================ #!/usr/bin/env python """ Module containing tests for the Sunpower CryoTel GT """ # IMPORTS #################################################################### import pytest import instruments as ik from instruments.abstract_instruments.comm import GPIBCommunicator from instruments.units import ureg as u from tests import expected_protocol, make_name_test, unit_eq # TESTS ###################################################################### # PROPERTIES # def test_at_temperature_band(): """Set/ get the at_temperature_band property of the CryoTel GT.""" with expected_protocol( ik.sunpower.CryoTelGT, ["SET TBAND", "SET TBAND=0.07", "SET TBAND"], ["SET TBAND", "0.500", "SET TBAND=0.07", "0.07", "SET TBAND", "0.07"], sep="\r", ) as ct: assert ct.at_temperature_band == 0.5 * u.K ct.at_temperature_band = 0.07 * u.K assert ct.at_temperature_band == 0.07 * u.K def test_control_mode(): """Set/ get the control_mode property of the CryoTel GT.""" with expected_protocol( ik.sunpower.CryoTelGT, ["SET PID", "SET PID=0", "SET PID"], ["SET PID", "2", "SET PID=0", "0", "SET PID", "0"], sep="\r", ) as ct: assert ct.control_mode == ct.ControlMode.TEMPERATURE ct.control_mode = ct.ControlMode.POWER assert ct.control_mode == ct.ControlMode.POWER with pytest.raises(ValueError): ct.control_mode = "invalid_mode" def test_errors(): """Get the error codes of the CryoTel GT and return error strings.""" with expected_protocol( ik.sunpower.CryoTelGT, ["ERROR", "ERROR", "ERROR"], ["ERROR", "100000", "ERROR", "000000", "ERROR", "011001"], sep="\r", ) as ct: assert ct.errors == ["Temperature Sensor Error"] assert ct.errors == [] assert ct.errors == [ "Over Current", "Non-volatile Memory Error", "Watchdog Error", ] def test_ki(): """Set/get the ki property of the CryoTel GT.""" with expected_protocol( ik.sunpower.CryoTelGT, ["SET KI", "SET KI=0.10", "SET KI"], ["SET KI", "5.0", "SET KI=0.10", "0.1", "SET KI", "0.1"], sep="\r", ) as ct: assert ct.ki == 5.0 ct.ki = 0.1 assert ct.ki == 0.1 def test_kp(): """Set/get the kp property of the CryoTel GT.""" with expected_protocol( ik.sunpower.CryoTelGT, ["SET KP", "SET KP=0.10", "SET KP"], ["SET KP", "0.5", "SET KP=0.10", "0.10", "SET KP", "0.10"], sep="\r", ) as ct: assert ct.kp == 0.5 ct.kp = 0.1 assert ct.kp == 0.1 def test_power(): """Get the current power of the CryoTel GT.""" with expected_protocol( ik.sunpower.CryoTelGT, ["P", "P"], ["P", "0.00", "P", "500.0"], sep="\r", ) as ct: assert ct.power == 0.0 * u.W assert ct.power == 500 * u.W def test_power_current_and_limits(): """Get the current power and power limits of the CryoTel GT.""" with expected_protocol( ik.sunpower.CryoTelGT, ["E"], ["P", "0.00", "500.00", "1000.00"], sep="\r", ) as ct: assert ct.power_current_and_limits == (0.0 * u.W, 500 * u.W, 1000 * u.W) def test_power_max(): """Get/set the maximum user power.""" with expected_protocol( ik.sunpower.CryoTelGT, ["SET MAX", "SET MAX=100.00", "SET MAX"], ["SET MAX", "10.0", "SET MAX=100.00", "100.0", "SET MAX", "100.0"], sep="\r", ) as ct: assert ct.power_max == 10 * u.W ct.power_max = 100 * u.W assert ct.power_max == 100 * u.W with pytest.raises(ValueError): ct.power_max = 1000 * u.W with pytest.raises(ValueError): ct.power_max = -10 * u.W def test_power_min(): """Get/set the minimum user power.""" with expected_protocol( ik.sunpower.CryoTelGT, ["SET MIN", "SET MIN=10.00", "SET MIN"], ["SET MIN", "0.0", "SET MIN=10.00", "10.0", "SET MIN", "10.0"], sep="\r", ) as ct: assert ct.power_min == 0 * u.W ct.power_min = 10 * u.W assert ct.power_min == 10 * u.W with pytest.raises(ValueError): ct.power_min = 1000 * u.W with pytest.raises(ValueError): ct.power_min = -10 * u.W def test_power_setpoint(): """Get/set the power setpoint of the CryoTel GT.""" with expected_protocol( ik.sunpower.CryoTelGT, ["SET PWOUT", "SET PWOUT=100.00", "SET PWOUT"], ["SET PWOUT", "0.00", "SET PWOUT=100.00", "100.00", "SET PWOUT", "100.00"], sep="\r", ) as ct: assert ct.power_setpoint == 0 * u.W ct.power_setpoint = 100 * u.W assert ct.power_setpoint == 100 * u.W with pytest.raises(ValueError): ct.power_setpoint = 1000 * u.W with pytest.raises(ValueError): ct.power_setpoint = -10 * u.W def test_serial_number(): """Get the serial number of the CryoTel GT.""" with expected_protocol( ik.sunpower.CryoTelGT, ["SERIAL"], ["SERIAL", "serial", "revision"], sep="\r", ) as ct: assert ct.serial_number == ["serial", "revision"] def test_state(): """Get the state of the CryoTel GT.""" STATE_EXAMPLE = [ "MODE = 002.00", "TSTATM = 000.00", "TSTAT = 000.00", "SSTOPM = 000.00", "SSTOP = 000.00", "PID = 002.00", "LOCK = 000.00", "MAX = 300.00", "MIN = 000.00", "PWOUT = 000.00", "TTARGET = 000.00", "TBAND = 000.50", "KI = 000.50", "KP = 050.00000", ] with expected_protocol( ik.sunpower.CryoTelGT, ["STATE"], ["STATE"] + STATE_EXAMPLE, sep="\r", ) as ct: assert ct.state == STATE_EXAMPLE def test_temperature(): """Get the temperature of the CryoTel GT.""" with expected_protocol( ik.sunpower.CryoTelGT, ["TC", "TC"], ["TC", "77.00", "TC", "72.89"], sep="\r", ) as ct: assert ct.temperature == 77.0 * u.K assert ct.temperature == 72.89 * u.K def test_temperature_setpoint(): """Get/set the temperature setpoint of the CryoTel GT.""" with expected_protocol( ik.sunpower.CryoTelGT, ["SET TTARGET", "SET TTARGET=100.00", "SET TTARGET"], [ "SET TTARGET", "0.00", "SET TTARGET=100.00", "100.00", "SET TTARGET", "100.00", ], sep="\r", ) as ct: assert ct.temperature_setpoint == 0 * u.K ct.temperature_setpoint = 100 * u.K assert ct.temperature_setpoint == 100 * u.K def test_thermostat(): """Get/set the thermostat property of the CryoTel GT.""" with expected_protocol( ik.sunpower.CryoTelGT, ["SET TSTATM", "SET TSTATM=1", "SET TSTATM"], ["SET TSTATM", "0", "SET TSTATM=1", "1", "SET TSTATM", "1"], sep="\r", ) as ct: assert ct.thermostat is False ct.thermostat = True assert ct.thermostat is True with pytest.raises(ValueError): ct.thermostat = "invalid_value" def test_thermostat_status(): """Get the thermostat status of the CryoTel GT.""" with expected_protocol( ik.sunpower.CryoTelGT, ["TSTAT", "TSTAT"], ["TSTAT", "0.00", "TSTAT", "1.00"], sep="\r", ) as ct: assert ct.thermostat_status == ct.ThermostatStatus.OFF assert ct.thermostat_status == ct.ThermostatStatus.ON def test_stop(): """Get/set the soft stop property of the CryoTel GT to turn it off.""" with expected_protocol( ik.sunpower.CryoTelGT, ["SET SSTOP", "SET SSTOP=1", "SET SSTOP"], ["SET SSTOP", "0", "SET SSTOP=1", "1", "SET SSTOP", "1"], sep="\r", ) as ct: assert ct.stop is False ct.stop = True assert ct.stop is True with pytest.raises(ValueError): ct.stop = "invalid_value" def test_stop_mode(): """Get/set the stop mode of the CryoTel GT.""" with expected_protocol( ik.sunpower.CryoTelGT, ["SET SSTOPM", "SET SSTOPM=1", "SET SSTOPM"], ["SET SSTOPM", "0", "SET SSTOPM=1", "1", "SET SSTOPM", "1"], sep="\r", ) as ct: assert ct.stop_mode == ct.StopMode.HOST ct.stop_mode = ct.StopMode.DIGIO assert ct.stop_mode == ct.StopMode.DIGIO with pytest.raises(ValueError): ct.stop_mode = "invalid_mode" # METHODS # def test_reset(): """Reset the CryoTel GT.""" with expected_protocol( ik.sunpower.CryoTelGT, ["RESET=F"], ["RESET=F", "RESETTING TO FACTORY DEFAULT...", "FACTORY RESET COMPLETE!"], sep="\r", ) as ct: ct.reset() def test_save_control_mode(): """Save the current control mode of the CryoTel GT.""" with expected_protocol( ik.sunpower.CryoTelGT, ["SAVE PID"], ["SAVE PID"], sep="\r", ) as ct: ct.save_control_mode() def test_query_warning(): """Raise a warning if a value was not set because not accepted.""" with expected_protocol( ik.sunpower.CryoTelGT, ["SET TTARGET=0.00"], ["SET TTARGET=0.00", "77.00"], sep="\r", ) as ct: with pytest.warns(UserWarning) as warn: ct.temperature_setpoint = 0 * u.K assert "Set value 0" in warn[0].message.args[0] assert "returned value 77" in warn[0].message.args[0] ================================================ FILE: tests/test_tektronix/__init__.py ================================================ ================================================ FILE: tests/test_tektronix/test_tekawg2000.py ================================================ #!/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 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=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 ================================================ FILE: tests/test_tektronix/test_tekdpo70000.py ================================================ #!/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 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() ================================================ FILE: tests/test_tektronix/test_tektronix_tds224.py ================================================ #!/usr/bin/env python """ Module containing tests for the Tektronix TDS224 """ # IMPORTS #################################################################### from enum import Enum import time from hypothesis import given, strategies as st import pytest import instruments as ik from instruments.optional_dep_finder import numpy from tests import ( expected_protocol, iterable_eq, make_name_test, ) # TESTS ###################################################################### # 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"] ) as tek: assert tek.data_width == 2 tek.data_width = 1 @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?", "DAT:SOU MATH"], ["MATH", "CH1"], ) as tek: 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.TekTDS224.Channel(tek, 0) def test_tektds224_channel_coupling(): with expected_protocol( 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, [ "DAT:SOU?", "DAT:SOU CH2", "DAT:ENC RIB", "DATA:WIDTH?", "CURVE?", "WFMP:CH2:YOF?", "WFMP:CH2:YMU?", "WFMP:CH2:YZE?", "WFMP:XZE?", "WFMP:XIN?", "WFMP:CH2:NR_P?", "DAT:SOU CH1", ], [ "CH1", "2", # pylint: disable=no-member "#210" + bytes.fromhex("00000001000200030004").decode("utf-8") + "0", "1", "0", "0", "1", "5", ], ) as tek: data = tuple(range(5)) if numpy: data = numpy.array([0, 1, 2, 3, 4]) x, y = tek.channel[1].read_waveform() 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() ================================================ FILE: tests/test_tektronix/test_tktds5xx.py ================================================ #!/usr/bin/env python """ Tests for the Tektronix TDS 5xx series oscilloscope. """ # IMPORTS ##################################################################### from datetime import datetime import struct import time from unittest import mock from hypothesis import ( given, strategies as st, ) import pytest import instruments as ik from instruments.optional_dep_finder import numpy from 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)) @mock.patch.object(time, "sleep", return_value=None) 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 '= 2 ================================================ FILE: tests/test_thorlabs/test_packets.py ================================================ #!/usr/bin/env python """ Module containing tests for the ThorLabsPacket class. """ # IMPORTS #################################################################### import struct import pytest from instruments.thorlabs import _packets # TESTS ###################################################################### # pylint: disable=protected-access,redefined-outer-name # variable to parametrize parameters or data: param1, param2, data, has_data params_or_data = ( (0x00, 0x00, None, False), (None, None, struct.pack(" 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" ) as lcc: name = lcc.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", ) as lcc: unit_eq(lcc.frequency, u.Quantity(20, "Hz")) lcc.frequency = 10.0 def test_lcc25_frequency_lowlimit(): with pytest.raises(ValueError), expected_protocol( ik.thorlabs.LCC25, ["freq=0.0"], ["freq=0.0", "> "], sep="\r" ) as lcc: lcc.frequency = 0.0 def test_lcc25_frequency_highlimit(): with pytest.raises(ValueError), expected_protocol( ik.thorlabs.LCC25, ["freq=160.0"], ["freq=160.0", "> "], sep="\r" ) as lcc: lcc.frequency = 160.0 def test_lcc25_mode(): with expected_protocol( ik.thorlabs.LCC25, ["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 def test_lcc25_mode_invalid(): 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", ) as lcc: assert lcc.enable is False lcc.enable = True def test_lcc25_enable_invalid_type(): 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", ) as lcc: assert lcc.extern is False lcc.extern = True def test_tc200_extern_invalid_type(): 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", ) as lcc: assert lcc.remote is False lcc.remote = True def test_tc200_remote_invalid_type(): 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.000"], ["volt1?", "20", "> volt1=10.000", "> "], sep="\r", ) as lcc: unit_eq(lcc.voltage1, u.Quantity(20, "V")) lcc.voltage1 = 10.0 def test_check_cmd(): assert ik.thorlabs.thorlabs_utils.check_cmd("blo") == 1 assert ik.thorlabs.thorlabs_utils.check_cmd("CMD_NOT_DEFINED") == 0 assert ik.thorlabs.thorlabs_utils.check_cmd("CMD_ARG_INVALID") == 0 def test_lcc25_voltage2(): with expected_protocol( ik.thorlabs.LCC25, [ "volt2?", "volt2=10.000", ], ["volt2?", "20", "> volt2=10.000", "> "], sep="\r", ) as lcc: 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.000"], ["min?", "20", "> min=10.000", "> "], sep="\r", ) as lcc: 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.000"], ["max?", "20", "> max=10.000", "> "], sep="\r", ) as lcc: 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", ) as lcc: unit_eq(lcc.dwell, u.Quantity(20, "ms")) lcc.dwell = 10 def test_lcc25_dwell_positive(): with pytest.raises(ValueError), expected_protocol( ik.thorlabs.LCC25, ["dwell=-10"], ["dwell=-10", "> "], sep="\r" ) as lcc: lcc.dwell = -10 def test_lcc25_increment(): with expected_protocol( ik.thorlabs.LCC25, ["increment?", "increment=10.000"], ["increment?", "20", "> increment=10.000", "> "], sep="\r", ) as lcc: unit_eq(lcc.increment, u.Quantity(20, "V")) lcc.increment = 10.0 def test_lcc25_increment_positive(): 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" ) as lcc: lcc.default() def test_lcc25_save(): with expected_protocol( 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" ) as lcc: lcc.set_settings(2) def test_lcc25_set_settings_invalid(): 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" ) as lcc: lcc.get_settings(2) def test_lcc25_get_settings_invalid(): 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" ) as lcc: lcc.test_mode() def test_lcc25_remote_invalid_type(): with pytest.raises(TypeError), expected_protocol(ik.thorlabs.LCC25, [], []) as lcc: lcc.remote = "blo" def test_lcc25_extern_invalid_type(): with pytest.raises(TypeError), expected_protocol(ik.thorlabs.LCC25, [], []) as lcc: lcc.extern = "blo" ================================================ FILE: tests/test_thorlabs/test_thorlabs_pm100usb.py ================================================ #!/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 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 ================================================ FILE: tests/test_thorlabs/test_thorlabs_sc10.py ================================================ #!/usr/bin/env python """ Module containing tests for the Thorlabs SC10 """ # IMPORTS #################################################################### import pytest from instruments.units import ureg as u import instruments as ik from tests import expected_protocol, unit_eq # TESTS ###################################################################### def test_sc10_name(): with expected_protocol( ik.thorlabs.SC10, ["id?"], ["id?", "bloopbloop", "> "], sep="\r" ) as sc: assert sc.name == "bloopbloop" def test_sc10_enable_query(): with expected_protocol( ik.thorlabs.SC10, ["ens?"], ["ens?", "0", "> "], sep="\r" ) as sc: assert sc.enable is False @pytest.mark.parametrize("status", [0, 1]) @pytest.mark.parametrize("value", [0, 1]) def test_sc10_enable_send(status, value): host_to_ins = ["ens?"] ins_to_host = ["ens?", f"{status}"] if value != status: host_to_ins.append("ens") ins_to_host += ["> ens", "> "] else: ins_to_host.append("> ") with expected_protocol(ik.thorlabs.SC10, host_to_ins, ins_to_host, sep="\r") as sc: sc.enable = bool(value) def test_sc10_enable_invalid(): 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" ) as sc: assert sc.repeat == 20 sc.repeat = 10 def test_sc10_repeat_invalid(): with pytest.raises(ValueError), expected_protocol( ik.thorlabs.SC10, [], [], sep="\r" ) as sc: sc.repeat = -1 def test_sc10_mode(): with expected_protocol( ik.thorlabs.SC10, ["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 def test_sc10_mode_invalid(): with pytest.raises(ValueError), expected_protocol( ik.thorlabs.SC10, [], [], sep="\r" ) as sc: sc.mode = "blo" def test_sc10_trigger(): with expected_protocol( ik.thorlabs.SC10, ["trig?", "trig=1"], ["trig?", "0", "> trig=1", "> "], sep="\r", ) as sc: assert sc.trigger == 0 sc.trigger = 1 def test_sc10_out_trigger(): with expected_protocol( ik.thorlabs.SC10, ["xto?", "xto=1"], ["xto?", "0", "> xto=1", "> "], sep="\r" ) as sc: assert sc.out_trigger == 0 sc.out_trigger = 1 def test_sc10_open_time(): with expected_protocol( ik.thorlabs.SC10, ["open?", "open=10"], ["open?", "20", "> open=10", "> "], sep="\r", ) as sc: 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", ) as sc: 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", ) as sc: assert sc.baud_rate == 9600 sc.baud_rate = 115200 def test_sc10_baud_rate_error(): 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" ) as sc: assert sc.closed def test_sc10_interlock(): with expected_protocol( 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" ) as sc: assert sc.default() def test_sc10_save(): with expected_protocol( 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" ) as sc: assert sc.save_mode() def test_sc10_restore(): with expected_protocol( ik.thorlabs.SC10, ["resp"], ["resp", "1", "> "], sep="\r" ) as sc: assert sc.restore() ================================================ FILE: tests/test_thorlabs/test_thorlabs_tc200.py ================================================ #!/usr/bin/env python """ Module containing tests for the Thorlabs TC200 """ # IMPORTS #################################################################### from enum import IntEnum import pytest from instruments.units import ureg as u import instruments as ik from tests import expected_protocol # TESTS ###################################################################### def test_tc200_name(): with expected_protocol( ik.thorlabs.TC200, ["*idn?"], ["*idn?", "bloopbloop", "> "], sep="\r" ) as tc: assert tc.name() == "bloopbloop" def test_tc200_mode(): with expected_protocol( ik.thorlabs.TC200, ["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 tc.mode = ik.thorlabs.TC200.Mode.cycle def test_tc200_mode_2(): with expected_protocol( ik.thorlabs.TC200, ["mode=normal"], ["mode=normal", "Command error CMD_ARG_RANGE_ERR\n", "> "], sep="\r", ) as tc: tc.mode = ik.thorlabs.TC200.Mode.normal def test_tc200_mode_error(): with pytest.raises(TypeError), expected_protocol( ik.thorlabs.TC200, [], [], sep="\r" ) as tc: tc.mode = "blo" def test_tc200_mode_error2(): 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", ) as tc: assert tc.enable == 0 tc.enable = True tc.enable = False def test_tc200_enable_type(): with pytest.raises(TypeError), expected_protocol( ik.thorlabs.TC200, [], [], sep="\r" ) as tc: tc.enable = "blo" def test_tc200_temperature(): with expected_protocol( ik.thorlabs.TC200, [ "tact?", ], [ "tact?", "30 C", "> ", ], sep="\r", ) as tc: assert tc.temperature == u.Quantity(30.0, u.degC) def test_tc200_temperature_set(): with expected_protocol( ik.thorlabs.TC200, ["tset?", "tmax?", "tset=40"], ["tset?", "30 C", "> tmax?", "250", "> tset=40", "> "], sep="\r", ) as tc: assert tc.temperature_set == u.Quantity(30.0, u.degC) tc.temperature_set = u.Quantity(40, u.degC) def test_tc200_temperature_set_celsius(): """Ensure celsius is stripped if returned by instrument, see issue #331""" with expected_protocol( ik.thorlabs.TC200, ["tset?"], ["tset?", "30 Celsius", "> "], sep="\r" ) as tc: assert tc.temperature_set == u.Quantity(30.0, u.degC) def test_tc200_temperature_range(): with pytest.raises(ValueError), expected_protocol( ik.thorlabs.TC200, ["tmax?"], ["tmax?", "40", "> "], sep="\r" ) as tc: 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", ) 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", ) 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", ) 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", ) as tc: assert tc.pid == [2, 0, 220] tc.pid = (2, 0, 220) def test_tc200_pid_invalid_type(): with pytest.raises(TypeError), expected_protocol( ik.thorlabs.TC200, [], [], sep="\r" ) as tc: tc.pid = "foo" def test_tc200_pmin(): with pytest.raises(ValueError), expected_protocol( ik.thorlabs.TC200, ["pgain=-1"], ["pgain=-1", "> "], sep="\r" ) as tc: tc.p = -1 def test_tc200_pmax(): with pytest.raises(ValueError), expected_protocol( ik.thorlabs.TC200, ["pgain=260"], ["pgain=260", "> "], sep="\r" ) as tc: tc.p = 260 def test_tc200_imin(): with pytest.raises(ValueError), expected_protocol( ik.thorlabs.TC200, ["igain=-1"], ["igain=-1", "> "], sep="\r" ) as tc: tc.i = -1 def test_tc200_imax(): with pytest.raises(ValueError), expected_protocol( ik.thorlabs.TC200, ["igain=260"], ["igain=260", "> "], sep="\r" ) as tc: tc.i = 260 def test_tc200_dmin(): with pytest.raises(ValueError), expected_protocol( ik.thorlabs.TC200, ["dgain=-1"], ["dgain=-1", "> "], sep="\r" ) as tc: tc.d = -1 def test_tc200_dmax(): with pytest.raises(ValueError), expected_protocol( ik.thorlabs.TC200, ["dgain=260"], ["dgain=260", "> "], sep="\r" ) as tc: tc.d = 260 def test_tc200_degrees(): with expected_protocol( ik.thorlabs.TC200, ["stat?", "stat?", "stat?", "unit=c", "unit=f", "unit=k"], [ "stat?", "44 > stat?", "54 > stat?", "0 > unit=c", "> unit=f", "> unit=k", "> ", ], sep="\r", ) as tc: assert tc.degrees == u.degK assert tc.degrees == u.degC assert tc.degrees == u.degF tc.degrees = u.degC tc.degrees = u.degF tc.degrees = u.degK def test_tc200_degrees_invalid(): with pytest.raises(TypeError), expected_protocol( ik.thorlabs.TC200, [], [], sep="\r" ) as tc: tc.degrees = "blo" def test_tc200_sensor(): with expected_protocol( ik.thorlabs.TC200, ["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 def test_tc200_sensor_error(): with pytest.raises(ValueError), expected_protocol(ik.thorlabs.TC200, [], []) as tc: tc.sensor = "blo" def test_tc200_sensor_error2(): 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", ) as tc: assert tc.beta == 5600 tc.beta = 2000 def test_tc200_beta_min(): with pytest.raises(ValueError), expected_protocol( ik.thorlabs.TC200, ["beta=200"], ["beta=200", "> "], sep="\r" ) as tc: tc.beta = 200 def test_tc200_beta_max(): with pytest.raises(ValueError), expected_protocol( ik.thorlabs.TC200, ["beta=20000"], ["beta=20000", "> "], sep="\r" ) as tc: tc.beta = 20000 def test_tc200_max_power(): with expected_protocol( ik.thorlabs.TC200, ["pmax?", "pmax=12.0"], ["pmax?", "15.0", "> pmax=12.0", "> "], sep="\r", ) as tc: assert tc.max_power == 15.0 * u.W tc.max_power = 12 * u.W def test_tc200_power_min(): with pytest.raises(ValueError), expected_protocol( ik.thorlabs.TC200, ["PMAX=-2"], ["PMAX=-2", "> "], sep="\r" ) as tc: tc.max_power = -1 def test_tc200_power_max(): with pytest.raises(ValueError), expected_protocol( ik.thorlabs.TC200, ["PMAX=20000"], ["PMAX=20000", "> "], sep="\r" ) as tc: tc.max_power = 20000 def test_tc200_max_temperature(): with expected_protocol( ik.thorlabs.TC200, ["tmax?", "tmax=180.0"], ["tmax?", "200.0", "> tmax=180.0", "> "], sep="\r", ) as tc: assert tc.max_temperature == u.Quantity(200.0, u.degC) tc.max_temperature = u.Quantity(180, u.degC) def test_tc200_temp_min(): with pytest.raises(ValueError), expected_protocol( ik.thorlabs.TC200, ["TMAX=-2"], ["TMAX=-2", ">"], sep="\r" ) as tc: tc.max_temperature = -1 def test_tc200_temp_max(): with pytest.raises(ValueError), expected_protocol( ik.thorlabs.TC200, ["TMAX=20000"], ["TMAX=20000", ">"], sep="\r" ) as tc: tc.max_temperature = 20000 ================================================ FILE: tests/test_thorlabs/test_utils.py ================================================ #!/usr/bin/env python """ Module containing tests for the Thorlabs util functions """ # IMPORTS #################################################################### import instruments as ik # TESTS ###################################################################### def test_check_cmd(): assert ik.thorlabs.thorlabs_utils.check_cmd("blo") == 1 assert ik.thorlabs.thorlabs_utils.check_cmd("CMD_NOT_DEFINED") == 0 assert ik.thorlabs.thorlabs_utils.check_cmd("CMD_ARG_INVALID") == 0 ================================================ FILE: tests/test_toptica/__init__.py ================================================ ================================================ FILE: tests/test_toptica/test_toptica_topmode.py ================================================ #!/usr/bin/env python """ Module containing tests for the Toptica Topmode """ # IMPORTS ##################################################################### from datetime import datetime import pytest from instruments.units import ureg as u import instruments as ik from tests import expected_protocol # TESTS ####################################################################### 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)", "bloop1", "> (param-ref 'laser2:serial-number)", "bloop2", "> ", ], sep="\r\n", ) as tm: assert tm.laser[0].serial_number == "bloop1" assert tm.laser[1].serial_number == "bloop2" def test_model(): with expected_protocol( ik.toptica.TopMode, ["(param-ref 'laser1:model)", "(param-ref 'laser2:model)"], [ "(param-ref 'laser1:model)", "bloop1", "> (param-ref 'laser2:model)", "bloop2", "> ", ], sep="\r\n", ) as tm: assert tm.laser[0].model == "bloop1" assert tm.laser[1].model == "bloop2" def test_wavelength(): with expected_protocol( ik.toptica.TopMode, ["(param-ref 'laser1:wavelength)", "(param-ref 'laser2:wavelength)"], [ "(param-ref 'laser1:wavelength)", "640", "> (param-ref 'laser2:wavelength)", "405.3", "> ", ], sep="\r\n", ) as tm: assert tm.laser[0].wavelength == 640 * u.nm assert tm.laser[1].wavelength == 405.3 * u.nm def test_laser_enable(): with expected_protocol( ik.toptica.TopMode, [ "(param-ref 'laser1:emission)", "(param-ref 'laser1:serial-number)", "(param-set! 'laser1:enable-emission #t)", ], [ "(param-ref 'laser1:emission)", "#f", "> (param-ref 'laser1:serial-number)", "bloop1", "> (param-set! 'laser1:enable-emission #t)", "0", "> ", ], sep="\r\n", ) as tm: assert tm.laser[0].enable is False tm.laser[0].enable = True def test_laser_enable_no_laser(): with pytest.raises(RuntimeError), expected_protocol( ik.toptica.TopMode, [ "(param-ref 'laser1:serial-number)", "(param-set! 'laser1:enable-emission #t)", ], [ "(param-ref 'laser1:serial-number)", "unknown", "> (param-set! 'laser1:enable-emission #t)", "0", "> ", ], sep="\r\n", ) as tm: tm.laser[0].enable = True def test_laser_enable_error(): with pytest.raises(TypeError), expected_protocol( ik.toptica.TopMode, [ "(param-ref 'laser1:serial-number)", "(param-set! 'laser1:enable-emission #t)", ], [ "(param-ref 'laser1:serial-number)", "bloop1", "> (param-set! 'laser1:enable-emission #t)", "0", "> ", ], sep="\n", ) as tm: 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", ) as tm: assert tm.laser[0].tec_status is False def test_laser_intensity(): with expected_protocol( ik.toptica.TopMode, ["(param-ref 'laser1:intensity)"], ["(param-ref 'laser1:intensity)", "0.666", "> "], sep="\r\n", ) as tm: assert tm.laser[0].intensity == 0.666 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", ) as tm: assert tm.laser[0].mode_hop is False def test_laser_lock_start(): with expected_protocol( ik.toptica.TopMode, [ "(param-ref 'laser1:charm:correction-status)", "(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"', "> ", ], sep="\r\n", ) as tm: _date = datetime(2012, 12, 1, 1, 2, 1) assert tm.laser[0].lock_start == _date def test_laser_lock_start_runtime_error(): 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:correction-status)", "0", "> (param-ref 'laser1:charm:reg:started)", '""', "> ", ], sep="\r\n", ) as tm: _date = datetime(2012, 12, 1, 1, 2, 1) assert tm.laser[0].lock_start == _date def test_laser_first_mode_hop_time_runtime_error(): 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:mh-occurred)", "#f", "> (param-ref 'laser1:charm:reg:first-mh)", '""', "> ", ], sep="\r\n", ) as tm: assert tm.laser[0].first_mode_hop_time is None def test_laser_first_mode_hop_time(): with expected_protocol( ik.toptica.TopMode, [ "(param-ref 'laser1:charm:reg:mh-occurred)", "(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"', "> ", ], sep="\r\n", ) as tm: _date = datetime(2012, 12, 1, 1, 2, 1) assert tm.laser[0].first_mode_hop_time == _date def test_laser_latest_mode_hop_time_none(): 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:mh-occurred)", "#f", "> (param-ref 'laser1:charm:reg:latest-mh)", '""', "> ", ], sep="\r\n", ) as tm: assert tm.laser[0].latest_mode_hop_time is None def test_laser_latest_mode_hop_time(): with expected_protocol( ik.toptica.TopMode, [ "(param-ref 'laser1:charm:reg:mh-occurred)", "(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"', "> ", ], sep="\r\n", ) as tm: _date = datetime(2012, 12, 1, 1, 2, 1) assert tm.laser[0].latest_mode_hop_time == _date 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", ) as tm: assert ( tm.laser[0].correction_status == ik.toptica.TopMode.CharmStatus.un_initialized ) def test_laser_correction(): with expected_protocol( ik.toptica.TopMode, [ "(param-ref 'laser1:charm:correction-status)", # 1st "(exec 'laser1:charm:start-correction-initial)", "(param-ref 'laser1:charm:correction-status)", # 2nd "(exec 'laser1:charm:start-correction)", "(param-ref 'laser1:charm:correction-status)", # 3rd "(param-ref 'laser1:charm:correction-status)", # 4th "(exec 'laser1:charm:start-correction)", ], [ "(param-ref 'laser1:charm:correction-status)", # 1st "0", "> (exec 'laser1:charm:start-correction-initial)", "()", "> (param-ref 'laser1:charm:correction-status)", # 3nd "1", "> (exec 'laser1:charm:start-correction)", "()", "> (param-ref 'laser1:charm:correction-status)", # 3rd "3", "> (param-ref 'laser1:charm:correction-status)", # 4th "2", "> (exec 'laser1:charm:start-correction)", "()", "> ", ], sep="\r\n", ) as tm: tm.laser[0].correction() tm.laser[0].correction() _ = tm.laser[0].correction_status tm.laser[0].correction() def test_reboot_system(): with expected_protocol( ik.toptica.TopMode, ["(exec 'reboot-system)"], ["(exec 'reboot-system)", "reboot process started.", "> "], sep="\r\n", ) as tm: tm.reboot() def test_laser_ontime(): with expected_protocol( ik.toptica.TopMode, ["(param-ref 'laser1:ontime)"], ["(param-ref 'laser1:ontime)", "10000", "> "], sep="\r\n", ) as tm: 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", ) as tm: assert tm.laser[0].charm_status == 1 def test_laser_temperature_control_status(): with expected_protocol( ik.toptica.TopMode, ["(param-ref 'laser1:health)"], ["(param-ref 'laser1:health)", "230", "> "], sep="\r\n", ) as tm: assert tm.laser[0].temperature_control_status == 1 def test_laser_current_control_status(): with expected_protocol( ik.toptica.TopMode, ["(param-ref 'laser1:health)"], ["(param-ref 'laser1:health)", "230", "> "], sep="\r\n", ) as tm: assert tm.laser[0].current_control_status == 1 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", ) as tm: assert tm.laser[0].production_date == "2016-01-16" def test_set_str(): with expected_protocol( ik.toptica.TopMode, ['(param-set! \'blo "blee")'], ['(param-set! \'blo "blee")', "0", "> "], sep="\r\n", ) as tm: 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", ) as tm: tm.set("blo", ["blee", "blo"]) def test_display(): with expected_protocol( ik.toptica.TopMode, ["(param-disp 'blo)"], ["(param-disp 'blo)", "bloop", "> "], sep="\r\n", ) as tm: 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)", "#f", "> (param-set! 'enable-emission #f)", "0", "> ", ], sep="\r\n", ) as tm: assert tm.enable is False tm.enable = False def test_firmware(): with expected_protocol( ik.toptica.TopMode, ["(param-ref 'fw-ver)"], ["(param-ref 'fw-ver)", "1.02.01", "> "], sep="\r\n", ) as tm: assert tm.firmware == (1, 2, 1) def test_serial_number(): with expected_protocol( ik.toptica.TopMode, ["(param-ref 'serial-number)"], ["(param-ref 'serial-number)", "010101", "> "], sep="\r\n", ) as tm: assert tm.serial_number == "010101" def test_enable_error(): 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", ) as tm: assert tm.locked is False def test_interlock(): with expected_protocol( ik.toptica.TopMode, ["(param-ref 'interlock-open)"], ["(param-ref 'interlock-open)", "#f", "> "], sep="\r\n", ) as tm: assert tm.interlock is False def test_fpga_status(): with expected_protocol( ik.toptica.TopMode, ["(param-ref 'system-health)"], ["(param-ref 'system-health)", "0", "> "], sep="\r\n", ) as tm: assert tm.fpga_status is True def test_fpga_status_false(): with expected_protocol( ik.toptica.TopMode, ["(param-ref 'system-health)"], ["(param-ref 'system-health)", "#f", "> "], sep="\r\n", ) as tm: assert tm.fpga_status is False def test_temperature_status(): with expected_protocol( ik.toptica.TopMode, ["(param-ref 'system-health)"], ["(param-ref 'system-health)", "2", "> "], sep="\r\n", ) as tm: assert tm.temperature_status is False def test_current_status(): with expected_protocol( ik.toptica.TopMode, ["(param-ref 'system-health)"], ["(param-ref 'system-health)", "4", "> "], sep="\r\n", ) as tm: assert tm.current_status is False ================================================ FILE: tests/test_toptica/test_toptica_utils.py ================================================ #!/usr/bin/env python """ Module containing tests for Topical util functions """ # IMPORTS ##################################################################### import datetime import pytest from instruments.toptica import toptica_utils # TESTS ####################################################################### def test_convert_boolean(): assert toptica_utils.convert_toptica_boolean("bloof") is False assert toptica_utils.convert_toptica_boolean("boot") is True assert toptica_utils.convert_toptica_boolean("Error: -3") is None def test_convert_boolean_value(): with pytest.raises(ValueError): toptica_utils.convert_toptica_boolean("blo") def test_convert_toptica_datetime(): blo = datetime.datetime.now() blo_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") assert toptica_utils.convert_toptica_datetime('""\r') is None blo2 = toptica_utils.convert_toptica_datetime(blo_str) diff = blo - blo2 assert diff.seconds < 60 ================================================ FILE: tests/test_util_fns.py ================================================ #!/usr/bin/env python """ Module containing tests for util_fns.py """ # IMPORTS #################################################################### from enum import Enum 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, setattr_expression, string_property, unitful_property, unitless_property, ) from tests import unit_eq # 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_limited = unitful_property( "42", u.m, valid_range=(1 * u.m, 100 * u.m) ) unitful_property_limited_numbers = unitful_property( "42", u.m, valid_range=(1, 100.0) ) unitful_property = unitful_property("42", u.m) string_property = string_property("'STRING'") return Inst() # TEST CASES ################################################################# # pylint: disable=protected-access,missing-docstring,redefined-outer-name def test_ProxyList_basics(): class ProxyChild: def __init__(self, parent, name): self._parent = parent self._name = name parent = object() proxy_list = ProxyList(parent, ProxyChild, range(10)) child = proxy_list[0] assert child._parent is parent assert child._name == 0 def test_ProxyList_valid_range_is_enum(): class ProxyChild: def __init__(self, parent, name): self._parent = parent self._name = name class MockEnum(Enum): a = "aa" b = "bb" 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[MockEnum.a]._name == MockEnum.a.value def test_ProxyList_length(): class ProxyChild: def __init__(self, parent, name): self._parent = parent self._name = name parent = object() proxy_list = ProxyList(parent, ProxyChild, range(10)) assert len(proxy_list) == 10 def test_ProxyList_iterator(): class ProxyChild: def __init__(self, parent, name): self._parent = parent self._name = name parent = object() proxy_list = ProxyList(parent, ProxyChild, range(10)) i = 0 for item in proxy_list: assert item._name == i i = i + 1 def test_ProxyList_invalid_idx_enum(): with pytest.raises(IndexError): class ProxyChild: def __init__(self, parent, name): self._parent = parent self._name = name class MockEnum(Enum): a = "aa" b = "bb" parent = object() proxy_list = ProxyList(parent, ProxyChild, MockEnum) _ = proxy_list["c"] # Should raise IndexError def test_ProxyList_invalid_idx(): with pytest.raises(IndexError): class ProxyChild: def __init__(self, parent, name): self._parent = parent self._name = name parent = object() proxy_list = ProxyList(parent, ProxyChild, range(5)) _ = proxy_list[10] # Should raise IndexError @pytest.mark.parametrize( "input, out", ( (1, u.Quantity(1, "m")), (5 * u.mm, u.Quantity(5, "mm")), ("7.3 km", u.Quantity(7.3, "km")), ("7.5", u.Quantity(7.5, u.m)), (u.Quantity(9, "nm"), 9 * u.nm), ), ) def test_assume_units_correct(input, out): unit_eq(assume_units(input, "m"), out) 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() class Test_unitful_property: def test_unitful_property_sendcmd_query(self, mock_inst): """Assert that unitful_property calls sendcmd, query of parent class.""" # getter assert mock_inst.unitful_property == u.Quantity(42, u.m) mock_inst.spy_query.assert_called() # setter value = 13 mock_inst.unitful_property = u.Quantity(value, u.m) assert mock_inst._sendcmd == f"42 {value:e}" mock_inst.spy_sendcmd.assert_called() def test_unitful_property_sendcmd_query_unitless(self, mock_inst): """Assert that unitful_property calls sendcmd, query of parent class. Here for a unitless input """ # getter assert mock_inst.unitful_property == u.Quantity(42, u.m) mock_inst.spy_query.assert_called() # setter value = 13 mock_inst.unitful_property = value assert mock_inst._sendcmd == f"42 {value:e}" mock_inst.spy_sendcmd.assert_called() @pytest.mark.parametrize("value", (0.1, 200, 0.1 * u.m, 200 * u.m)) def test_unitful_property_sendcmd_limited_unfit(self, mock_inst, value): """Assert that unitful_property calls sendcmd, query of parent class. Here an input out of bounds for quantity limited property.""" # setter with pytest.raises(ValueError): mock_inst.unitful_property_limited = value @pytest.mark.parametrize("value", (13 * u.m, 17 * u.m, 55 * u.m)) def test_unitful_property_sendcmd_limited_pass_un(self, mock_inst, value): """Assert that unitful_property calls sendcmd, query of parent class. Here a quantity input fit for quantity limited property.""" # setter mock_inst.unitful_property_limited = value assert mock_inst._sendcmd == f"42 {value.magnitude:e}" mock_inst.spy_sendcmd.assert_called() @pytest.mark.parametrize("value", (13, 17.0, 55.5, 99)) def test_unitful_property_sendcmd_limited_pass_ul(self, mock_inst, value): """Assert that unitful_property calls sendcmd, query of parent class. Here a numbers input fit for quantity limited property.""" # setter mock_inst.unitful_property_limited = value assert mock_inst._sendcmd == f"42 {value:e}" mock_inst.spy_sendcmd.assert_called() @pytest.mark.parametrize("value", (0.1, 200, 0.1 * u.m, 200 * u.m)) def test_unitful_property_sendcmd_limited_unfit2(self, mock_inst, value): """Assert that unitful_property calls sendcmd, query of parent class. Here an input out of numbered bounds for limited property.""" # setter with pytest.raises(ValueError): mock_inst.unitful_property_limited_numbers = value @pytest.mark.parametrize("value", (13 * u.m, 17 * u.m, 55 * u.m)) def test_unitful_property_sendcmd_limited_pass_un2(self, mock_inst, value): """Assert that unitful_property calls sendcmd, query of parent class. Here a quantity input fit for numbers limited property.""" # setter mock_inst.unitful_property_limited_numbers = value assert mock_inst._sendcmd == f"42 {value.magnitude:e}" mock_inst.spy_sendcmd.assert_called() @pytest.mark.parametrize("value", (13, 17.0, 55.5, 99)) def test_unitful_property_sendcmd_limited_pass_ul2(self, mock_inst, value): """Assert that unitful_property calls sendcmd, query of parent class. Here a numbers input fit for numbers limited property.""" # setter mock_inst.unitful_property_limited_numbers = value 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() ================================================ FILE: tests/test_yokogawa/__init__.py ================================================ ================================================ FILE: tests/test_yokogawa/test_yokogawa7651.py ================================================ #!/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 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 ================================================ FILE: tests/test_yokogawa/test_yokogawa_6370.py ================================================ #!/usr/bin/env python """ Unit tests for the Yokogawa 6370 """ # IMPORTS ##################################################################### import struct from hypothesis import ( given, strategies as st, ) import socket import instruments as ik from instruments.optional_dep_finder import numpy from tests import ( expected_protocol, iterable_eq, pytest, ) from instruments.units import ureg as u from .. import mock # TESTS ####################################################################### def test_channel_is_channel_class(): inst = ik.yokogawa.Yokogawa6370.open_test() assert inst._channel_count == len(inst.Traces) assert isinstance(inst.channel["A"], inst.Channel) is True def test_init(): with expected_protocol(ik.yokogawa.Yokogawa6370, [":FORMat:DATA REAL,64"], []) as _: pass def test_id(): with expected_protocol( ik.yokogawa.Yokogawa6370, [":FORMat:DATA REAL,64", "*IDN?"], ["'YOKOGAWA,AQ6370D,x,02.08'"], ) as inst: assert inst.id == "YOKOGAWA,AQ6370D,x,02.08" @mock.patch("instruments.abstract_instruments.instrument.socket") def test_tcpip_connection_terminator(mock_socket): """Ensure terminator is `\r\n` if connected via TCP-IP (issue #386).""" mock_socket.socket.return_value.__class__ = socket.socket inst = ik.yokogawa.Yokogawa6370.open_tcpip("127.0.0.1", port=1001) assert inst.terminator == "\r\n" @mock.patch("instruments.abstract_instruments.instrument.socket") def test_tcpip_authentication(mock_socket, mocker): mock_socket.socket.return_value.__class__ = socket.socket call_order = [] mock_query = mocker.patch("instruments.yokogawa.Yokogawa6370.query") mock_sendcmd = mocker.patch("instruments.yokogawa.Yokogawa6370.sendcmd") def query_return(*args, **kwargs): """Return results and add to `call_order`.""" call_order.append(mock_query) return "ready" mock_query.side_effect = query_return mock_sendcmd.side_effect = lambda *a, **kw: call_order.append(mock_sendcmd) username = "user" password = "my_password" _ = ik.yokogawa.Yokogawa6370.open_tcpip( "127.0.0.1", 1234, auth=(username, password) ) calls = [ mocker.call(f'OPEN "{username}"'), mocker.call(f'"{password}"'), ] mock_query.assert_has_calls(calls, any_order=False) assert call_order == [mock_query, mock_query, mock_sendcmd] @mock.patch("instruments.abstract_instruments.instrument.socket") def test_tcpip_authentication_anonymous(mock_socket, mocker): """Authenticate as anonymous user (any password accepted).""" mock_socket.socket.return_value.__class__ = socket.socket call_order = [] mock_query = mocker.patch("instruments.yokogawa.Yokogawa6370.query") mock_sendcmd = mocker.patch("instruments.yokogawa.Yokogawa6370.sendcmd") def query_return(*args, **kwargs): """Return results and add to `call_order`.""" call_order.append(mock_query) return "ready" mock_query.side_effect = query_return mock_sendcmd.side_effect = lambda *a, **kw: call_order.append(mock_sendcmd) username = "anonymous" password = "my_password" _ = ik.yokogawa.Yokogawa6370.open_tcpip( "127.0.0.1", 1234, auth=(username, password) ) calls = [ mocker.call(f'OPEN "{username}"'), mocker.call(f'"{password}"'), # this is the password since any is accepted ] mock_query.assert_has_calls(calls, any_order=False) assert call_order == [mock_query, mock_query, mock_sendcmd] @mock.patch("instruments.abstract_instruments.instrument.socket") def test_tcpip_authentication_error(mock_socket, mocker): mock_socket.socket.return_value.__class__ = socket.socket mock_query = mocker.patch("instruments.yokogawa.Yokogawa6370.query") mock_query.side_effect = ["asdf", "asdf", "error"] # three calls total username = "user" password = "my_password" with pytest.raises(ConnectionError): _ = ik.yokogawa.Yokogawa6370.open_tcpip( "127.0.0.1", 1234, auth=(username, password) ) def test_status(): with expected_protocol( ik.yokogawa.Yokogawa6370, [":FORMat:DATA REAL,64", "*STB?"], ["7"] ) as inst: assert inst.status == 7 def test_operation_event(): with expected_protocol( ik.yokogawa.Yokogawa6370, [":FORMat:DATA REAL,64", ":status:operation:event?"], ["7"], ) as inst: assert inst.operation_event == 7 @pytest.mark.parametrize("axis", ("X", "Y")) @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_private_data_wo_limits(values, channel, axis): values_packed = b"".join(struct.pack("