From 4c2f4464194555b032d025b5625114315df63d41 Mon Sep 17 00:00:00 2001 From: Andreas Svela Date: Thu, 17 Dec 2020 12:46:07 +0100 Subject: [PATCH 01/52] WIP Major update across nearly all files, see changelog --- README.rst | 81 +- docs/changelog.rst | 165 +++- docs/conf.py | 23 +- docs/contents/auxiliary.rst | 6 + docs/contents/commandline-programmes.rst | 10 +- docs/contents/dataprocessing.rst | 31 +- docs/contents/osc-class.rst | 32 +- docs/contents/overview.rst | 41 +- docs/contents/usage.rst | 108 ++- docs/index.rst | 12 +- docs/known-issues.rst | 18 +- docs/requirements.txt | 3 +- keyoscacquire/VERSION | 2 +- keyoscacquire/__init__.py | 6 +- keyoscacquire/auxiliary.py | 70 ++ keyoscacquire/config.py | 2 +- keyoscacquire/installed_cli_programmes.py | 21 +- keyoscacquire/oscacq.py | 874 +++++++++++----------- keyoscacquire/programmes.py | 149 ++-- keyoscacquire/scripts/example.py | 5 +- keyoscacquire/traceio.py | 208 +++++ tests/format_comparison.py | 8 +- 22 files changed, 1210 insertions(+), 665 deletions(-) create mode 100644 docs/contents/auxiliary.rst create mode 100644 keyoscacquire/auxiliary.py create mode 100644 keyoscacquire/traceio.py diff --git a/README.rst b/README.rst index a56f5eb..49f7215 100644 --- a/README.rst +++ b/README.rst @@ -1,26 +1,83 @@ keyoscacquire: Keysight oscilloscope acquire ============================================ -keyoscacquire is a Python package for acquiring traces from Keysight InfiniiVision oscilloscopes through a VISA interface. +keyoscacquire is a Python package for acquiring traces from Keysight +InfiniiVision oscilloscopes through a VISA interface. -Based on `PyVISA `_, keyoscacquire provides programmes for acquiring and exporting traces to your choice of ASCII format files (default csv) or `numpy `_ npy, and a png of the trace plot. The package provides a class ``Oscilloscope`` and data processing functions that can be used in other scripts. For example, to capture the active channels on an oscilloscope connected with VISA address ``USB0::1234::1234::MY1234567::INSTR`` from command prompt:: +Based on `PyVISA `_, keyoscacquire +provides programmes for acquiring and exporting traces to your choice of ASCII +format files (default csv) or numpy `npy `_, +and a png of the trace plot. The package also provides an API for integration +in other Python code. - get_single_trace -v USB0::1234::1234::MY1234567::INSTR +keyoscacquire uses the :py:mod:`logging` module, see :ref:`logging`. -or in the python console:: +The code has been tested on Windows 7 and 10 with a Keysight DSO2024A model +using a USB connection. + +.. note:: In order to connect to a VISA instrument, NI MAX or similar might + need to be running on the computer. Installation of Keysight Connection + Expert might also be necessary. + +.. command-line-use-marker + +Command line use +---------------- + +Capture the active channels on an oscilloscope connected with VISA address +from command prompt + +.. prompt:: bash + + get_single_trace -v "USB0::1234::1234::MY1234567::INSTR" + +The ``get_single_trace`` programme takes several other arguments too, see them with + +.. prompt:: bash + + get_single_trace -h + + +If you need to find the VISA address of your oscilloscope, simply use the +command line programme ``list_visa_devices`` provided by this package + +.. prompt:: bash + + list_visa_devices + +The package installs the following command line programmes in the Python path + +* ``list_visa_devices``: list the available VISA devices +* ``path_of_config``: find the path of :mod:`keyoscacquire.config` + storing default options. Change this file to your choice of standard + settings, see :ref:`default-options`. +* ``get_single_trace``: use with option ``-h`` for instructions +* ``get_num_traces``: get a set number of traces, use with + option ``-h`` for instructions +* ``get_traces_single_connection``: get a trace each time enter is + pressed, use with option ``-h`` for instructions + +See more under :ref:`cli-programmes-short`. + + +Python console/API +------------------ + +In the Python console:: >>> import keyoscacquire.oscacq as koa >>> osc = koa.Oscilloscope(address='USB0::1234::1234::MY1234567::INSTR') >>> time, y, channel_numbers = osc.set_options_get_trace() + >>> osc.close() -where ``time`` is a vertical numpy vector of time values and ``y`` is a numpy array which columns contain the data from the active channels listed in ``channel_numbers``. - -If you need to find the VISA address of your oscilloscope, use the command line programme ``list_visa_devices`` provided by this package. - -The code has been tested on Windows 7 and 10 with a Keysight DSO2024A model using a USB connection. +where ``time`` is a vertical numpy (2D) array of time values and ``y`` is a numpy +array which columns contain the data from the active channels listed in +``channel_numbers``. -.. note:: In order to connect to a VISA instrument, NI MAX or similar might need to be running on the computer. Installation of Keysight Connection Expert might also be necessary. +Explore the Reference section (particularly :ref:`osc-class`) to get more +information about the API. +.. documentation-marker Documentation ------------- @@ -31,4 +88,6 @@ Available at `keyoscacquire.rtfd.io `_. The package is written and maintained by Andreas Svela. +Contributions are welcome, find the project on +`github `_. +The package is written and maintained by Andreas Svela. diff --git a/docs/changelog.rst b/docs/changelog.rst index 5279cf1..90ec61c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,9 +1,96 @@ Changelog ========= +v4.0: Extreme (API) makeover +---------------------------- +Big makeover with many no compatible changes. When writing the base of this back +in 2019 I had very limited Python development experience, so it was time to make +a few better choices now to make the API easier to use. + +That means that there are quite a few non-compatible changes to previous versions, +all of which are detailed below. + +v4.0.0 (2020-12) + - More attributes are used to make the information accessible not only through returns + + * Captured data stored to ``Oscilloscope.time`` and ``Oscilloscope.y`` + * Fname used (not the argument as it might be updated duing saving process) + stored in ``Oscilloscope.fname`` + * ``Oscilloscope.raw`` and ``Oscilloscope.metadata`` are now available + + - More active use of attributes that are carried forward rather than always + setting the arguments of methods in the ``Oscilloscope`` class. This + affects some functions as their arguments have changed (see below), but + for most functions the arguments stay the same as before. The arguments + can now be used to change attributes of the ``Oscilloscope`` instance. + + - Bugfixes and docfixes for the number of points to be transferred from the + instrument (``num_points`` argument). Zero will set the to the + maximum number of points available. + + - New ``keyoscacquire.traceio.load_trace()`` function for loading saved a trace + + - Moved save and plot functions to ``keyoscacquire.traceio``, but are imported + in ``oscacq`` to keep compatibility + + - ``Oscilloscope.read_and_capture()`` will now try to read the error from the + instrument if pyvisa fails + + - Importing ``keyoscacquire.programmes`` in module ``init.py`` to make it accessible + + - Changes in ``list_visa_devices`` and cli programme: now displaying different + errors more clearly; cli programme now has ``-n`` flag that can be set to not + ask for instrument IDNs; and the cli programme will display the instrument's + firmware rather than Keysight model series. + + - Indicating functions for internal and external use by prefix ``_`` + + - Documentation updates, including moving from read-the-docs theme to Furo theme + + - PEP8 improvements + + - *(New methods)*: + + * ``Oscilloscope.get_error()`` + * ``Oscilloscope.set_waveform_export_options()`` + * ``Oscilloscope.save_trace()`` (``Oscilloscope.savepng`` and + ``Oscilloscope.showplot`` can be set to contol its behaviour) + * ``Oscilloscope.plot_trace()`` + + - *No compatibility*: Several functions no longer take ``sources`` and + ``sourcesstring`` as arguments, rather ``Oscilloscope.sources`` and + ``Oscilloscope.sourcesstring`` must be set by + ``Oscilloscope.set_channels_for_capture()`` + + * ``Oscilloscope.capture_and_read()`` and its ``Oscilloscope._read_ascii()`` + and ``Oscilloscope._read_binary()`` + * ``Oscilloscope.get_trace()`` + + - *No compatibility*: Name changes + + * ``Oscilloscope.determine_channels()`` -> ``Oscilloscope.set_channels_for_capture()`` + * ``Oscilloscope.acquire_print`` -> ``Oscilloscope.verbose_acquistion`` + * ``Oscilloscope.set_acquire_print()`` set ``Oscilloscope.verbose_acquistion`` + attribute instead + * ``Oscilloscope.capture_and_read_ascii()`` -> ``Oscilloscope._read_ascii()`` + (also major changes in the function) + * ``Oscilloscope.capture_and_read_binary()`` -> ``Oscilloscope._read_binary()`` + (also major changes in the function) + + - *No compatibility*: Moved functions + + * ``interpret_visa_id()`` from ``oscacq`` to ``auxiliary`` + * ``check_file()`` from ``oscacq`` to ``auxiliary`` + + - *No compatibility*: ``Oscilloscope.get_trace()`` now also returns + also ``Oscilloscope.num_channels`` + + + v3.0: Docs are overrated ------------------------ -Comprehensive documentation now available on read the docs, added more command line programme options, some function name changes without compatibility, and bug fixes. +Comprehensive documentation now available on read the docs, added more command +line programme options, some function name changes without compatibility, and bug fixes. v3.0.2 (2020-02-10) - Context manager compatibility (``__enter__`` and ``__exit__`` functions implemented) @@ -18,61 +105,63 @@ v3.0.1 (2019-10-31) v3.0.0 (2019-10-28) - Expanded command line programmes to take many more options: - * *Connection settings*: visa_address, timeout - * *Acquiring settings*: channels, acq_type - * *Transfer and storage settings*: wav_format, num_points, filename, file_delimiter + - *Connection settings*: visa_address, timeout + - *Acquiring settings*: channels, acq_type + - *Transfer and storage settings*: wav_format, num_points, filename, file_delimiter - Added ``Oscilloscope.generate_file_header()`` to generate file header with structure:: - - , - - time, + + , + + time, Now used by ``save_trace()`` - - *(No compatibility measures introduced)*: Camel case in function names is no more + - *No compatibility*: Camel case in function names is no more * ``getTrace`` -> ``get_trace`` * ``saveTrace`` -> ``save_trace`` * ``plotTrace`` -> ``plot_trace`` * and others - - *(No compatibility measures introduced)*: ``Oscilloscope.build_sourcesstring()`` -> ``Oscilloscope.determine_channels()`` and changed return sequence + - *No compatibility*: ``Oscilloscope.build_sourcesstring()`` -> + ``Oscilloscope.determine_channels()`` and changed return sequence - - *(No compatibility measures introduced)*: module ``installed_commandline_funcs`` -> ``installed_cli_programmes`` + - *No compatibility*: module ``installed_commandline_funcs`` -> ``installed_cli_programmes`` - - *(No compatibility measures introduced)*: functions ending with ``_command_line()`` -> ``_cli()`` + - *No compatibility*: functions ending with ``_command_line()`` -> ``_cli()`` - Fixed issue when setting number of points to transfer - - Fixed issue (hopefully) with sometimes getting wrong traces exported. Have now set communication to signed ints, and setting least significant bit first + - Fixed issue (hopefully) with sometimes getting wrong traces exported. Have + now set communication to signed ints, and setting least significant bit first - Fixed issue where ``ASCii`` wave format would set zero time to the beginning of the trace - Wrote comprehensive documentation on read the docs + v2.1: May I have your address? ------------------------------ New command line programmes for listing visa devices and finding config v2.1.0 (2019-10-18) - - Added command line programme ``list_visa_devices`` to list the addresses of the VISA instruments available - - - Added command line programme ``path_of_config`` to show the path of config.py - - - Explicitly setting scope to transfer in unsigned ints when doing ``BYTE`` and ``WORD`` waveform formats - - - Added functions for setting oscilloscope to running and stopped, and for direct VISA command write and query - - - Changed dependency from visa to pyvisa (the package called visa on pypi is not pyvisa..!), and added tqdm dependency - - - *(No compatibility measures introduced)*: ``get_n_traces`` now called ``get_num_traces`` - + - Added command line programme ``list_visa_devices`` to list the addresses + of the VISA instruments available + - Added command line programme ``path_of_config`` to show the path of ``config.py`` + - Explicitly setting scope to transfer in unsigned ints when doing ``BYTE`` + and ``WORD`` waveform formats + - Added functions for setting oscilloscope to running and stopped, and for + direct VISA command write and query + - Changed dependency from visa to pyvisa (the package called visa on pypi is + not pyvisa..!), and added tqdm dependency + - *No compatibility*: ``get_n_traces`` now called ``get_num_traces`` - And minor cosmetic changes + v2.0: Labels for everyone ------------------------- @@ -81,32 +170,32 @@ v2.0.1 (2019-09-13) v2.0.0 (2019-08-29) - - When using ``Oscilloscope.set_options_get_trace_save()``, channels are now comma separated in the csv to provide channel headings according to the data columns. This is not directly compatible with previous versions as these had two lines of preamble in csvs, whereas it is now three (Instrument info, columns descriptions, date and time) - + - When using ``Oscilloscope.set_options_get_trace_save()``, channels are now + comma separated in the csv to provide channel headings according to the data + columns. This is not directly compatible with previous versions as these had + two lines of preamble in csvs, whereas it is now three (Instrument info, + columns descriptions, date and time) - Added BYTE/WORD issue to README + v1.1: Need for speed -------------------- +Order of magnitude speed-up in data processing, logging enabled, new command +line programme v1.1.1 (2019-08-14) - Logging gives elapsed time in milliseconds - - Change in logging level for elapsed time v1.1.0 (2019-04-04) - Extra command line programme, logging enabled, order of magnitude speed-up in data processing - - - Added command line programme for obtaining a given number of traces consecutively - - - Former debugging print is now directed to ``logging.debug()`` - - - ``Oscilloscope.process_data_binary()`` is approx an order of magnitude faster - - - Added license file + - Added command line programme for obtaining a given number of traces consecutively + - Former debugging print is now directed to ``logging.debug()`` + - ``Oscilloscope.process_data_binary()`` is approx an order of magnitude faster + - Added license file + - Changes in README - - Changes in README v1.0: Hello world diff --git a/docs/conf.py b/docs/conf.py index bdd1bcf..ae39d28 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -21,14 +21,15 @@ # -- Project information ----------------------------------------------------- -project = 'Keysight oscilloscope acquire' -copyright = '2019, Andreas Svela' +project = 'keyoscacquire'#'Keysight oscilloscope acquire' +copyright = '2019-2020, Andreas Svela' author = 'Andreas Svela' # The full version, including alpha/beta/rc tags version = ver release = version +html_title = f"{project} v{version}" # -- General configuration --------------------------------------------------- @@ -52,7 +53,8 @@ intersphinx_mapping = { 'python': ('https://docs.python.org/3', None), 'pyvisa': ('https://pyvisa.readthedocs.io/en/latest/', None), - 'numpy': ('https://docs.scipy.org/doc/numpy/', None)} + 'numpy': ('https://docs.scipy.org/doc/numpy/', None), + 'pandas': ('https://pandas.pydata.org/pandas-docs/stable/', None)} # autodoc_default_options = { # 'special-members': '__init__', # } @@ -73,12 +75,19 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -# + +# Uncomment for standard theme # html_theme = 'alabaster' -import sphinx_rtd_theme -html_theme = "sphinx_rtd_theme" +# Uncomment for +# import furo +html_theme = "furo" + +# Uncomment for read the docs theme +# import sphinx_rtd_theme +# html_theme = "sphinx_rtd_theme" +# Uncomment for Guzzle theme # import guzzle_sphinx_theme # html_theme_path = guzzle_sphinx_theme.html_theme_path() # html_theme = 'guzzle_sphinx_theme' @@ -94,4 +103,4 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +# html_static_path = ['_static'] diff --git a/docs/contents/auxiliary.rst b/docs/contents/auxiliary.rst new file mode 100644 index 0000000..9b639bd --- /dev/null +++ b/docs/contents/auxiliary.rst @@ -0,0 +1,6 @@ +Auxiliary module :mod:`~keyoscacquire.auxiliary` +************************************************* + +.. automodule:: keyoscacquire.auxiliary + :members: + :private-members: diff --git a/docs/contents/commandline-programmes.rst b/docs/contents/commandline-programmes.rst index 3d6724f..555da28 100644 --- a/docs/contents/commandline-programmes.rst +++ b/docs/contents/commandline-programmes.rst @@ -7,9 +7,12 @@ Command line programmes
-keyoscacquire installs command line programmes to find VISA devices, find the path of the :mod:`~keyoscacquire.config` file and obtain single or multiple traces. +keyoscacquire installs command line programmes to find VISA devices, find the +path of the :mod:`~keyoscacquire.config` file and obtain single or multiple traces. -For all the trace-obtaining programmes, the filename is checked to ensure no overwrite, if a file exists from before the programme prompts for suffix to the filename. The filename is recursively checked after appending. +For all the trace-obtaining programmes, the filename is checked to ensure no +overwrite, if a file exists from before the programme prompts for suffix to the +filename. The filename is recursively checked after appending. The file header in the ascii files saved is:: @@ -18,7 +21,8 @@ The file header in the ascii files saved is:: time, -Where ```` is the :attr:`~keyoscacquire.oscacq.Oscilloscope.id` of the oscilloscope, and ```` are the comma separated channels used. For example:: +Where ```` is the :attr:`~keyoscacquire.oscacq.Oscilloscope.id` of the +oscilloscope, and ```` are the comma separated channels used. For example:: # AGILENT TECHNOLOGIES,DSO-X 2024A,MY1234567,12.34.1234567890 # AVER8 diff --git a/docs/contents/dataprocessing.rst b/docs/contents/dataprocessing.rst index 51475e2..d6e6f76 100644 --- a/docs/contents/dataprocessing.rst +++ b/docs/contents/dataprocessing.rst @@ -1,28 +1,33 @@ .. _data-proc: -Data processing and file saving -******************************* +Data processing, file saving & loading +************************************** .. py:currentmodule:: keyoscacquire.oscacq -The :mod:`keyoscacquire.oscacq` module contains function for processing the raw data captured with :class:`Oscilloscope`, and for saving the processed data to files and plots. +The :mod:`keyoscacquire.oscacq` module contains function for processing +the raw data captured with :class:`Oscilloscope`, and :mod:`keyoscacquire.traceio` +for saving the processed data to files and plots. Data processing --------------- -The output from the :func:`Oscilloscope.capture_and_read` function is processed by :func:`process_data`, a wrapper function that sends the data to the respective binary or ascii processing function. +The output from the :func:`Oscilloscope.capture_and_read` function is processed +by :func:`process_data`, a wrapper function that sends the data to the +respective binary or ascii processing function. .. autofunction:: process_data -.. autofunction:: process_data_binary -.. autofunction:: process_data_ascii -File saving ------------ +File saving (:mod:`keyoscacquire.traceio`) +------------------------------------------ -The package has built-in functions for saving traces to :mod:`numpy.lib.format` files or ascii values (the latter is slower but will give a header that can be customised, for instance :func:`Oscilloscope.generate_file_header` can be used). +The package has built-in functions for saving traces to npy format +(see :mod:`numpy.lib.format`) files or ascii values (the latter is slower but will +give a header that can be customised, :func:`Oscilloscope.generate_file_header` +is used by default). -.. autofunction:: save_trace -.. autofunction:: save_trace_npy -.. autofunction:: plot_trace -.. autofunction:: check_file +.. autofunction:: keyoscacquire.traceio.save_trace +.. autofunction:: keyoscacquire.traceio.plot_trace +.. autofunction:: keyoscacquire.traceio.load_trace +.. autofunction:: keyoscacquire.traceio.load_header diff --git a/docs/contents/osc-class.rst b/docs/contents/osc-class.rst index e354011..6536aeb 100644 --- a/docs/contents/osc-class.rst +++ b/docs/contents/osc-class.rst @@ -4,8 +4,8 @@ Instrument communication: The Oscilloscope class ************************************************ -Class API -========= +Oscilloscope API +================ .. py:currentmodule:: keyoscacquire.oscacq @@ -15,8 +15,6 @@ Class API Auxiliary to the class ====================== -.. autofunction:: interpret_visa_id - .. autodata:: _supported_series .. autodata:: _screen_colors .. autodata:: _datatypes @@ -27,15 +25,17 @@ Auxiliary to the class The preamble ============ -The preamble returned by the capture_and_read functions (i.e. returned by the oscilloscope when querying the VISA command ``:WAV:PREamble?``) is a string of comma separated values, the values have the following meaning: - - 0. FORMAT : int16 - 0 = BYTE, 1 = WORD, 4 = ASCII. - 1. TYPE : int16 - 0 = NORMAL, 1 = PEAK DETECT, 2 = AVERAGE - 2. POINTS : int32 - number of data points transferred. - 3. COUNT : int32 - 1 and is always 1. - 4. XINCREMENT : float64 - time difference between data points. - 5. XORIGIN : float64 - always the first data point in memory. - 6. XREFERENCE : int32 - specifies the data point associated with x-origin. - 7. YINCREMENT : float32 - voltage diff between data points. - 8. YORIGIN : float32 - value is the voltage at center screen. - 9. YREFERENCE : int32 - specifies the data point where y-origin occurs. +The preamble returned by the capture_and_read functions (i.e. returned by the +oscilloscope when querying the VISA command ``:WAV:PREamble?``) is a string of +comma separated values, the values have the following meaning:: + + 0. FORMAT : int16 - 0 = BYTE, 1 = WORD, 4 = ASCII. + 1. TYPE : int16 - 0 = NORMAL, 1 = PEAK DETECT, 2 = AVERAGE + 2. POINTS : int32 - number of data points transferred. + 3. COUNT : int32 - 1 and is always 1. + 4. XINCREMENT : float64 - time difference between data points. + 5. XORIGIN : float64 - always the first data point in memory. + 6. XREFERENCE : int32 - specifies the data point associated with x-origin. + 7. YINCREMENT : float32 - voltage diff between data points. + 8. YORIGIN : float32 - value is the voltage at centre of the screen. + 9. YREFERENCE : int32 - specifies the data point where y-origin occurs. diff --git a/docs/contents/overview.rst b/docs/contents/overview.rst index 7b6732e..404987c 100644 --- a/docs/contents/overview.rst +++ b/docs/contents/overview.rst @@ -2,40 +2,47 @@ Overview and getting started **************************** -The code is structured as a module :mod:`keyoscacquire.oscacq` containing the engine doing the `PyVISA `_ interfacing in a class :class:`~keyoscacquire.oscacq.Oscilloscope`, and support functions for data processing and saving. Programmes are located in :mod:`keyoscacquire.programmes`, and the same programmes can be run directly from the command line as they are installed in the python path, see :ref:`cli-programmes`. Default options are found in :mod:`keyoscacquire.config`. +The code is structured as a module :mod:`keyoscacquire.oscacq` containing the +engine doing the `PyVISA `_ +interfacing in a class :class:`~keyoscacquire.oscacq.Oscilloscope`, and +support functions for data processing. Programmes are located +in :mod:`keyoscacquire.programmes`, and the same programmes can be run +directly from the command line as they are installed in the Python path, +see :ref:`cli-programmes`. Default options are found in :mod:`keyoscacquire.config`. Quick reference =============== -The package installs the following command line programmes in the python path +.. include:: ../../README.rst + :start-after: command-line-use-marker + :end-before: documentation-marker -* :program:`list_visa_devices`: list the available VISA devices -* :program:`path_of_config`: find the path of :mod:`keyoscacquireuire.config` storing default options. Change this file to your choice of standard settings, see :ref:`default-options`. - -* :program:`get_single_trace`: use with option ``-h`` for instructions - -* :program:`get_num_traces`: get a set number of traces, use with option ``-h`` for instructions +Installation +============ -* :program:`get_traces_single_connection`: get a trace each time enter is pressed, use with option ``-h`` for instructions +Install the package with pip -See more under :ref:`cli-programmes-short`. +.. prompt:: bash -.. todo:: Add info about API + pip install keyoscacquire -:mod:`keyoscacquire` uses the :py:mod:`logging` module, see :ref:`logging`. +or download locally and install with ``$ python setup.py install`` or +by running ``install.bat``. -Installation -============ -Install the package with pip:: +Building the docs +----------------- +To build a local copy of the sphinx docs make sure the necessary packages +are installed - $ pip install keysightoscilloscopeacquire +.. prompt:: bash + pip install sphinx sphinx-prompt furo recommonmark -or download locally and install with ``$ python setup.py install`` or by running ``install.bat``. +Then build by executing ``make html`` in the ``docs`` folder. .. include:: ../known-issues.rst diff --git a/docs/contents/usage.rst b/docs/contents/usage.rst index fa7dc71..37153d7 100644 --- a/docs/contents/usage.rst +++ b/docs/contents/usage.rst @@ -2,41 +2,58 @@ How to use ********** -The VISA addresses of connected instruments can be found with ``$ list_visa_devices`` in the command line, or can be found in NI MAX or the `PyVISA shell `_. The address should be set as the :data:`~keyoscacquire.config._visa_address` in :mod:`keyoscacquire.config` +The VISA addresses of connected instruments can be found running ``list_visa_devices`` +in cmd or the terminal (this programme is installed with keyoscacquire), +or can be found in NI MAX or the +`PyVISA shell `_. -.. note:: In order to connect to a VISA instrument, NI MAX or similar might need to be running on the computer. +Setting the address of your oscilloscope in +:data:`~keyoscacquire.config._visa_address` in :mod:`keyoscacquire.config` +will make all the installed command line programmes talk to your by default. +The config file can be found with cmd/terminal by ``path_of_config`` (see :ref:`default-options`). + +.. note:: In order to connect to a VISA instrument, NI MAX or similar might + need to be running on the computer. .. _cli-programmes-short: Command line programmes for trace export ======================================== -Four command line programmes for trace exporting can be ran directly from the command line after installation (i.e. from whatever folder and no need for ``$ python [...].py``): +Four command line programmes for trace exporting can be ran directly from the +command line after installation (i.e. from whatever folder and no need for +``$ python [...].py``): * :program:`get_single_trace` - * :program:`get_num_traces` - * :program:`get_traces_connect_each_time` and - * :program:`get_traces_single_connection` They all have options, the manuals are available using the flag ``-h``. -The two first programmes will obtain one and a specified number of traces, respectively. The two latter programmes are loops for which every time ``enter`` is hit a trace will be obtained and exported as csv and png files with successive numbering. By default all active channels on the oscilloscope will be captured (this can be changed, see :ref:`default-options`). +The two first programmes will obtain one and a specified number of traces, +respectively. The two latter programmes are loops for which every time ``enter`` +is hit a trace will be obtained and exported as csv and png files with successive +numbering. By default all active channels on the oscilloscope will be captured +(this can be changed, see :ref:`default-options`). -The difference between the two latter programmes is that the first programme is establishing a new connection to the instrument each time a trace is to be captured, whereas the second opens a connection to start with and does not close the connection until the program is quit. The second programme only checks which channels are active when it connects, i.e. the first programme will save only the currently active channels for each saved trace; the second will each time save the channels that were active at the time of starting the programme. +The difference between the two latter programmes is that the first programme is +establishing a new connection to the instrument each time a trace is to be captured, +whereas the second opens a connection to start with and does not close the +connection until the program is quit. The second programme only checks which +channels are active when it connects, i.e. the first programme will save only +the currently active channels for each saved trace; the second will each time +save the channels that were active at the time of starting the programme. Optional command line arguments ------------------------------- -The programmes takes optional arguments, the manuals are available using the flag ``-h`` (see also :ref:`cli-programmes` for more details). Here are three examples +The programmes takes optional arguments, the manuals are available using the +flag ``-h`` (see also :ref:`cli-programmes` for more details). Here are three examples * ``-v USB0::1234::1234::MY1234567::INSTR`` set the visa address of the instrument - * ``-f "custom filename"`` sets the base filename to "custom filename" - * ``-a AVER8`` sets acquiring type to average with eight traces .. highlight:: console @@ -45,9 +62,14 @@ For example .. prompt:: bash - get_traces_single_connection_loop -f measurement + get_traces_single_connection_loop -f "measurement" -will give output files ``measurement n.csv`` and ``measurement n.png``. The programmes will check if the file ``"measurement"+_file_delimiter+num+_filetype)`` exists, and if it does, prompt the user for something to append to ``measurement`` until ``"measurement"+appended+"0"+_filetype`` is not an existing file. The same checking procedure applies also when no base filename is supplied and ``config._default_filename`` is used. +will give output files ``measurement n.csv`` and ``measurement n.png``. +The programmes will check if the file ``"measurement"+_file_delimiter+num+_filetype)`` +exists, and if it does, prompt the user for something to append to ``measurement`` +until ``"measurement"+appended+"0"+_filetype`` is not an existing file. The same +checking procedure applies also when no base filename is supplied and +``config._default_filename`` is used. .. highlight:: python @@ -58,35 +80,54 @@ Waveform formats The oscilloscope can transfer the waveform to the computer in three different ways * Comma separated ASCII values - * 8-bit integers - * 16-bit integers -Keysight call these ASCii, BYTE and WORD, respectively. The two latter integer types must be post-processed on the computer using a preamble that can be queried for from the ocilloscope. The keyoscacquire package supports all three formats and does the conversion for the integer transfer types, i.e. the output files will be ASCII format anyway, it is simply a question of how the data is transferred to and processed on the computer (see :func:`~keyoscacquire.oscacq.Oscilloscope.capture_and_read` and :func:`~keyoscacquire.oscacq.process_data`). +Keysight call these ASCii, BYTE and WORD, respectively. The two latter integer +types must be post-processed on the computer using a preamble that can be queried +for from the ocilloscope. The keyoscacquire package supports all three formats +and does the conversion for the integer transfer types, i.e. the output files +will be ASCII format anyway, it is simply a question of how the data is +transferred to and processed on the computer +(see :func:`~keyoscacquire.oscacq.Oscilloscope.capture_and_read` and +:func:`~keyoscacquire.oscacq.process_data`). -The 16-bit values format is approximately 10x faster than ascii. and gives the same vertical resolution. 8-bit has significantly lower vertical resolution than the two others, but gives an even higher speed-up. +The 16-bit values format is approximately 10x faster than ascii and gives the +same vertical resolution. 8-bit has significantly lower vertical resolution +than the two others, but gives an even higher speed-up. -The default waveform type can be set in with :const:`~keyoscacquire.config._waveform_format`, see :ref:`default-options`, or using the API :attr:`~keyoscacquire.oscacq.Oscilloscope.wav_format`. +The default waveform type can be set in with +:const:`~keyoscacquire.config._waveform_format`, see :ref:`default-options`, +or using the API :attr:`~keyoscacquire.oscacq.Oscilloscope.wav_format`. Using the API ============= -The package can also be used in python scripts. For example +The package provides an API for use with your Python code. For example .. literalinclude :: ../../keyoscacquire/scripts/example.py + :linenos: + +.. todo :: Expand examples -See :ref:`osc-class` and :ref:`data-proc` for more. The command line programmes have a python backend that can integrated in python scripts or used as examples, see :ref:`py-programmes`. +See :ref:`osc-class` and :ref:`data-proc` for more. The command line programmes +have a python backend that can integrated in Python scripts or used as +examples, see :ref:`py-programmes`. Note on obtaining traces when the scope is running vs when stopped ================================================================== -When the scope **is running** the ``capture_and_read`` functions will obtain a trace by the ``:DIGitize`` VISA command, causing the instrument to acquire a trace and then stop the oscilloscope. When the scope **is stopped** the current trace on the screen of the oscilloscope will be captured. +When the scope **is running** the ``capture_and_read`` functions will obtain a +trace by the ``:DIGitize`` VISA command, causing the instrument to acquire a +trace and then stop the oscilloscope. When the scope **is stopped** the current +trace on the screen of the oscilloscope will be captured. -.. warning:: The settings specified with VISA commands under ``:ACQuire``, i.e. acquiring mode and number of points to be captured, will not be applied to the acquisition if the scope already is stopped while in a different mode. +.. warning:: The settings specified with VISA commands under ``:ACQuire``, i.e. + acquiring mode and number of points to be captured, will not be applied to + the acquisition if the scope already is stopped while in a different mode. The scope will always be set to running after a trace is captured. @@ -96,13 +137,18 @@ The scope will always be set to running after a trace is captured. Default options in :mod:`keyoscacquire.config` ================================================================ -The package takes its default options from :mod:`keyoscacquire.config` (to find the location of the file run ``$ path_to_config`` in the command line): +The package takes its default options from :mod:`keyoscacquire.config` +(to find the location of the file run ``$ path_to_config`` in the command line): .. literalinclude :: ../../keyoscacquire/config.py + :linenos: -.. note:: None of the functions access the global variables directly, but they are feed them as default arguments. +.. note:: None of the functions access the global variables directly, but they + are feed them as default arguments. -The command line programmes will save traces in the folder from where they are ran as ``_filename+_file_delimiter++_filetype``, i.e. by default as ``data n.csv`` and ``data n.png``. +The command line programmes will save traces in the folder from where they are +ran as ``_filename+_file_delimiter++_filetype``, i.e. by default as +``data n.csv`` and ``data n.png``. .. _logging: @@ -110,7 +156,9 @@ The command line programmes will save traces in the folder from where they are r Logging ======= -The module gives output for debugging through :mod:`logging`. The output can be directed to the terminal by adding the following to the top level file using the keyoscacquire package:: +The module gives output for debugging through :mod:`logging`. The output can be +directed to the terminal by adding the following to the top level file using +the keyoscacquire package:: import logging logging.basicConfig(level=logging.DEBUG) @@ -133,7 +181,9 @@ Running the module with python -m keyoscacquire -obtains and saves a trace with default options being used. Alternatively, the filename and acquisition type can be specified as per the paragraph above using the executable, e.g. +obtains and saves a trace with default options being used. Alternatively, the +filename and acquisition type can be specified as per the paragraph above using +the executable, e.g. .. prompt:: bash @@ -144,4 +194,6 @@ obtains and saves a trace with default options being used. Alternatively, the fi Scripts in ./scripts -------------------- -These can be ran as command line programmes from the scripts folder with ``$ python [script].py [options]``, where the options are as for the installed command line programmes, and can be displayed with the flag ``-h``. +These can be ran as command line programmes from the scripts folder with +``$ python [script].py [options]``, where the options are as for the installed +command line programmes, and can be displayed with the flag ``-h``. diff --git a/docs/index.rst b/docs/index.rst index 429102b..1f671e6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2,32 +2,36 @@ sphinx-quickstart on Mon Oct 21 05:06:26 2019. .. include:: ../README.rst + :end-before: command-line-use-marker + .. toctree:: :maxdepth: 2 - :caption: User guide: + :caption: User guide contents/overview contents/usage contents/commandline-programmes .. toctree:: - :maxdepth: 2 - :caption: Reference: + :maxdepth: 3 + :caption: Reference contents/osc-class contents/dataprocessing contents/programmes contents/config + contents/auxiliary .. toctree:: :maxdepth: 2 - :caption: Project documentation: + :caption: Project docs ../changelog ../known-issues contents/license + GitHub repository License diff --git a/docs/known-issues.rst b/docs/known-issues.rst index 39a9f64..21c0232 100644 --- a/docs/known-issues.rst +++ b/docs/known-issues.rst @@ -4,12 +4,18 @@ Known issues and suggested improvements * Issues: - - There has previously been an issue with the data transfer/interpretation where the output waveform is not as it is on the oscilloscope screen. If this happens, open *KeySight BenchVue* and obtain one trace through the software. Now try to obtain a trace through this package -- it should now work again. Please report this if this happens. + - There has previously been an issue with the data transfer/interpretation + where the output waveform is not as it is on the oscilloscope screen. If + this happens, open *KeySight BenchVue* and obtain one trace through the + software. Now try to obtain a trace through this package -- it should now + work again. Please report this if this happens. * Improvements: - - (feature) Make a dynamic default setting for number of points - - (feature) Build functionality for pickling measurement to disk and then post-processing for speed-up in consecutive measurements - - (instrument support) expand support for Infiniium oscilloscopes - - (docs) Write tutorial page for documentation - - (housekeeping) PEP8 compliance and code audit + - (feature) capture MATH waveform + - (feature) measurements provided by the scope + - (feature) Build functionality for pickling measurement to disk and then + post-processing for speed-up in consecutive measurements + - (instrument support) expand support for Infiniium oscilloscopes + - (docs) Write tutorial page for documentation + - (housekeeping) PEP8 compliance and code audit diff --git a/docs/requirements.txt b/docs/requirements.txt index 769c2e8..a4223ef 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,6 +1,7 @@ sphinx>=2.2 sphinx-prompt -sphinx-rtd-theme +furo +recommonmark pyvisa numpy argparse diff --git a/keyoscacquire/VERSION b/keyoscacquire/VERSION index b502146..fcdb2e1 100644 --- a/keyoscacquire/VERSION +++ b/keyoscacquire/VERSION @@ -1 +1 @@ -3.0.2 +4.0.0 diff --git a/keyoscacquire/__init__.py b/keyoscacquire/__init__.py index e021ff7..c6b320b 100644 --- a/keyoscacquire/__init__.py +++ b/keyoscacquire/__init__.py @@ -14,7 +14,11 @@ import logging; _log = logging.getLogger(__name__) -# local file with default options: +import keyoscacquire.oscacq as oscacq +import keyoscacquire.auxiliary as traceio import keyoscacquire.config as config +import keyoscacquire.programmes as programmes +import keyoscacquire.auxiliary as auxiliary + from keyoscacquire.oscacq import Oscilloscope diff --git a/keyoscacquire/auxiliary.py b/keyoscacquire/auxiliary.py new file mode 100644 index 0000000..c729db4 --- /dev/null +++ b/keyoscacquire/auxiliary.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +""" +Auxiliary functions for the keyoscacquire package + +Andreas Svela // 2020 +""" + +import os +import logging; _log = logging.getLogger(__name__) +import keyoscacquire.config as config + + +def interpret_visa_id(id): + """Interprets VISA ID, finds oscilloscope model series if applicable + + Parameters + ---------- + id : str + VISA ID as returned by the ``*IDN?`` command + + Returns + ------- + maker : str + Maker of the instrument, e.g. Keysight Technologies + model : str + Model of the instrument + serial : str + Serial number of the instrument + firmware : str + Firmware version + model_series : str + "N/A" unless the instrument is a Keysight/Agilent DSO and MSO oscilloscope. + Returns the model series, e.g. '2000'. Returns "not found" if the model name cannot be interpreted. + """ + maker, model, serial, firmware = id.split(",") + # Find model_series if applicable + if model[:3] in ['DSO', 'MSO']: + # Find the numbers in the model string + model_number = [c for c in model if c.isdigit()] + # Pick the first number and add 000 or use not found + model_series = model_number[0]+'000' if not model_number == [] else "not found" + else: + model_series = "N/A" + return maker, model, serial, firmware, model_series + + +def check_file(fname, ext=config._filetype, num=""): + """Checking if file ``fname+num+ext`` exists. If it does, the user is + prompted for a string to append to fname until a unique fname is found. + + Parameters + ---------- + fname : str + Base filename to test + ext : str, default :data:`~keyoscacquire.config._filetype` + File extension + num : str, default "" + Filename suffix that is tested for, but the appended part to the fname + will be placed before it,and the suffix will not be part of the + returned fname + + Returns + ------- + fname : str + New fname base + """ + while os.path.exists(fname+num+ext): + append = input(f"File '{fname+num+ext}' exists! Append to filename '{fname}' before saving: ") + fname += append + return fname diff --git a/keyoscacquire/config.py b/keyoscacquire/config.py index e48ed21..56584cf 100644 --- a/keyoscacquire/config.py +++ b/keyoscacquire/config.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """Default options for keyoscacquire""" -#: address of instrument +#: VISA address of instrument _visa_address = 'USB0::1234::1234::MY1234567::INSTR' #: waveform format transferred from the oscilloscope to the computer #: WORD formatted data is transferred as 16-bit uint. diff --git a/keyoscacquire/installed_cli_programmes.py b/keyoscacquire/installed_cli_programmes.py index 54433bf..59d8cc5 100644 --- a/keyoscacquire/installed_cli_programmes.py +++ b/keyoscacquire/installed_cli_programmes.py @@ -26,15 +26,15 @@ ##============================================================================## # Help strings -acq_help = "The acquire type: {HRESolution, NORMal, AVER} where is the number of averages in range [2, 65536]. Defaults to \'"+config._acq_type+"\'." -wav_help = "The waveform format: {BYTE, WORD, ASCii}. \nDefaults to \'"+config._waveform_format+"\'." -file_help = "The filename base, (without extension, \'"+config._filetype+"\' is added). Defaults to \'"+config._filename+"\'." -visa_help = "Visa address of instrument. To find the visa addresses of the instruments connected to the computer run 'list_visa_devices' in the command line. Defaults to \'"+config._visa_address+"\'." -timeout_help = "Milliseconds before timeout on the channel to the instrument. Defaults to "+str(config._timeout)+"." +acq_help = f"The acquire type: {{HRESolution, NORMal, AVER}} where is the number of averages in range [2, 65536]. Defaults to '{config._acq_type}'." +wav_help = f"The waveform format: {{BYTE, WORD, ASCii}}. \nDefaults to '{config._waveform_format}'." +file_help = f"The filename base, (without extension, '{config._filetype}' is added). Defaults to '{config._filename}'." +visa_help = f"Visa address of instrument. To find the visa addresses of the instruments connected to the computer run 'list_visa_devices' in the command line. Defaults to '{config._visa_address}." +timeout_help = f"Milliseconds before timeout on the channel to the instrument. Defaults to {config._timeout}." default_channels = " ".join(config._ch_nums) if isinstance(config._ch_nums, list) else config._ch_nums -channels_help = "List of the channel numbers to be acquired, for example '1 3' (without ') or 'active' (without ') to capture all the currently active channels on the oscilloscope. Defaults to \'"+default_channels+"\'." -points_help = "Use 0 to get the maximum number of points, or set a smaller number to speed up the acquisition and transfer. Defaults to 0." -delim_help = "Delimiter used between filename and filenumber (before filetype). Defaults to \'"+config._file_delimiter+"\'." +channels_help = f"List of the channel numbers to be acquired, for example '1 3' (without ') or 'active' (without ') to capture all the currently active channels on the oscilloscope. Defaults to {default_channels}'." +points_help = f"Use 0 to get the maximum number of points, or set a smaller number to speed up the acquisition and transfer. Defaults to 0." +delim_help = f"Delimiter used between filename and filenumber (before filetype). Defaults to '{config._file_delimiter}'." def connect_each_time_cli(): """Function installed on the command line: Obtains and stores multiple traces, @@ -121,8 +121,11 @@ def num_traces_cli(): def list_visa_devices_cli(): """Function installed on the command line: Lists VISA devices""" parser = argparse.ArgumentParser(description=acqprog.list_visa_devices.__doc__) + parser.add_argument('-n', action="store_false", + help=("If this flag is set, the programme will not query " + "the instruments for their IDNs.")) args = parser.parse_args() - acqprog.list_visa_devices() + acqprog.list_visa_devices(ask_idn=args.n) def path_of_config_cli(): """Function installed on the command line: Prints the full path of the config module""" diff --git a/keyoscacquire/oscacq.py b/keyoscacquire/oscacq.py index ca94116..9dade2c 100644 --- a/keyoscacquire/oscacq.py +++ b/keyoscacquire/oscacq.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- -"""The PyVISA communication with the oscilloscope. +""" +The PyVISA communication with the oscilloscope. See Keysight's Programmer's Guide for reference. @@ -8,15 +9,22 @@ __docformat__ = "restructuredtext en" -import sys, os # required for reading user input and command line arguments -import pyvisa # instrument communication -import time, datetime # for measuring elapsed time and adding current date and time to exported files +import os +import sys +import pyvisa +import time +import datetime as dt import numpy as np import matplotlib.pyplot as plt import logging; _log = logging.getLogger(__name__) # local file with default options: import keyoscacquire.config as config +import keyoscacquire.auxiliary as auxiliary +import keyoscacquire.traceio as traceio + +# for compatibility (discouraged to use) +from keyoscacquire.traceio import save_trace, save_trace_npy, plot_trace #: Supported Keysight DSO/MSO InfiniiVision series _supported_series = ['1000', '2000', '3000', '4000', '6000'] @@ -27,12 +35,12 @@ _datatypes = {'BYTE':'b', 'WORD':'h'} -##============================================================================## +## ========================================================================= ## -class Oscilloscope(): +class Oscilloscope: """PyVISA communication with the oscilloscope. - Creator opens a connection to an instrument and chooses settings for the connection. + Init opens a connection to an instrument and chooses settings for the connection. Parameters ---------- @@ -44,7 +52,7 @@ class Oscilloscope(): Milliseconds before timeout on the channel to the instrument verbose : bool, default True If ``True``: prints when the connection to the device is opened etc, - and sets attr:`acquire_print` to ``True`` + and sets attr:`verbose_acquistion` to ``True`` Raises ------ @@ -64,7 +72,7 @@ class Oscilloscope(): model : str The instrument model name model_series : str - The model series, e.g. '2000' for a DSO-X 2024A. See :func:`interpret_visa_id`. + The model series, e.g. '2000' for a DSO-X 2024A. See :func:`keyoscacquire.auxiliary.interpret_visa_id`. address : str Visa address of instrument timeout : int @@ -74,9 +82,12 @@ class Oscilloscope(): * ``'NORMal'`` — sets the oscilloscope in the normal mode. - * ``'AVERage'`` — sets the oscilloscope in the averaging mode. You can set the count by :attr:`num_averages`. + * ``'AVERage'`` — sets the oscilloscope in the averaging mode. + You can set the count by :attr:`num_averages`. - * ``'HRESolution'`` — sets the oscilloscope in the high-resolution mode (also known as smoothing). This mode is used to reduce noise at slower sweep speeds where the digitizer samples faster than needed to fill memory for the displayed time range. + * ``'HRESolution'`` — sets the oscilloscope in the high-resolution mode + (also known as smoothing). This mode is used to reduce noise at slower + sweep speeds where the digitizer samples faster than needed to fill memory for the displayed time range. For example, if the digitizer samples at 200 MSa/s, but the effective sample rate is 1 MSa/s (because of a slower sweep speed), only 1 out of every 200 samples needs to be stored. @@ -86,34 +97,48 @@ class Oscilloscope(): num_averages : int, 2 to 65536 The number of averages applied (applies only to the ``'AVERage'`` :attr:`acq_type`) p_mode : {``'NORMal'``, ``'RAW'``, ``'MAXimum'``} - ``'NORMal'`` is limited to 62,500 points, whereas ``'RAW'`` gives up to 1e6 points. Use ``'MAXimum'`` for sources that are not analogue or digital. + ``'NORMal'`` is limited to 62,500 points, whereas ``'RAW'`` gives up to + 1e6 points. Use ``'MAXimum'`` for sources that are not analogue or digital. num_points : int - Use 0 to let :attr:`p_mode` control the number of points, otherwise override with a lower number than maximum for the :attr:`p_mode` + Use 0 to let :attr:`p_mode` control the number of points, otherwise + override with a lower number than maximum for the :attr:`p_mode` wav_format : {'WORD', 'BYTE', 'ASCii'} - Select the data transmission mode for waveform data points, i.e. how the data is formatted when sent from the oscilloscope. + Select the data transmission mode for waveform data points, i.e. how + the data is formatted when sent from the oscilloscope. - * ``'ASCii'`` formatted data converts the internal integer data values to real Y-axis values. Values are transferred as ascii digits in floating point notation, separated by commas. + * ``'ASCii'`` formatted data converts the internal integer data values + to real Y-axis values. Values are transferred as ascii digits in + floating point notation, separated by commas. * ``'WORD'`` formatted data transfers signed 16-bit data as two bytes. * ``'BYTE'`` formatted data is transferred as signed 8-bit bytes. - acquire_print : bool + verbose_acquistion : bool ``True`` prints that the capturing starts and the number of points captured """ + raw = None + metadata = None + time = None + y = None + source_type = 'CHANNel' + fname = config._filename + ext = config._filetype + savepng = config._export_png + showplot = config._show_plot def __init__(self, address=config._visa_address, timeout=config._timeout, verbose=True): """See class docstring""" self.address = address self.timeout = timeout self.verbose = verbose - self.acquire_print = verbose - # connect to the scope + self.verbose_acquistion = verbose + # Connect to the scope try: rm = pyvisa.ResourceManager() self.inst = rm.open_resource(address) except pyvisa.Error as err: - print('\nVisaError: Could not connect to \'%s\'.' % address) + print(f"\n\nCould not connect to '{address}', see traceback below:\n") raise self.inst.timeout = self.timeout # For TCP/IP socket connections enable the read Termination Character, or reads will timeout @@ -126,13 +151,18 @@ def __init__(self, address=config._visa_address, timeout=config._timeout, verbos self.inst.write(':WAVeform:BYTeorder LSBFirst') # MSBF is default, must be overridden for WORD to work # Get information about the connected device self.id = self.inst.query('*IDN?').strip() # get the id of the connected device - if self.verbose: print("Connected to \'%s\'" % self.id) - _, self.model, _, _, self.model_series = interpret_visa_id(self.id) + if self.verbose: + print(f"Connected to '{self.id}'") + _, self.model, _, _, self.model_series = chsy.interpret_visa_id(self.id) if not self.model_series in _supported_series: print("(!) WARNING: This model (%s) is not yet fully supported by keyoscacquire," % self.model) print(" but might work to some extent. keyoscacquire supports Keysight's") print(" InfiniiVision X-series oscilloscopes.") - + # Populate attributes and set standard settings + self.set_acquiring_options(wav_format=config._waveform_format, acq_type=config._acq_type, + num_averages=config._num_avg, p_mode='RAW', num_points=0, + verbose_acquistion=verbose) + self.set_channels_for_capture(channel_nums=config._ch_nums) def __enter__(self): return self @@ -174,7 +204,8 @@ def is_running(self): bool ``True`` if running, ``False`` otherwise """ - reg = int(self.inst.query(':OPERegister:CONDition?')) # The third bit of the operation register is 1 if the instrument is running + # The third bit of the operation register is 1 if the instrument is running + reg = int(self.inst.query(':OPERegister:CONDition?')) return (reg & 8) == 8 def close(self, set_running=True): @@ -183,122 +214,159 @@ def close(self, set_running=True): Parameters ---------- set_running : bool, default ``True`` - ``True`` sets the oscilloscope to running before closing the connection, ``False`` leaves it in its current state + ``True`` sets the oscilloscope to running before closing the + connection, ``False`` leaves it in its current state """ # Set the oscilloscope running before closing the connection - if set_running: self.run() + if set_running: + self.run() self.inst.close() - _log.debug("Closed connection to \'%s\'" % self.id) + _log.debug(f"Closed connection to '{self.id}'") - def set_acquire_print(self, value): - """Control attribute which decides whether to print information while acquiring. + def get_error(self): + """Get the latest error - Parameters - ---------- - value : bool - ``True`` to print information to info level in log + Returns + ------- + str + error number,description """ - self.acquire_print = value + return self.query(":SYSTem:ERRor?").strip() + + def get_active_channels(self): + """Get list of the currently active channels on the instrument + + Returns + ------- + list of chars + list of the active channels, example ``['1', '3']`` + """ + channels = np.array(['1', '2', '3', '4']) + # querying DISP for each channel to determine which channels are currently displayed + displayed_channels = [self.inst.query(f":CHANnel{channel}:DISPlay?")[0] for channel in channels] + # get a mask of bools for the channels that are on [need the int() as bool('0') = True] + channel_mask = np.array([bool(int(i)) for i in displayed_channels]) + # apply mask to the channel list + self.channel_nums = channels[channel_mask] + return self.channel_nums - def set_acquiring_options(self, wav_format=config._waveform_format, acq_type=config._acq_type, - num_averages=config._num_avg, p_mode='RAW', num_points=0, acq_print=None): - """Sets the options for acquisition from the oscilloscope. + def set_acquiring_options(self, wav_format=None, acq_type=None, + num_averages=None, p_mode=None, num_points=None, + verbose_acquistion=None): + """Change acquisition options Parameters ---------- wav_format : {``'WORD'``, ``'BYTE'``, ``'ASCii'``}, default :data:`~keyoscacquire.config._waveform_format` - Select the format of the communication of waveform from the oscilloscope, see :attr:`wav_format` + Select the format of the communication of waveform from the + oscilloscope, see :attr:`wav_format` acq_type : {``'HRESolution'``, ``'NORMal'``, ``'AVERage'``, ``'AVER'``}, default :data:`~keyoscacquire.config._acq_type` - Acquisition mode of the oscilloscope. will be used as num_averages if supplied, see :attr:`acq_type` + Acquisition mode of the oscilloscope. will be used as + num_averages if supplied, see :attr:`acq_type` num_averages : int, 2 to 65536, default :data:`~keyoscacquire.config._num_avg` Applies only to the ``'AVERage'`` mode: The number of averages applied p_mode : {``'NORMal'``, ``'RAW'``, ``'MAXimum'``}, default ``'RAW'`` - ``'NORMal'`` is limited to 62,500 points, whereas ``'RAW'`` gives up to 1e6 points. Use ``'MAXimum'`` for sources that are not analogue or digital + ``'NORMal'`` is limited to 62,500 points, whereas ``'RAW'`` gives up to 1e6 points. + Use ``'MAXimum'`` for sources that are not analogue or digital num_points : int, default 0 - Use 0 to let :attr:`p_mode` control the number of points, otherwise override with a lower number than maximum for the :attr:`p_mode` - acq_print : bool or ``None``, default ``None`` - Temporarily control attribute which decides whether to print information while acquiring: bool sets it to the bool value, ``None`` leaves as the it is in the Oscilloscope object + Use 0 to get the maximum amount of points, otherwise + override with a lower number than maximum for the :attr:`p_mode` + verbose_acquistion : bool or ``None``, default ``None`` + Temporarily control attribute which decides whether to print + information while acquiring: bool sets it to the bool value, + ``None`` leaves as the it is in the Oscilloscope object Raises ------ ValueError - if num_averages are outside of the range or in acq_type cannot be converted to int + If num_averages are outside of the range or in acq_type cannot + be converted to int """ - self.wav_format = wav_format; self.acq_type = acq_type[:4] - self.p_mode = p_mode; self.num_points = num_points - - # set acquire_print only if not None - if acq_print is not None: - self.acquire_print = acq_print - + # Set verbose_acquistion only if not None + if verbose_acquistion is not None: + self.verbose_acquistion = verbose_acquistion + # Set the acquistion type + if acq_type is not None: + self.acq_type = acq_type[:4].upper() + # Handle AVER expressions + if self.acq_type == 'AVER' and not acq_type[4:].lower() == 'age': + if len(acq_type) > 4: + try: + self.num_averages = int(acq_type[4:]) + except ValueError: + ValueError(f"\nValueError: Failed to convert '{acq_type[4:]}' to an integer, " + "check that acquisition type is on the form AVER or AVER " + f"where is an integer (currently acq. type is '{acq_type}').\n") + else: + if num_averages is not None: + self.num_averages = num_averages self.inst.write(':ACQuire:TYPE ' + self.acq_type) - if self.verbose: print(" Acquiring type:", self.acq_type) - # handle AVER expressions + if self.verbose: + print(" Acquisition type:", self.acq_type) + # Check that self.num_averages is within the acceptable range if self.acq_type == 'AVER': - try: - # if the type is longer than four characters, treat characters - # from fifth to end as number of averages - self.num_averages = int(acq_type[4:]) if len(acq_type) > 4 else num_averages - except ValueError: - print("\nValueError: Failed to convert \'%s\' to an integer," - " check that acquisition type is on the form AVER or AVER" - " where is an integer (currently acq. type is \'%s\').\n" - % (acq_type[4:], acq_type)) - # check that self.num_averages is within acceptable range if not (1 <= self.num_averages <= 65536): - raise ValueError("\nThe number of averages {} is out of range.".format(self.num_averages)) - else: - self.num_averages = num_averages - - # now set the number of averages parameter if relevant - if self.acq_type[:4] == 'AVER': # averaging applies AVERage modes only - self.inst.write(':ACQuire:COUNt ' + str(self.num_averages)) - if self.verbose: print(" # of averages: ", self.num_averages) + raise ValueError(f"\nThe number of averages {self.num_averages} is out of range.") + self.inst.write(f":ACQuire:COUNt {self.num_averages}") + if self.verbose: + print(" # of averages: ", self.num_averages) + # Set options for waveform export + self.set_waveform_export_options(wav_format, num_points, p_mode) + + def set_waveform_export_options(self, wav_format=None, num_points=None, p_mode=None): + """ + Set options for the waveform export from the oscilloscope to the computer - ## Set options for waveform export - # choose format for the transmitted waveform - self.inst.write(':WAVeform:FORMat ' + self.wav_format) + Parameters + ---------- + wav_format : {``'WORD'``, ``'BYTE'``, ``'ASCii'``}, default :data:`~keyoscacquire.config._waveform_format` + Select the format of the communication of waveform from the + oscilloscope, see :attr:`wav_format` + p_mode : {``'NORMal'``, ``'RAW'``, ``'MAXimum'``}, default ``'RAW'`` + ``'NORMal'`` is limited to 62,500 points, whereas ``'RAW'`` gives up to 1e6 points. + Use ``'MAXimum'`` for sources that are not analogue or digital + num_points : int, default 0 + Use 0 to get the maximum amount of points, otherwise + override with a lower number than maximum for the :attr:`p_mode` + """ + # Choose format for the transmitted waveform + if wav_format is not None: + self.inst.write(f":WAVeform:FORMat {wav_format}") + self.wav_format = wav_format + if p_mode is not None: + self.p_mode = p_mode a_isaver = self.acq_type == 'AVER' p_isnorm = self.p_mode[:4] == 'NORM' if a_isaver and not p_isnorm: self.p_mode = 'NORM' - _log.debug(":WAVeform:POINts:MODE overridden (from %s) to NORMal due to :ACQuire:TYPE:AVERage." % p_mode) - else: - self.p_mode = p_mode - self.inst.write(':WAVeform:POINts:MODE ' + self.p_mode) - #_log.debug("Max number of points for mode %s: %s" % (self.p_mode, self.inst.query(':ACQuire:POINts?'))) - # if number of points has been specified, tell the instrument to + _log.debug(f":WAVeform:POINts:MODE overridden (from {p_mode}) to " + "NORMal due to :ACQuire:TYPE:AVERage.") + self.inst.write(f":WAVeform:POINts:MODE {self.p_mode}") + # Set to maximum number of points if + if num_points == 0: + self.inst.write(f":WAVeform:POINts MAXimum") + _log.debug("Number of points set to: MAX") + self.num_points = num_points + # If number of points has been specified, tell the instrument to # use this number of points - if self.num_points > 0: + elif num_points > 0: if self.model_series in _supported_series: - self.inst.write(':WAVeform:POINts ' + str(self.num_points)) + self.inst.write(f":WAVeform:POINts {self.num_points}") elif self.model_series in ['9000']: - self.inst.write(':ACQuire:POINts ' + str(self.num_points)) + self.inst.write(f":ACQuire:POINts {self.num_points}") else: - self.inst.write(':WAVeform:POINts ' + str(self.num_points)) + self.inst.write(f":WAVeform:POINts {self.num_points}") _log.debug("Number of points set to: ", self.num_points) + self.num_points = num_points - def get_active_channels(self): - """Get list of the currently active channels on the instrument - - Returns - ------- - list of chars - list of the active channels, example ``['1', '3']`` - """ - channels = np.array(['1', '2', '3', '4']) - # querying DISP for each channel to determine which channels are currently displayed - displayed_channels = [self.inst.query(':CHANnel'+channel+':DISPlay?')[0] for channel in channels] - # get a mask of bools for the channels that are on [need the int() as int('0') = True] - channel_mask = np.array([bool(int(i)) for i in displayed_channels]) - # apply mask to the channel list - channel_nums = channels[channel_mask] - return channel_nums + ## Capture and read functions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## - def determine_channels(self, source_type='CHANnel', channel_nums=config._ch_nums): - """Decide the channels to be acquired, or determine by checking active channels on the oscilloscope. + def set_channels_for_capture(self, source_types=None, channel_nums=None): + """Decide the channels to be acquired, or determine by checking active + channels on the oscilloscope. - .. note:: Use ``channel_nums='active'`` to capture all the currently active channels on the oscilloscope. + .. note:: Use ``channel_nums='active'`` to capture all the currently + active channels on the oscilloscope. Parameters ---------- @@ -307,49 +375,45 @@ def determine_channels(self, source_type='CHANnel', channel_nums=config._ch_nums Future version might include {'MATH', 'FUNCtion'}. channel_nums : list or ``'active'``, default :data:`~keyoscacquire.config._ch_nums` list of the channel numbers to be acquired, example ``['1', '3']``. - Use ``'active'`` or ``['']`` to capture all the currently active channels on the oscilloscope. + Use ``'active'`` or ``['']`` to capture all the currently active + channels on the oscilloscope. Returns ------- - sources : list of str - list of the sources, example ``['CHAN1', 'CHAN3']`` - sourcesstring : str - String of comma separated sources, example ``'CHANnel1, CHANnel3'`` channel_nums : list of chars list of the channels, example ``['1', '3']`` """ - # if no channels specified, find the channels currently active and acquire from those - if channel_nums in [[''], ['active'], 'active']: - channel_nums = self.get_active_channels() - # build list of sources - sources = [source_type+channel for channel in channel_nums] - # make string of sources - sourcesstring = ", ".join(sources) - if self.acquire_print: print("Acquire from sources", sourcesstring) - return sources, sourcesstring, channel_nums + # If no channels specified, find the channels currently active and acquire from those + if channel_nums is not None: + self.channel_nums = channel_nums + if self.channel_nums in [[''], ['active'], 'active']: + self.get_active_channels() + # Build list of sources + self.sources = [self.source_type+channel for channel in self.channel_nums] + # Make string of sources + self.sourcesstring = ", ".join(self.sources) + if self.verbose_acquistion: + print("Acquire from sources", self.sourcesstring) + return self.channel_nums + + def capture_and_read(self, set_running=True): + """Acquire raw data from selected channels according to acquring options + currently set with :func:`set_acquiring_options`. + The parameters are provided by :func:`set_channels_for_capture`. + + The populated attributes raw and metadata should be processed + by :func:`process_data`. - def capture_and_read(self, sources, sourcestring, set_running=True): - """This is a wrapper function for choosing the correct capture_and_read function according to :attr:`wav_format`, :func:`capture_and_read_binary` or :func:`capture_and_read_ascii`. - - Acquire raw data from selected channels according to acquring options currently set with :func:`set_acquiring_options`. - The parameters are provided by :func:`determine_channels`. - - The output should be processed by :func:`process_data`. + raw : :class:`~numpy.ndarray` + An ndarray of ints that can be converted to voltage values using the preamble. + metadata + depends on the wav_format Parameters ---------- - sources : list of str - list of sources, example ``['CHANnel1', 'CHANnel3']`` - sourcesstring : str - String of comma separated sources, example ``'CHANnel1, CHANnel3'`` set_running : bool, default ``True`` ``True`` leaves oscilloscope running after data capture - Returns - ------- - See respective - Depends on the capture_and_read function used - Raises ------ ValueError @@ -359,160 +423,164 @@ def capture_and_read(self, sources, sourcestring, set_running=True): -------- :func:`process_data` """ + ## Capture data + if self.verbose_acquistion: + print("Start acquisition..") + start_time = time.time() # time the acquiring process + # If the instrument is not running, we presumably want the data + # on the screen and hence don't want to use DIGitize as digitize + # will obtain a new trace. + if self.is_running(): + # DIGitize is a specialized RUN command. + # Waveforms are acquired according to the settings of the :ACQuire commands. + # When acquisition is complete, the instrument is stopped. + self.inst.write(':DIGitize ' + self.sourcesstring) + ## Read from the scope if self.wav_format[:3] in ['WOR', 'BYT']: - return self.capture_and_read_binary(sources, sourcestring, datatype=_datatypes[self.wav_format], set_running=set_running) + self._read_binary(datatype=_datatypes[self.wav_format]) elif self.wav_format[:3] == 'ASC': - return self.capture_and_read_ascii(sources, sourcestring, set_running=set_running) + self._read_ascii() else: - raise ValueError("Could not capture and read data, waveform format \'{}\' is unknown.\n".format(self.wav_format)) + raise ValueError(f"Could not capture and read data, waveform format '{self.wav_format}' is unknown.\n") + ## Print to log + to_log = f"Elapsed time capture and read: {(time.time()-start_time)*1e3:.1f} ms" + if self.verbose_acquistion: + _log.info(to_log) + else: + _log.debug(to_log) + if set_running: + self.run() - def capture_and_read_binary(self, sources, sourcesstring, datatype='standard', set_running=False): - """Capture and read data and metadata from sources of the oscilloscope when waveform format is ``'WORD'`` or ``'BYTE'``. + def _read_binary(self, datatype='standard'): + """Read data and metadata from sources of the oscilloscope + when waveform format is ``'WORD'`` or ``'BYTE'``. - The parameters are provided by :func:`determine_channels`. + The parameters are provided by :func:`set_channels_for_capture`. The output should be processed by :func:`process_data_binary`. + Populates the following attributes + raw : :class:`~numpy.ndarray` + Raw data to be processed by :func:`process_data_binary`. + An ndarray of ints that can be converted to voltage values using the preamble. + metadata : list of str + List of preamble metadata (comma separated ascii values) for each channel + Parameters ---------- - sources : list of str - list of sources, example ``['CHANnel1', 'CHANnel3']`` - sourcesstring : str - String of comma separated sources, example ``'CHANnel1, CHANnel3'`` datatype : char or ``'standard'``, optional but must match waveform format used - To determine how to read the values from the oscilloscope depending on :attr:`wav_format`. Datatype is ``'h'`` for 16 bit signed int (``'WORD'``), for 8 bit signed bit (``'BYTE'``) (same naming as for structs, `https://docs.python.org/3/library/struct.html#format-characters`). - ``'standard'`` will evaluate :data:`oscacq._datatypes[self.wav_format]` to automatically choose according to the waveform format + To determine how to read the values from the oscilloscope depending + on :attr:`wav_format`. Datatype is ``'h'`` for 16 bit signed int + (``'WORD'``), for 8 bit signed bit (``'BYTE'``) (same naming as for + structs, `https://docs.python.org/3/library/struct.html#format-characters`). + ``'standard'`` will evaluate :data:`oscacq._datatypes[self.wav_format]` + to automatically choose according to the waveform format set_running : bool, default ``True`` ``True`` leaves oscilloscope running after data capture - - Returns - ------- - raw : :class:`~numpy.ndarray` - Raw data to be processed by :func:`process_data_binary`. - An ndarray of ints that can be converted to voltage values using the preamble. - preamble : str - Preamble metadata (comma separated ascii values) """ - ## Capture data - if self.acquire_print: print("Start acquisition..") - start_time = time.time() # time the acquiring process - # If the instrument is not running, we presumably want the data on the screen and hence don't want - # to use DIGitize as digitize will obtain a new trace. - if self.is_running(): - self.inst.write(':DIGitize ' + sourcesstring) # DIGitize is a specialized RUN command. - # Waveforms are acquired according to the settings of the :ACQuire commands. - # When acquisition is complete, the instrument is stopped. - ## Read out metadata and data - raw, preambles = [], [] - for source in sources: - # select the channel for which the succeeding WAVeform commands applies to + self.raw, self.metadata = [], [] + # Loop through all the sources + for source in self.sources: + # Select the channel for which the succeeding WAVeform commands applies to self.inst.write(':WAVeform:SOURce ' + source) try: # obtain comma separated metadata values for processing of raw data for this source - preambles.append(self.inst.query(':WAVeform:PREamble?')) + self.metadata.append(self.inst.query(':WAVeform:PREamble?')) # obtain the data # read out data for this source - raw.append(self.inst.query_binary_values(':WAVeform:DATA?', datatype=datatype, container=np.array)) + self.raw.append(self.inst.query_binary_values(':WAVeform:DATA?', datatype=datatype, container=np.array)) except pyvisa.Error as err: - print("\nError: Failed to obtain waveform, have you checked that" - " the timeout (currently %d ms) is sufficently long?" % self.timeout) - print(err) + print(f"\n\nError: Failed to obtain waveform, have you checked that" + f" the timeout (currently {self.timeout:,d} ms) is sufficently long?") + try: + print(f"Latest error from the oscilloscope: '{self.get_error()}'\n") + except Exception: + print("Could not retrieve error from the oscilloscope") self.close() + print(err) raise - if self.acquire_print: - _log.info("Elapsed time capture and read: %.1f ms" % ((time.time()-start_time)*1e3)) - else: - _log.debug("Elapsed time capture and read: %.1f ms" % ((time.time()-start_time)*1e3)) - if set_running: self.inst.write(':RUN') # set the oscilloscope running again - return raw, preambles - def capture_and_read_ascii(self, sources, sourcesstring, set_running=True): - """Capture and read data and metadata from sources of the oscilloscope when waveform format is ASCii. + def _read_ascii(self): + """Read data and metadata from sources of the oscilloscope + when waveform format is ASCii. - The parameters are provided by :func:`determine_channels`. + The parameters are provided by :func:`set_channels_for_capture`. The output should be processed by :func:`process_data_ascii`. + Populates the following attributes + raw : str + Raw data to be processed by :func:`process_data_ascii`. + The raw data is a list of one IEEE block per channel with a head + and then comma separated ascii values. + metadata : tuple of str + Tuple of the preamble for one of the channels to calculate time + axis (same for all channels) and the model series + Parameters ---------- - sources : list of str - list of sources, example ``['CHANnel1', 'CHANnel3']`` - sourcesstring : str - String of comma separated sources, example ``'CHANnel1, CHANnel3'`` set_running : bool, default ``True`` ``True`` leaves oscilloscope running after data capture - - Returns - ------- - raw : str - Raw data to be processed by :func:`process_data_ascii`. - The raw data is a list of one IEEE block per channel with a head and then comma separated ascii values. - metadata : tuple of str - Tuple of the preamble for one of the channels to calculate time axis (same for all channels) and the model series """ - ## Capture data - if self.acquire_print: print("Start acquisition..") - start_time = time.time() # time the acquiring process - # If the instrument is not running, we presumably want the data on the screen and hence don't want - # to use DIGitize as digitize will obtain a new trace. - if self.is_running(): - self.inst.write(':DIGitize ' + sourcesstring) # DIGitize is a specialized RUN command. - # Waveforms are acquired according to the settings of the :ACQuire commands. - # When acquisition is complete, the instrument is stopped. - ## Read out data - raw = [] - for source in sources: # loop through all the sources - # select the channel for which the succeeding WAVeform commands applies to + self.raw = [] + # Loop through all the sources + for source in self.sources: + # Select the channel for which the succeeding WAVeform commands applies to self.inst.write(':WAVeform:SOURce ' + source) try: - raw.append(self.inst.query(':WAVeform:DATA?')) # read out data for this source + # Read out data for this source + self.raw.append(self.inst.query(':WAVeform:DATA?')) except pyvisa.Error: - print("\nVisaError: Failed to obtain waveform, have you checked that" - " the timeout (currently %d ms) is sufficently long?" % self.timeout) + print(f"\nVisaError: Failed to obtain waveform, have you checked that" + f" the timeout (currently {self.timeout:,d} ms) is sufficently long?") + try: + print(f"Latest error from the oscilloscope: '{self.get_error()}'\n") + except Exception: + print("Could not retrieve error from the oscilloscope") self.close() + print(err) raise - if self.acquire_print: - _log.info("Elapsed time capture and read: %.1f ms" % ((time.time()-start_time)*1e3)) - else: - _log.debug("Elapsed time capture and read: %.1f ms" % ((time.time()-start_time)*1e3)) - # preamble (used for calculating time axis, which is the same for all channels) + # Get the preamble (used for calculating time axis, which is the same + # for all channels) preamble = self.inst.query(':WAVeform:PREamble?') - metadata = (preamble, self.model_series) - if set_running: self.inst.write(':RUN') # set the oscilloscope running again - return raw, metadata + self.metadata = (preamble, self.model_series) + - ##~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~## + ## Building functions to get a trace and various option setting and processing ## - def get_trace(self, sources, sourcesstring, acquire_print=None): + def get_trace(self, verbose_acquistion=None): """Obtain one trace with current settings. Parameters ---------- - sources : list of str - list of sources, example ``['CHANnel1', 'CHANnel3']`` - sourcesstring : str - String of comma separated sources, example ``'CHANnel1, CHANnel3'`` - acquire_print : bool or ``None``, default ``None`` - Possibility to override :attr:`acquire_print` temporarily, but the current - setting will be restored afterwards + verbose_acquistion : bool or ``None``, default ``None`` + Possibility to override :attr:`verbose_acquistion` temporarily, + but the current setting will be restored afterwards Returns ------- time : :class:`~numpy.ndarray` Time axis for the measurement y : :class:`~numpy.ndarray` - Voltage values, same sequence as sources input, each row represents one channel - + Voltage values, same sequence as sources input, each row + represents one channel + channel_nums : list of chars + list of the channels obtained from, example ``['1', '3']`` """ - if acquire_print is not None: # possibility to override acquire_print - temp = self.acquire_print # store current setting - self.acquire_print = acquire_print # set temporary setting - raw, metadata = self.capture_and_read(sources, sourcesstring) - time, y = process_data(raw, metadata, self.wav_format, acquire_print=self.acquire_print) # capture, read and process data - if acquire_print is not None: self.acquire_print = temp # restore to previous setting - return time, y - - def set_options_get_trace(self, channel_nums=config._ch_nums, source_type='CHANnel', - wav_format=config._waveform_format, acq_type=config._acq_type, - num_averages=config._num_avg, p_mode='RAW', num_points=0): + # Possibility to override verbose_acquistion + if verbose_acquistion is not None: + # Store current setting and set temporary setting + temp = self.verbose_acquistion + self.verbose_acquistion = verbose_acquistion + # Capture, read and process data + self.capture_and_read() + self.time, self.y = process_data(self.raw, self.metadata, self.wav_format, verbose_acquistion=self.verbose_acquistion) + # Restore self.verbose_acquistion to previous setting + if verbose_acquistion is not None: + self.verbose_acquistion = temp + return self.time, self.y, self.num_channels + + def set_options_get_trace(self, channel_nums=None, source_type=None, + wav_format=None, acq_type=None, + num_averages=None, p_mode=None, num_points=None): """Set the options provided by the parameters and obtain one trace. Parameters @@ -524,15 +592,19 @@ def set_options_get_trace(self, channel_nums=config._ch_nums, source_type='CHANn Selects the source type. Must be ``'CHANnel'`` in current implementation. Future version might include {'MATH', 'FUNCtion'}. wav_format : {``'WORD'``, ``'BYTE'``, ``'ASCii'``}, default :data:`~keyoscacquire.config._waveform_format` - Select the format of the communication of waveform from the oscilloscope, see :attr:`wav_format` + Select the format of the communication of waveform from the + oscilloscope, see :attr:`wav_format` acq_type : {``'HRESolution'``, ``'NORMal'``, ``'AVERage'``, ``'AVER'``}, default :data:`~keyoscacquire.config._acq_type` - Acquisition mode of the oscilloscope. will be used as num_averages if supplied, see :attr:`acq_type` + Acquisition mode of the oscilloscope. will be used as + num_averages if supplied, see :attr:`acq_type` num_averages : int, 2 to 65536, default :data:`~keyoscacquire.config._num_avg` Applies only to the ``'AVERage'`` mode: The number of averages applied p_mode : {``'NORMal'``, ``'RAW'``, ``'MAXimum'``}, default ``'RAW'`` - ``'NORMal'`` is limited to 62,500 points, whereas ``'RAW'`` gives up to 1e6 points. Use ``'MAXimum'`` for sources that are not analogue or digital + ``'NORMal'`` is limited to 62,500 points, whereas ``'RAW'`` gives + up to 1e6 points. Use ``'MAXimum'`` for sources that are not analogue or digital num_points : int, default 0 - Use 0 to let :attr:`p_mode` control the number of points, otherwise override with a lower number than maximum for the :attr:`p_mode` + Use 0 to let :attr:`p_mode` control the number of points, otherwise + override with a lower number than maximum for the :attr:`p_mode` Returns ------- @@ -548,21 +620,24 @@ def set_options_get_trace(self, channel_nums=config._ch_nums, source_type='CHANn num_averages=num_averages, p_mode=p_mode, num_points=num_points) ## Select sources - sources, sourcesstring, channel_nums = self.determine_channels(source_type=source_type, channel_nums=channel_nums) + self.set_channels_for_capture(source_type=source_type, channel_nums=channel_nums) ## Capture, read and process data - time, y = self.get_trace(sources, sourcesstring) - return time, y, channel_nums + self.get_trace() + return self.time, self.y, self.channel_nums - def set_options_get_trace_save(self, fname=config._filename, ext=config._filetype, channel_nums=config._ch_nums, source_type='CHANnel', - wav_format=config._waveform_format, acq_type=config._acq_type, num_averages=config._num_avg, p_mode='RAW', num_points=0): + def set_options_get_trace_save(self, fname=None, ext=None, + channel_nums=None, source_type=None, + wav_format=None, acq_type=None, + num_averages=None, p_mode=None, num_points=None): """Get trace and save the trace to a file and plot to png. Filename is recursively checked to ensure no overwrite. - The file header is:: + The file header when capturing ch 1 and 3 in AVER8 is:: - self.id+"\\n"+ - "time,"+",".join(channel_nums)+"\\n"+ - timestamp + # AGILENT TECHNOLOGIES,DSO-X 2024A,MY1234567,12.34.1234567890 + # AVER,8 + # 2019-09-06 20:01:15.187598 + # time,1,3 Parameters ---------- @@ -577,25 +652,27 @@ def set_options_get_trace_save(self, fname=config._filename, ext=config._filetyp Selects the source type. Must be ``'CHANnel'`` in current implementation. Future version might include {'MATH', 'FUNCtion'} wav_format : {``'WORD'``, ``'BYTE'``, ``'ASCii'``}, default :data:`~keyoscacquire.config._waveform_format` - Select the format of the communication of waveform from the oscilloscope, see :attr:`wav_format` + Select the format of the communication of waveform from the + oscilloscope, see :attr:`wav_format` acq_type : {``'HRESolution'``, ``'NORMal'``, ``'AVERage'``, ``'AVER'``}, default :data:`~keyoscacquire.config._acq_type` - Acquisition mode of the oscilloscope. will be used as num_averages if supplied, see :attr:`acq_type` + Acquisition mode of the oscilloscope. will be used as + num_averages if supplied, see :attr:`acq_type` num_averages : int, 2 to 65536, default :data:`~keyoscacquire.config._num_avg` Applies only to the ``'AVERage'`` mode: The number of averages applied p_mode : {``'NORMal'``, ``'RAW'``, ``'MAXimum'``}, default ``'RAW'`` - ``'NORMal'`` is limited to 62,500 points, whereas ``'RAW'`` gives up to 1e6 points. Use ``'MAXimum'`` for sources that are not analogue or digital + ``'NORMal'`` is limited to 62,500 points, whereas ``'RAW'`` gives up + to 1e6 points. Use ``'MAXimum'`` for sources that are not analogue + or digital num_points : int, default 0 - Use 0 to let :attr:`p_mode` control the number of points, otherwise override with a lower number than maximum for the :attr:`p_mode` + Use 0 to let :attr:`p_mode` control the number of points, otherwise + override with a lower number than maximum for the :attr:`p_mode` """ - fname = check_file(fname, ext) - x, y, channel_nums = self.set_options_get_trace(channel_nums=channel_nums, source_type=source_type, - wav_format=wav_format, acq_type=acq_type, num_averages=num_averages, - p_mode=p_mode, num_points=num_points) - plot_trace(x, y, channel_nums, fname=fname) - head = self.generate_file_header(channel_nums) - save_trace(fname, x, y, fileheader=head, ext=ext, print_filename=self.acquire_print) - - def generate_file_header(self, channels, additional_line=None, timestamp=True): + self.set_options_get_trace(channel_nums=channel_nums, source_type=source_type, + wav_format=wav_format, acq_type=acq_type, num_averages=num_averages, + p_mode=p_mode, num_points=num_points) + self.save_trace(fname, ext) + + def generate_file_header(self, channels=None, additional_line=None, timestamp=True): """Generate string to be used as file header for saved files The file header has this structure:: @@ -606,11 +683,15 @@ def generate_file_header(self, channels, additional_line=None, timestamp=True): additional_line time, - Where ```` is the :attr:`~keyoscacquire.oscacq.Oscilloscope.id` of the oscilloscope, - ```` is the :attr:`~keyoscacquire.oscacq.Oscilloscope.acq_type`, ```` :attr:`~keyoscacquire.oscacq.Oscilloscope.num_averages` (``"N/A"`` if not applicable) - and ```` are the comma separated channels used. + Where ```` is the :attr:`~keyoscacquire.oscacq.Oscilloscope.id` of + the oscilloscope, ```` is the :attr:`~keyoscacquire.oscacq.Oscilloscope.acq_type`, + ```` :attr:`~keyoscacquire.oscacq.Oscilloscope.num_averages` + (``"N/A"`` if not applicable) and ```` are the comma separated + channels used. - .. note:: If ``additional_line`` is not supplied the fileheader will be four lines. If ``timestamp=False`` the timestamp line will not be present. + .. note:: If ``additional_line`` is not supplied the fileheader will + be four lines. If ``timestamp=False`` the timestamp line will not + be present. Parameters ---------- @@ -642,30 +723,76 @@ def generate_file_header(self, channels, additional_line=None, timestamp=True): # time,1,3 """ + # Set num averages only if AVERage mode num_averages = str(self.num_averages) if self.acq_type[:3] == 'AVE' else "N/A" - mode_line = self.acq_type+","+num_averages+"\n" - timestamp_line = str(datetime.datetime.now())+"\n" if timestamp else "" + mode_line = f"{self.acq_type},{num_averages}\n" + # Set timestamp if called for + timestamp_line = str(dt.datetime.now())+"\n" if timestamp else "" + # Set addtional line if called for add_line = additional_line+"\n" if additional_line is not None else "" - channels_line = "time,"+",".join(channels) + # Use channel_nums unless channel argument is not None + channels = self.channel_nums if channels is None else channels + ch_str = ",".join(channels) + channels_line = f"time,{ch_str}" return self.id+"\n"+mode_line+timestamp_line+add_line+channels_line + def save_trace(self, fname=None, ext=None, savepng=None, showplot=None): + """Save the most recent trace to fname+ext. Will check if the filename + exists, and let the user append to the fname if that is the case. + + Parameters + ---------- + fname : str, default :data:`~keyoscacquire.config._filename` + Filename of trace + ext : ``{'.csv', '.npy'}``, default :data:`~keyoscacquire.config._ext` + Choose the filetype of the saved trace + savepng : bool, default :data:`~keyoscacquire.config._export_png` + Choose whether to also save a png with the same filename + showplot : bool, default :data:`~keyoscacquire.config._show_plot` + Choose whether to show a plot of the trace + """ + if fname is not None: + self.fname = fname + if ext is not None: + self.ext = ext + if savepng is not None: + self.savepng = savepng + if showplot is not None: + self.showplot = showplot + self.fname = chsy.check_file(self.fname, self.ext) + traceio.plot_trace(self.time, self.y, self.channel_nums, fname=self.fname, + showplot=self.showplot, savepng=self.savepng) + head = self.generate_file_header() + traceio.save_trace(self.fname, self.time, self.y, fileheader=head, ext=self.ext, + print_filename=self.verbose_acquistion) + + def plot_trace(self): + """Plot and show the most recent trace""" + traceio.plot_trace(self.time, self.y, self.channel_nums, + savepng=False, show=True) ##============================================================================## ## DATA PROCESSING ## ##============================================================================## -def process_data(raw, metadata, wav_format, acquire_print=True): - """Wrapper function for choosing the correct process_data function according to :attr:`wav_format` for the data obtained from :func:`Oscilloscope.capture_and_read` +def process_data(raw, metadata, wav_format, verbose_acquistion=True): + """Wrapper function for choosing the correct process_data function + according to :attr:`wav_format` for the data obtained from + :func:`Oscilloscope.capture_and_read` Parameters ---------- raw : ~numpy.ndarray or str - From :func:`~Oscilloscope.capture_and_read`: Raw data, type depending on :attr:`wav_format` + From :func:`~Oscilloscope.capture_and_read`: Raw data, type depending + on :attr:`wav_format` metadata : list or tuple - From :func:`~Oscilloscope.capture_and_read`: List of preambles or tuple of preamble and model series depending on :attr:`wav_format`. See :ref:`preamble`. + From :func:`~Oscilloscope.capture_and_read`: List of preambles or + tuple of preamble and model series depending on :attr:`wav_format`. + See :ref:`preamble`. wav_format : {``'WORD'``, ``'BYTE'``, ``'ASCii'``} - Specify what waveform type was used for acquiring to choose the correct processing function. - acquire_print : bool + Specify what waveform type was used for acquiring to choose the correct + processing function. + verbose_acquistion : bool True prints the number of points captured per channel Returns @@ -685,22 +812,26 @@ def process_data(raw, metadata, wav_format, acquire_print=True): :func:`Oscilloscope.capture_and_read` """ if wav_format[:3] in ['WOR', 'BYT']: - return process_data_binary(raw, metadata, acquire_print) + return _process_data_binary(raw, metadata, verbose_acquistion) elif wav_format[:3] == 'ASC': - return process_data_ascii(raw, metadata, acquire_print) + return _process_data_ascii(raw, metadata, verbose_acquistion) else: raise ValueError("Could not process data, waveform format \'{}\' is unknown.".format(wav_format)) -def process_data_binary(raw, preambles, acquire_print=True): - """Process raw 8/16-bit data to time values and y voltage values as received from :func:`Oscilloscope.capture_and_read_binary`. +def _process_data_binary(raw, preambles, verbose_acquistion=True): + """Process raw 8/16-bit data to time values and y voltage values as received + from :func:`Oscilloscope.capture_and_read_binary`. Parameters ---------- raw : ~numpy.ndarray - From :func:`~Oscilloscope.capture_and_read_binary`: An ndarray of ints that is converted to voltage values using the preamble. + From :func:`~Oscilloscope.capture_and_read_binary`: An ndarray of ints + that is converted to voltage values using the preamble. preambles : list of str - From :func:`~Oscilloscope.capture_and_read_binary`: List of preamble metadata for each channel (list of comma separated ascii values, see :ref:`preamble`) - acquire_print : bool + From :func:`~Oscilloscope.capture_and_read_binary`: List of preamble + metadata for each channel (list of comma separated ascii values, + see :ref:`preamble`) + verbose_acquistion : bool True prints the number of points captured per channel Returns @@ -714,9 +845,9 @@ def process_data_binary(raw, preambles, acquire_print=True): preamble = preambles[0].split(',') # values separated by commas num_samples = int(float(preamble[2])) xIncr, xOrig, xRef = float(preamble[4]), float(preamble[5]), float(preamble[6]) - x = np.array([(np.arange(num_samples)-xRef)*xIncr + xOrig]) # compute x-values - x = x.T # make x values vertical - if acquire_print: + time = np.array([(np.arange(num_samples)-xRef)*xIncr + xOrig]) # compute x-values + time = time.T # make x values vertical + if verbose_acquistion: print("Points captured per channel: ", num_samples) _log.info("Points captured per channel: ", num_samples) y = np.empty((len(raw), num_samples)) @@ -725,18 +856,22 @@ def process_data_binary(raw, preambles, acquire_print=True): yIncr, yOrig, yRef = float(preamble[7]), float(preamble[8]), float(preamble[9]) y[i,:] = (data-yRef)*yIncr + yOrig y = y.T # convert y to np array and transpose for vertical channel columns in csv file - return x, y + return time, y -def process_data_ascii(raw, metadata, acquire_print=True): - """Process raw comma separated ascii data to time values and y voltage values as received from :func:`Oscilloscope.capture_and_read_ascii` +def _process_data_ascii(raw, metadata, verbose_acquistion=True): + """Process raw comma separated ascii data to time values and y voltage + values as received from :func:`Oscilloscope.capture_and_read_ascii` Parameters ---------- raw : str - From :func:`~Oscilloscope.capture_and_read_ascii`: A string containing a block header and comma separated ascii values + From :func:`~Oscilloscope.capture_and_read_ascii`: A string containing + a block header and comma separated ascii values metadata : tuple - From :func:`~Oscilloscope.capture_and_read_ascii`: Tuple of the preamble for one of the channels to calculate time axis (same for all channels) and the model series. See :ref:`preamble`. - acquire_print : bool + From :func:`~Oscilloscope.capture_and_read_ascii`: Tuple of the + preamble for one of the channels to calculate time axis (same for + all channels) and the model series. See :ref:`preamble`. + verbose_acquistion : bool True prints the number of points captured per channel Returns @@ -747,12 +882,13 @@ def process_data_ascii(raw, metadata, acquire_print=True): Voltage values, each row represents one channel """ preamble, model_series = metadata - preamble = preamble.split(',') # values separated by commas + preamble = preamble.split(',') # Values separated by commas num_samples = int(float(preamble[2])) xIncr, xOrig, xRef = float(preamble[4]), float(preamble[5]), float(preamble[6]) - x = np.array([(np.arange(num_samples)-xRef)*xIncr + xOrig]) # compute x-values - x = x.T # make list vertical - if acquire_print: + # Compute time axis and wrap in extra [] to make it 2D + time = np.array([(np.arange(num_samples)-xRef)*xIncr + xOrig]) + time = time.T # Make list vertical + if verbose_acquistion: print("Points captured per channel: ", num_samples) _log.info("Points captured per channel: ", num_samples) y = [] @@ -765,147 +901,17 @@ def process_data_ascii(raw, metadata, acquire_print=True): data = np.array([float(sample) for sample in data]) y.append(data) # add ascii data for this channel to y array y = np.transpose(np.array(y)) - return x, y - + return time, y -##============================================================================## -## SAVING FILES ## -##============================================================================## - -def check_file(fname, ext=config._filetype, num=""): - """Checking if file ``fname+num+ext`` exists. If it does, the user is prompted for a string to append to fname until a unique fname is found. - - Parameters - ---------- - fname : str - Base filename to test - ext : str, default :data:`~keyoscacquire.config._filetype` - File extension - num : str, default "" - Filename suffix that is tested for, but the appended part to the fname will be placed before it, - and the suffix will not be part of the returned fname - - Returns - ------- - fname : str - New fname base - """ - while os.path.exists(fname+num+ext): - append = input("File \'%s\' exists! Append to filename \'%s\' before saving: " % (fname+num+ext, fname)) - fname += append - return fname -def save_trace(fname, time, y, fileheader="", ext=config._filetype, print_filename=True): - """Saves the trace with time values and y values to file. +## Module main function ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## - Current date and time is automatically added to the header. Saving to numpy format with :func:`save_trace_npy` is faster, but does not include metadata and header. - - Parameters - ---------- - fname : str - Filename of trace - time : ~numpy.ndarray - Time axis for the measurement - y : ~numpy.ndarray - Voltage values, same sequence as channel_nums - fileheader : str, default ``""`` - Header of file, use :func:`generate_file_header` - ext : str, default :data:`~keyoscacquire.config._filetype` - Choose the filetype of the saved trace - print_filename : bool, default ``True`` - ``True`` prints the filename it is saved to - """ - if print_filename: print("Saving trace to %s\n" % (fname+ext)) - data = np.append(time, y, axis=1) # make one array with coloumns x y1 y2 .. - np.savetxt(fname+ext, data, delimiter=",", header=fileheader) - -def save_trace_npy(fname, time, y, print_filename=True): - """Saves the trace with time values and y values to npy file. - - .. note:: Saving to numpy files is faster than to ascii format files (:func:`save_trace`), but no file header is added. - - Parameters - ---------- - fname : str - Filename to save to - time : ~numpy.ndarray - Time axis for the measurement - y : ~numpy.ndarray - Voltage values, same sequence as channel_nums - print_filename : bool, default ``True`` - ``True`` prints the filename it is saved to - """ - if print_filename: print("Saving trace to %s\n" % (fname+ext)) - data = np.append(time, y, axis=1) - np.save(pathfname_no_ext+".npy", data) - -def plot_trace(time, y, channel_nums, fname="", show=config._show_plot, savepng=config._export_png): - """Plots the trace with oscilloscope channel screen colours according to the Keysight colourmap and saves as a png. - - .. Caution:: No filename check for the saved plot, can overwrite existing png files. - - Parameters - ---------- - time : ~numpy.ndarray - Time axis for the measurement - y : ~numpy.ndarray - Voltage values, same sequence as channel_nums - channel_nums : list of chars - list of the channels obtained, example ['1', '3'] - fname : str, default ``""`` - Filename of possible exported png - show : bool, default :data:`~keyoscacquire.config._show_plot` - True shows the plot (must be closed before the programme proceeds) - savepng : bool, default :data:`~keyoscacquire.`config._export_png` - ``True`` exports the plot to ``fname``.png - """ - for i, vals in enumerate(np.transpose(y)): # for each channel - plt.plot(time, vals, color=_screen_colors[channel_nums[i]]) - if savepng: plt.savefig(fname+".png", bbox_inches='tight') - if show: plt.show() - plt.close() - - -def interpret_visa_id(id): - """Interprets VISA ID, finds oscilloscope model series if applicable - - Parameters - ---------- - id : str - VISA ID as returned by the ``*IDN?`` command - - Returns - ------- - maker : str - Maker of the instrument, e.g. Keysight Technologies - model : str - Model of the instrument - serial : str - Serial number of the instrument - firmware : str - Firmware version - model_series : str - "N/A" unless the instrument is a Keysight/Agilent DSO and MSO oscilloscope. - Returns the model series, e.g. '2000'. Returns "not found" if the model name cannot be interpreted. - """ - maker, model, serial, firmware = id.split(",") - # find model_series if applicable - if model[:3] in ['DSO', 'MSO']: - model_number = [c for c in model if c.isdigit()] # find the numbers in the model string - model_series = model_number[0]+'000' if not model_number == [] else "not found" # pick the first number and add 000 or use not found - else: - model_series = "N/A" - return maker, model, serial, firmware, model_series +def main(): + fname = sys.argv[1] if len(sys.argv) >= 2 else config._filename + ext = config._filetype + with Oscilloscope() as scope: + scope.set_options_get_trace_save(fname, ext) -##============================================================================## -## MAIN FUNCTION ## -##============================================================================## - -## Main function, runs only if the script is called from the command line if __name__ == '__main__': - fname = sys.argv[1] if len(sys.argv) >= 2 else config._filename - ext = config._filetype - scope = Oscilloscope() - scope.set_options_get_trace_save(fname, ext) - scope.close() + main() diff --git a/keyoscacquire/programmes.py b/keyoscacquire/programmes.py index 6aae8ae..fa6ce36 100644 --- a/keyoscacquire/programmes.py +++ b/keyoscacquire/programmes.py @@ -2,63 +2,77 @@ """ Python backend for installed command line programmes. These can also be integrated in python scripts or used as examples. -* :func:`list_visa_devices`: listing visa devices - -* :func:`path_of_config`: finding path of config.py - -* :func:`get_single_trace`: taking a single trace and saving it to csv and png - -* :func:`get_traces_single_connection_loop` :func:`get_traces_connect_each_time_loop`: two programmes for taking multiple traces when a key is pressed, see descriptions for difference - -* :func:`get_num_traces`: get a specific number of traces + * :func:`list_visa_devices`: listing visa devices + * :func:`path_of_config`: finding path of config.py + * :func:`get_single_trace`: taking a single trace and saving it to csv and png + * :func:`get_traces_single_connection_loop` :func:`get_traces_connect_each_time_loop`: two programmes for taking multiple traces when a key is pressed, see descriptions for difference + * :func:`get_num_traces`: get a specific number of traces """ -import sys, logging; _log = logging.getLogger(__name__) +import os +import sys +import pyvisa +import logging; _log = logging.getLogger(__name__) import keyoscacquire.oscacq as acq import numpy as np -from tqdm import tqdm #progressbar +from tqdm import tqdm # local file with default options: import keyoscacquire.config as config +import keyoscacquire.config as auxiliary def list_visa_devices(ask_idn=True): - """Prints a list of the VISA instruments connected to the computer, including their addresses.""" - import pyvisa + """Prints a list of the VISA instruments connected to the computer, + including their addresses.""" rm = pyvisa.ResourceManager() resources = rm.list_resources() if len(resources) == 0: print("\nNo VISA devices found!") else: - print("Found {} resources. Now obtaining information about them..".format(len(resources))) + print(f"\nFound {len(resources)} resources. Now obtaining information about them..") information, could_not_connect = [], [] - for i, address in enumerate(resources): # loop through resources to learn more about them + # Loop through resources to learn more about them + for i, address in enumerate(resources): current_resource_info = [] info_object = rm.resource_info(address) alias = info_object.alias if info_object.alias is not None else "N/A" current_resource_info.extend((str(i), address, alias)) if ask_idn: - try: # open the instrument and get the identity string + # Open the instrument and get the identity string + try: + error_flag = False instrument = rm.open_resource(address) - id = instrument.query('*IDN?') - current_resource_info.extend(acq.interpret_visa_id(id)) + id = instrument.query("*IDN?").strip() instrument.close() except pyvisa.Error as e: + error_flag = True could_not_connect.append(i) - current_resource_info.extend(["no connection"]*5) + current_resource_info.extend(["no IDN response"]*5) + print(f"Instrument #{i}: Did not respond to *IDN?: {e}") except Exception as ex: - print("(i) Got exception %s when asking instrument #%i for identity." % (ex.__class__.__name__, i)) + error_flag = True + print(f"Instrument #{i}: Got exception {ex.__class__.__name__} " + f"when asking for its identity.") could_not_connect.append(i) current_resource_info.extend(["Error"]*5) + if not error_flag: + try: + current_resource_info.extend(auxiliary.interpret_visa_id(id)) + except Exception as ex: + print(f"Instrument #{i}: Could not interpret VISA id, got" + f"exception {ex.__class__.__name__}: VISA id returned was '{id}'") + could_not_connect.append(i) + current_resource_info.extend(["failed to interpret"]*5) information.append(current_resource_info) if ask_idn: # transpose to lists of property nums, addrs, aliases, makers, models, serials, firmwares, model_series = (list(category) for category in zip(*information)) # select what properties to list - selection = (nums, addrs, makers, models, model_series, aliases) + selection = (nums, addrs, makers, models, firmware, aliases) # name columns - header_fields = (' #', 'address', 'maker', 'model', 'series', 'alias') + header_fields = (' #', 'address', 'maker', 'model', 'firmware', 'alias') row_format = "{:>{p[0]}s} {:{p[1]}s} {:{p[2]}s} {:{p[3]}s} {:{p[4]}s} {:{p[5]}s}" else: selection = [list(category) for category in zip(*information)] @@ -76,10 +90,11 @@ def list_visa_devices(ask_idn=True): for info in zip(*selection): print(row_format.format(*info, p=padding)) + def path_of_config(): """Print the absolute path of the config.py file.""" - import os - print("config.py can be found in:\n\t%s\n" % os.path.dirname(os.path.abspath(__file__))) + print("config.py can be found in:") + print(f" {os.path.dirname(os.path.abspath(__file__))}\n") def get_single_trace(fname=config._filename, ext=config._filetype, address=config._visa_address, @@ -87,11 +102,10 @@ def get_single_trace(fname=config._filename, ext=config._filetype, address=confi channel_nums=config._ch_nums, source_type='CHANnel', acq_type=config._acq_type, num_averages=config._num_avg, p_mode='RAW', num_points=0): """This programme captures and stores a single trace.""" - scope = acq.Oscilloscope(address=address, timeout=timeout) - scope.set_options_get_trace_save(fname=fname, ext=ext, wav_format=wav_format, - channel_nums=channel_nums, source_type=source_type, acq_type=acq_type, - num_averages=num_averages, p_mode=p_mode, num_points=num_points) - scope.close() + with acq.Oscilloscope(address=address, timeout=timeout) as scope: + scope.set_options_get_trace_save(fname=fname, ext=ext, wav_format=wav_format, + channel_nums=channel_nums, source_type=source_type, acq_type=acq_type, + num_averages=num_averages, p_mode=p_mode, num_points=num_points) print("Done") @@ -108,24 +122,25 @@ def get_traces_connect_each_time_loop(fname=config._filename, ext=config._filety The loop runs each time 'enter' is hit. Alternatively one can input n-1 characters before hitting 'enter' to capture n traces back to back. To quit press 'q'+'enter'. """ + # Check that file does not exist from before, append to name if it does n = start_num fnum = file_delim+str(n) - fname = acq.check_file(fname, ext, num=fnum) # check that file does not exist from before, append to name if it does + fname = acq.check_file(fname, ext, num=fnum) print("Running a loop where at every 'enter' oscilloscope traces will be saved as %s%s," % (fname, ext)) print("where increases by one for each captured trace. Press 'q'+'enter' to quit the programme.") while sys.stdin.read(1) != 'q': # breaks the loop if q+enter is given as input. For any other character (incl. enter) fnum = file_delim+str(n) - scope = acq.Oscilloscope(address=address, timeout=timeout) - x, y, channels = scope.set_options_get_trace(wav_format=wav_format, - channel_nums=channel_nums, source_type=source_type, acq_type=acq_type, - num_averages=num_averages, p_mode=p_mode, num_points=num_points) - acq.plot_trace(x, y, channels, fname=fname+fnum) - fhead = scope.generate_file_header(channel_nums) + with acq.Oscilloscope(address=address, timeout=timeout) as scope: + x, y, channels = scope.set_options_get_trace(wav_format=wav_format, + channel_nums=channel_nums, source_type=source_type, acq_type=acq_type, + num_averages=num_averages, p_mode=p_mode, num_points=num_points) + acq.plot_trace(x, y, channels, fname=fname+fnum) + fhead = scope.generate_file_header() acq.save_trace(fname+fnum, x, y, fileheader=fhead, ext=ext) - scope.close() n += 1 print("Quit") + def get_traces_single_connection_loop(fname=config._filename, ext=config._filetype, address=config._visa_address, timeout=config._timeout, wav_format=config._waveform_format, channel_nums=config._ch_nums, source_type='CHANnel', acq_type=config._acq_type, @@ -139,52 +154,50 @@ def get_traces_single_connection_loop(fname=config._filename, ext=config._filety a trace is captured. The downside is that which channels are being captured cannot be changing thoughout the measurements. """ - ## Initialise - scope = acq.Oscilloscope(address=address, timeout=timeout) - scope.set_acquiring_options(wav_format=wav_format, acq_type=acq_type, - num_averages=num_averages, p_mode=p_mode, - num_points=num_points) - ## Select sources - sources, sourcesstring, channel_nums = scope.determine_channels(source_type=source_type, channel_nums=channel_nums) - fhead = scope.generate_file_header(channel_nums) - n = start_num - fnum = file_delim+str(n) - fname = acq.check_file(fname, ext, num=fnum) # check that file does not exist from before, append to name if it does - print("Running a loop where at every 'enter' oscilloscope traces will be saved as %s%s," % (fname, ext)) - print("where increases by one for each captured trace. Press 'q'+'enter' to quit the programme.") - while sys.stdin.read(1) != 'q': # breaks the loop if q+enter is given as input. For any other character (incl. enter) + with acq.Oscilloscope(address=address, timeout=timeout) as scope: + ## Initialise + scope.set_acquiring_options(wav_format=wav_format, acq_type=acq_type, + num_averages=num_averages, p_mode=p_mode, + num_points=num_points) + ## Select sources + scope.set_channels_for_capture(channel_nums=channel_nums) + fhead = scope.generate_file_header() + # Check that file does not exist from before, append to name if it does + n = start_num fnum = file_delim+str(n) - x, y = scope.get_trace(sources, sourcesstring) - acq.plot_trace(x, y, channel_nums, fname=fname+fnum) # plot trace and save png - acq.save_trace(fname+fnum, x, y, fileheader=fhead, ext=ext) # save trace to ext file - n += 1 - + fname = acq.check_file(fname, ext, num=fnum) + print("Running a loop where at every 'enter' oscilloscope traces will be saved as %s%s," % (fname, ext)) + print("where increases by one for each captured trace. Press 'q'+'enter' to quit the programme.") + while sys.stdin.read(1) != 'q': # breaks the loop if q+enter is given as input. For any other character (incl. enter) + fnum = file_delim+str(n) + x, y, channel_nums = scope.get_trace() + acq.plot_trace(x, y, channel_nums, fname=fname+fnum) # plot trace and save png + acq.save_trace(fname+fnum, x, y, fileheader=fhead, ext=ext) # save trace to ext file + n += 1 print("Quit") - scope.close() def get_num_traces(fname=config._filename, ext=config._filetype, num=1, address=config._visa_address, timeout=config._timeout, wav_format=config._waveform_format, channel_nums=config._ch_nums, source_type='CHANnel', acq_type=config._acq_type, num_averages=config._num_avg, p_mode='RAW', num_points=0, start_num=0, file_delim=config._file_delimiter): - """This program connects to the oscilloscope, sets options for the - acquisition, and captures and stores 'num' traces. - """ - ## Initialise - scope = acq.Oscilloscope(address=address, timeout=timeout) + """This program connects to the oscilloscope, sets options for the + acquisition, and captures and stores 'num' traces. + """ + with acq.Oscilloscope(address=address, timeout=timeout) as scope: scope.set_acquiring_options(wav_format=wav_format, acq_type=acq_type, num_averages=num_averages, p_mode=p_mode, num_points=num_points, acq_print=False) ## Select sources - sources, sourcesstring, channel_nums = scope.determine_channels(source_type=source_type, channel_nums=channel_nums) - fhead = scope.generate_file_header(channel_nums) + scope.set_channels_for_capture(channel_nums=channel_nums) + fhead = scope.generate_file_header() n = start_num fnum = file_delim+str(n) - fname = acq.check_file(fname, ext, num=fnum) # check that file does not exist from before, append to name if it does + # Check that file does not exist from before, append to name if it does + fname = acq.check_file(fname, ext, num=fnum) for i in tqdm(range(n, n+num)): fnum = file_delim+str(i) - x, y = scope.get_trace(sources, sourcesstring, acquire_print=(i==n)) + x, y, channel_nums = scope.get_trace(acquire_print=(i==n)) #acq.plot_trace(x, y, channel_nums, fname=fname+fnum) # plot trace and save png acq.save_trace(fname+fnum, x, y, fileheader=fhead, ext=ext, print_filename=(i==n)) # save trace to ext file - print("Done") - scope.close() + print("Done") diff --git a/keyoscacquire/scripts/example.py b/keyoscacquire/scripts/example.py index a6c7194..3a857e2 100644 --- a/keyoscacquire/scripts/example.py +++ b/keyoscacquire/scripts/example.py @@ -2,9 +2,8 @@ import matplotlib.pyplot as plt def get_averaged(osc_address, averages=8): - scope = koa.Oscilloscope(address=osc_address) - time, volts, channels = scope.set_options_get_trace(acq_type='AVER'+str(averages)) - scope.close() + with koa.Oscilloscope(address=osc_address) as scope: + time, volts, channels = scope.set_options_get_trace(acq_type='AVER'+str(averages)) return time, volts, channels time, volts, channels = get_averaged('USB0::1234::1234::MY1234567::INSTR') diff --git a/keyoscacquire/traceio.py b/keyoscacquire/traceio.py new file mode 100644 index 0000000..c1a7ee3 --- /dev/null +++ b/keyoscacquire/traceio.py @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- +""" +Trace input/output functions for the keyoscacquire package + +Andreas Svela // 2020 +""" + +import logging; _log = logging.getLogger(__name__) +import numpy as np +import pandas as pd + +import keyoscacquire.config as config +import keyoscacquire.oscacq as oscacq + + +## Trace saving and plotting ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## + +def save_trace(fname, time, y, fileheader="", ext=config._filetype, + print_filename=True, nowarn=False): + """Saves the trace with time values and y values to file. + + Current date and time is automatically added to the header. Saving to numpy + format with :func:`save_trace_npy` is faster, but does not include metadata + and header. + + Parameters + ---------- + fname : str + Filename of trace + time : ~numpy.ndarray + Time axis for the measurement + y : ~numpy.ndarray + Voltage values, same sequence as channel_nums + fileheader : str, default ``""`` + Header of file, use :func:`generate_file_header` + ext : str, default :data:`~keyoscacquire.config._filetype` + Choose the filetype of the saved trace + print_filename : bool, default ``True`` + ``True`` prints the filename it is saved to + + Raises + ------ + RuntimeError + If the file already exists + """ + if os.path.exists(fname+ext): + raise RuntimeError(f"{fname+ext} already exists") + if print_filename: + print(f"Saving trace to {fname+ext}\n") + data = np.append(time, y, axis=1) # make one array with columns x y1 y2 .. + if ext == ".npy": + if fileheader or nowarn: + _log.warning(f"File header {fileheader} is not saved as file format npy is chosen. " + "To surpress this warning, use save_trace_npy() instead or the nowarn flag") + np.save(fname+".npy", data) + else: + np.savetxt(fname+ext, data, delimiter=",", header=fileheader) + + +def save_trace_npy(fname, time, y, print_filename=True, **kwargs): + """Saves the trace with time values and y values to npy file. + + .. note:: Saving to numpy files is faster than to ascii format files + (:func:`save_trace`), but no file header is added. + + Parameters + ---------- + fname : str + Filename to save to + time : ~numpy.ndarray + Time axis for the measurement + y : ~numpy.ndarray + Voltage values, same sequence as channel_nums + print_filename : bool, default ``True`` + ``True`` prints the filename it is saved to + """ + save_trace(fname, time, y, ext=".npy", nowarn=True, print_filename=print_filename) + + +def plot_trace(time, y, channel_nums, fname="", show=config._show_plot, savepng=config._export_png): + """Plots the trace with oscilloscope channel screen colours according to + the Keysight colourmap and saves as a png. + + .. Caution:: No filename check for the saved plot, can overwrite + existing png files. + + Parameters + ---------- + time : ~numpy.ndarray + Time axis for the measurement + y : ~numpy.ndarray + Voltage values, same sequence as channel_nums + channel_nums : list of chars + list of the channels obtained, example ['1', '3'] + fname : str, default ``""`` + Filename of possible exported png + show : bool, default :data:`~keyoscacquire.config._show_plot` + True shows the plot (must be closed before the programme proceeds) + savepng : bool, default :data:`~keyoscacquire.`config._export_png` + ``True`` exports the plot to ``fname``.png + """ + for i, vals in enumerate(np.transpose(y)): # for each channel + plt.plot(time, vals, color=oscacq._screen_colors[channel_nums[i]]) + if savepng: + plt.savefig(fname+".png", bbox_inches='tight') + if show: + plt.show() + plt.close() + + +## Trace loading ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## + +def load_trace(fname, ext=config._filetype, column_names='auto', skip_lines='auto', return_df=True): + """Load a trace saved with keyoscacquire.oscacq.save_file() + + Parameters + ---------- + fname : str + Filename of trace, with or without extension + ext : str, default :data:`~keyoscacquire.config._filetype` + The filetype of the saved trace (with the period, e.g. '.csv') + column_names : ``{'auto' or list-like}``, default ``'auto'`` + Only useful if using with ``return_df=True``: + To infer df column names from the last line of the header, use ``'auto'`` + (expecting '# ' as the last line of the + header), or specify the column names manually + + skip_lines : ``{'auto' or int}``, default ``'auto'`` + + return_df : bool, default True + + Returns + ------- + + """ + # Remove extenstion if provided in the fname + if fname[-4:] in ['.npy', '.csv']: + ext = fname[-4:] + fname = fname[:-4] + # Format dependent + if ext == '.npy': + return np.load(fname+ext), None + else: + return _load_trace_with_header(fname, ext, column_names=column_names, + skip_lines=skip_lines, + return_df=return_df) + + +def _load_trace_with_header(fname, ext, skip_lines='auto', column_names='auto', return_df=True): + """ + + Parameters + ---------- + fname : str + Filename of trace, with or without extension + ext : str, default :data:`~keyoscacquire.config._filetype` + The filetype of the saved trace (with the period, e.g. ``'.csv'``) + + Returns + ------- + data : + :class:`~pandas.Dataframe` or :class:`~numpy.ndarray` + header : list + Lines at the beginning of the file starting with ``'#'``, stripped off ``'# '`` + """ + # Load header + header = load_header(fname, ext) + # Handle skipping and column names based on the header file + if skip_lines == 'auto': + skip_lines = len(header) + if column_names == 'auto': + column_names = header[-1].split(",") + # Load the file + df = pd.read_csv(fname+ext, delimiter=",", skiprows=skip_lines, names=column_names) + # Return df or array + if return_df: + return df, header + else: + return np.array([df[col].values for col in df.columns]), header + +def load_header(fname, ext=config._filetype): + """Open a trace file and get the header + + Parameters + ---------- + fname : str + Filename of trace, with or without extension + ext : str, default :data:`~keyoscacquire.config._filetype` + The filetype of the saved trace (with the period, e.g. ``'.csv'``) + + Returns + ------- + header : list + Lines at the beginning of the file starting with ``'#'``, stripped off ``'# '`` + """ + if fname[-4:] in ['.csv']: + ext = fname[-4:] + fname = fname[:-4] + header = [] + with open(fname+ext) as f: + for line in f: # A few tens of us slower than a 'while True: readline()' approach + # Check if part of the header + if line[0] == '#': + # Add the line without the initial '# ' to the header + header.append(line.strip()[2:]) + else: + break + return header diff --git a/tests/format_comparison.py b/tests/format_comparison.py index 2be1bd8..0dc559c 100644 --- a/tests/format_comparison.py +++ b/tests/format_comparison.py @@ -13,7 +13,7 @@ import numpy as np import matplotlib.pyplot as plt -import keyoscacquire.oscacq as koa +import keyoscacquire as koa format_dict = {0: "BYTE", 1: "WORD", 4: "ASCii"} formats = ['BYTE', 'WORD', 'ASCii'] @@ -23,15 +23,15 @@ scope = koa.Oscilloscope(address=visa_address) scope.set_acquiring_options(num_points=2000) -sources, sourcesstring, channel_nums = scope.determine_channels(channel_nums=['1']) +scope.set_channels_for_capture(channel_nums=['1']) scope.stop() times, values = [[], []], [[], []] for wav_format in formats: print("\nWaveform format", wav_format) scope.set_acquiring_options(wav_format=wav_format) - data = scope.capture_and_read(sources, sourcesstring, set_running=False) - time, vals = koa.process_data(*data, wav_format, acquire_print=True) + scope.capture_and_read(set_running=False) + time, vals = koa.process_data(scope.raw, scope.metadata, wav_format, acquire_print=True) times[0].append(time) values[0].append(vals) From c7f5679e91eb59a1ecd89c74bdde2f889691b7e9 Mon Sep 17 00:00:00 2001 From: asvela at 0011 Date: Fri, 18 Dec 2020 12:10:10 +0100 Subject: [PATCH 02/52] WIP bugfixes for changes made for v4.0, improved failed query handling --- docs/changelog.rst | 11 +- keyoscacquire/__init__.py | 2 +- keyoscacquire/oscacq.py | 209 +++++++++++++++++++++----------------- keyoscacquire/traceio.py | 9 +- 4 files changed, 130 insertions(+), 101 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 90ec61c..3b4263d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -14,7 +14,8 @@ v4.0.0 (2020-12) - More attributes are used to make the information accessible not only through returns * Captured data stored to ``Oscilloscope.time`` and ``Oscilloscope.y`` - * Fname used (not the argument as it might be updated duing saving process) + * The filename finally used when saving (which might not be the same as the + the argument passed as a filename check happens to avoid overwrite) is stored in ``Oscilloscope.fname`` * ``Oscilloscope.raw`` and ``Oscilloscope.metadata`` are now available @@ -43,7 +44,8 @@ v4.0.0 (2020-12) ask for instrument IDNs; and the cli programme will display the instrument's firmware rather than Keysight model series. - - Indicating functions for internal and external use by prefix ``_`` + - Indicating functions for internal use only and read only attributes with + prefix ``_``, see name changes below - Documentation updates, including moving from read-the-docs theme to Furo theme @@ -76,6 +78,11 @@ v4.0.0 (2020-12) (also major changes in the function) * ``Oscilloscope.capture_and_read_binary()`` -> ``Oscilloscope._read_binary()`` (also major changes in the function) + * ``Oscilloscope.inst`` -> ``Oscilloscope._inst`` + * ``Oscilloscope.id`` -> ``Oscilloscope._id`` + * ``Oscilloscope.address`` -> ``Oscilloscope._address`` + * ``Oscilloscope._model`` -> ``Oscilloscope._model`` + * ``Oscilloscope.model_series`` -> ``Oscilloscope._model_series`` - *No compatibility*: Moved functions diff --git a/keyoscacquire/__init__.py b/keyoscacquire/__init__.py index c6b320b..b9c0fb0 100644 --- a/keyoscacquire/__init__.py +++ b/keyoscacquire/__init__.py @@ -15,7 +15,7 @@ import logging; _log = logging.getLogger(__name__) import keyoscacquire.oscacq as oscacq -import keyoscacquire.auxiliary as traceio +import keyoscacquire.traceio as traceio import keyoscacquire.config as config import keyoscacquire.programmes as programmes import keyoscacquire.auxiliary as auxiliary diff --git a/keyoscacquire/oscacq.py b/keyoscacquire/oscacq.py index 9dade2c..fbc67b3 100644 --- a/keyoscacquire/oscacq.py +++ b/keyoscacquire/oscacq.py @@ -61,19 +61,19 @@ class Oscilloscope: Attributes ---------- - inst : pyvisa.resources.Resource + _inst : pyvisa.resources.Resource The oscilloscope PyVISA resource - id : str + _id : str The maker, model, serial and firmware version of the scope. Examples:: 'AGILENT TECHNOLOGIES,DSO-X 2024A,MY1234567,12.34.567891234' 'KEYSIGHT TECHNOLOGIES,MSO9104A,MY12345678,06.30.00609' - model : str + _model : str The instrument model name - model_series : str + _model_series : str The model series, e.g. '2000' for a DSO-X 2024A. See :func:`keyoscacquire.auxiliary.interpret_visa_id`. - address : str + _address : str Visa address of instrument timeout : int Milliseconds before timeout on the channel to the instrument @@ -121,47 +121,51 @@ class Oscilloscope: metadata = None time = None y = None - source_type = 'CHANNel' + source_type = 'CHAN' fname = config._filename ext = config._filetype savepng = config._export_png showplot = config._show_plot + channel_nums = config._ch_nums def __init__(self, address=config._visa_address, timeout=config._timeout, verbose=True): """See class docstring""" - self.address = address + self._address = address self.timeout = timeout self.verbose = verbose self.verbose_acquistion = verbose # Connect to the scope try: rm = pyvisa.ResourceManager() - self.inst = rm.open_resource(address) + self._inst = rm.open_resource(address) except pyvisa.Error as err: print(f"\n\nCould not connect to '{address}', see traceback below:\n") raise - self.inst.timeout = self.timeout + self._inst.timeout = self.timeout # For TCP/IP socket connections enable the read Termination Character, or reads will timeout - if self.inst.resource_name.endswith('SOCKET'): - self.inst.read_termination = '\n' + if self._inst.resource_name.endswith('SOCKET'): + self._inst.read_termination = '\n' # Clear the status data structures, the device-defined error queue, and the Request-for-OPC flag - self.inst.write('*CLS') + self._inst.write('*CLS') # Make sure WORD and BYTE data is transeferred as signed ints and lease significant bit first - self.inst.write(':WAVeform:UNSigned OFF') - self.inst.write(':WAVeform:BYTeorder LSBFirst') # MSBF is default, must be overridden for WORD to work + self._inst.write(':WAVeform:UNSigned OFF') + self._inst.write(':WAVeform:BYTeorder LSBFirst') # MSBF is default, must be overridden for WORD to work # Get information about the connected device - self.id = self.inst.query('*IDN?').strip() # get the id of the connected device + self._id = self.query('*IDN?') if self.verbose: - print(f"Connected to '{self.id}'") - _, self.model, _, _, self.model_series = chsy.interpret_visa_id(self.id) - if not self.model_series in _supported_series: - print("(!) WARNING: This model (%s) is not yet fully supported by keyoscacquire," % self.model) + print(f"Connected to '{self._id}'") + _, self._model, _, _, self._model_series = auxiliary.interpret_visa_id(self._id) + if not self._model_series in _supported_series: + print("(!) WARNING: This model (%s) is not yet fully supported by keyoscacquire," % self._model) print(" but might work to some extent. keyoscacquire supports Keysight's") print(" InfiniiVision X-series oscilloscopes.") # Populate attributes and set standard settings + if self.verbose: + print("Current settings:") self.set_acquiring_options(wav_format=config._waveform_format, acq_type=config._acq_type, num_averages=config._num_avg, p_mode='RAW', num_points=0, verbose_acquistion=verbose) + print(" ", end="") self.set_channels_for_capture(channel_nums=config._ch_nums) def __enter__(self): @@ -177,24 +181,40 @@ def write(self, command): ---------- command : str VISA command to be written""" - self.inst.write(command) + self._inst.write(command) - def query(self, command): + def query(self, command, action=""): """Query a VISA command to the oscilloscope. Parameters ---------- command : str - VISA query""" - return self.inst.query(command) + VISA query + action : str, default "" + + """ + try: + return self._inst.query(command).strip() + except pyvisa.Error as err: + if action: + msg = f"{action} (command '{command}')" + else: + msg = f"query '{command}'" + print(f"\nVisaError: {err}\n When trying {msg}.") + print(f" Have you checked that the timeout (currently {self.timeout:,d} ms) is sufficently long?") + try: + print(f"Latest error from the oscilloscope: '{self.get_error()}'\n") + except Exception: + print("Could not retrieve error from the oscilloscope") + raise def run(self): """Set the ocilloscope to running mode.""" - self.inst.write(':RUN') + self._inst.write(':RUN') def stop(self): """Stop the oscilloscope.""" - self.inst.write(':STOP') + self._inst.write(':STOP') def is_running(self): """Determine if the oscilloscope is running. @@ -205,7 +225,7 @@ def is_running(self): ``True`` if running, ``False`` otherwise """ # The third bit of the operation register is 1 if the instrument is running - reg = int(self.inst.query(':OPERegister:CONDition?')) + reg = int(self.query(':OPERegister:CONDition?')) return (reg & 8) == 8 def close(self, set_running=True): @@ -220,8 +240,8 @@ def close(self, set_running=True): # Set the oscilloscope running before closing the connection if set_running: self.run() - self.inst.close() - _log.debug(f"Closed connection to '{self.id}'") + self._inst.close() + _log.debug(f"Closed connection to '{self._id}'") def get_error(self): """Get the latest error @@ -231,7 +251,8 @@ def get_error(self): str error number,description """ - return self.query(":SYSTem:ERRor?").strip() + # Do not use self.query here as that can lead to infinite nesting! + return self._inst.query(":SYSTem:ERRor?") def get_active_channels(self): """Get list of the currently active channels on the instrument @@ -243,7 +264,7 @@ def get_active_channels(self): """ channels = np.array(['1', '2', '3', '4']) # querying DISP for each channel to determine which channels are currently displayed - displayed_channels = [self.inst.query(f":CHANnel{channel}:DISPlay?")[0] for channel in channels] + displayed_channels = [self.query(f":CHANnel{channel}:DISPlay?")[0] for channel in channels] # get a mask of bools for the channels that are on [need the int() as bool('0') = True] channel_mask = np.array([bool(int(i)) for i in displayed_channels]) # apply mask to the channel list @@ -300,14 +321,14 @@ def set_acquiring_options(self, wav_format=None, acq_type=None, else: if num_averages is not None: self.num_averages = num_averages - self.inst.write(':ACQuire:TYPE ' + self.acq_type) + self._inst.write(':ACQuire:TYPE ' + self.acq_type) if self.verbose: print(" Acquisition type:", self.acq_type) # Check that self.num_averages is within the acceptable range if self.acq_type == 'AVER': if not (1 <= self.num_averages <= 65536): raise ValueError(f"\nThe number of averages {self.num_averages} is out of range.") - self.inst.write(f":ACQuire:COUNt {self.num_averages}") + self._inst.write(f":ACQuire:COUNt {self.num_averages}") if self.verbose: print(" # of averages: ", self.num_averages) # Set options for waveform export @@ -331,7 +352,7 @@ def set_waveform_export_options(self, wav_format=None, num_points=None, p_mode=N """ # Choose format for the transmitted waveform if wav_format is not None: - self.inst.write(f":WAVeform:FORMat {wav_format}") + self._inst.write(f":WAVeform:FORMat {wav_format}") self.wav_format = wav_format if p_mode is not None: self.p_mode = p_mode @@ -341,23 +362,24 @@ def set_waveform_export_options(self, wav_format=None, num_points=None, p_mode=N self.p_mode = 'NORM' _log.debug(f":WAVeform:POINts:MODE overridden (from {p_mode}) to " "NORMal due to :ACQuire:TYPE:AVERage.") - self.inst.write(f":WAVeform:POINts:MODE {self.p_mode}") + self._inst.write(f":WAVeform:POINts:MODE {self.p_mode}") # Set to maximum number of points if - if num_points == 0: - self.inst.write(f":WAVeform:POINts MAXimum") - _log.debug("Number of points set to: MAX") - self.num_points = num_points - # If number of points has been specified, tell the instrument to - # use this number of points - elif num_points > 0: - if self.model_series in _supported_series: - self.inst.write(f":WAVeform:POINts {self.num_points}") - elif self.model_series in ['9000']: - self.inst.write(f":ACQuire:POINts {self.num_points}") - else: - self.inst.write(f":WAVeform:POINts {self.num_points}") - _log.debug("Number of points set to: ", self.num_points) - self.num_points = num_points + if num_points is not None: + if num_points == 0: + self._inst.write(f":WAVeform:POINts MAXimum") + _log.debug("Number of points set to: MAX") + self.num_points = num_points + # If number of points has been specified, tell the instrument to + # use this number of points + elif num_points > 0: + if self._model_series in _supported_series: + self._inst.write(f":WAVeform:POINts {self.num_points}") + elif self._model_series in ['9000']: + self._inst.write(f":ACQuire:POINts {self.num_points}") + else: + self._inst.write(f":WAVeform:POINts {self.num_points}") + _log.debug("Number of points set to: ", self.num_points) + self.num_points = num_points ## Capture and read functions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## @@ -434,7 +456,7 @@ def capture_and_read(self, set_running=True): # DIGitize is a specialized RUN command. # Waveforms are acquired according to the settings of the :ACQuire commands. # When acquisition is complete, the instrument is stopped. - self.inst.write(':DIGitize ' + self.sourcesstring) + self._inst.write(':DIGitize ' + self.sourcesstring) ## Read from the scope if self.wav_format[:3] in ['WOR', 'BYT']: self._read_binary(datatype=_datatypes[self.wav_format]) @@ -481,22 +503,20 @@ def _read_binary(self, datatype='standard'): # Loop through all the sources for source in self.sources: # Select the channel for which the succeeding WAVeform commands applies to - self.inst.write(':WAVeform:SOURce ' + source) + self._inst.write(':WAVeform:SOURce ' + source) try: # obtain comma separated metadata values for processing of raw data for this source - self.metadata.append(self.inst.query(':WAVeform:PREamble?')) + self.metadata.append(self.query(':WAVeform:PREamble?')) # obtain the data # read out data for this source - self.raw.append(self.inst.query_binary_values(':WAVeform:DATA?', datatype=datatype, container=np.array)) + self.raw.append(self._inst.query_binary_values(':WAVeform:DATA?', datatype=datatype, container=np.array)) except pyvisa.Error as err: - print(f"\n\nError: Failed to obtain waveform, have you checked that" - f" the timeout (currently {self.timeout:,d} ms) is sufficently long?") + print(f"\n\nVisaError: {err}\n When trying to obtain the waveform.") + print(f" Have you checked that the timeout (currently {self.timeout:,d} ms) is sufficently long?") try: print(f"Latest error from the oscilloscope: '{self.get_error()}'\n") except Exception: print("Could not retrieve error from the oscilloscope") - self.close() - print(err) raise def _read_ascii(self): @@ -524,24 +544,13 @@ def _read_ascii(self): # Loop through all the sources for source in self.sources: # Select the channel for which the succeeding WAVeform commands applies to - self.inst.write(':WAVeform:SOURce ' + source) - try: - # Read out data for this source - self.raw.append(self.inst.query(':WAVeform:DATA?')) - except pyvisa.Error: - print(f"\nVisaError: Failed to obtain waveform, have you checked that" - f" the timeout (currently {self.timeout:,d} ms) is sufficently long?") - try: - print(f"Latest error from the oscilloscope: '{self.get_error()}'\n") - except Exception: - print("Could not retrieve error from the oscilloscope") - self.close() - print(err) - raise + self._inst.write(':WAVeform:SOURce ' + source) + # Read out data for this source + self.raw.append(self.query(':WAVeform:DATA?', action="obtain the waveform")) # Get the preamble (used for calculating time axis, which is the same # for all channels) - preamble = self.inst.query(':WAVeform:PREamble?') - self.metadata = (preamble, self.model_series) + preamble = self.query(':WAVeform:PREamble?') + self.metadata = (preamble, self._model_series) ## Building functions to get a trace and various option setting and processing ## @@ -576,7 +585,7 @@ def get_trace(self, verbose_acquistion=None): # Restore self.verbose_acquistion to previous setting if verbose_acquistion is not None: self.verbose_acquistion = temp - return self.time, self.y, self.num_channels + return self.time, self.y, self.channel_nums def set_options_get_trace(self, channel_nums=None, source_type=None, wav_format=None, acq_type=None, @@ -620,7 +629,7 @@ def set_options_get_trace(self, channel_nums=None, source_type=None, num_averages=num_averages, p_mode=p_mode, num_points=num_points) ## Select sources - self.set_channels_for_capture(source_type=source_type, channel_nums=channel_nums) + self.set_channels_for_capture(channel_nums=channel_nums) ## Capture, read and process data self.get_trace() return self.time, self.y, self.channel_nums @@ -628,7 +637,8 @@ def set_options_get_trace(self, channel_nums=None, source_type=None, def set_options_get_trace_save(self, fname=None, ext=None, channel_nums=None, source_type=None, wav_format=None, acq_type=None, - num_averages=None, p_mode=None, num_points=None): + num_averages=None, p_mode=None, + num_points=None, additional_header_info=None): """Get trace and save the trace to a file and plot to png. Filename is recursively checked to ensure no overwrite. @@ -670,7 +680,7 @@ def set_options_get_trace_save(self, fname=None, ext=None, self.set_options_get_trace(channel_nums=channel_nums, source_type=source_type, wav_format=wav_format, acq_type=acq_type, num_averages=num_averages, p_mode=p_mode, num_points=num_points) - self.save_trace(fname, ext) + self.save_trace(fname, ext, additional_header_info=additional_header_info) def generate_file_header(self, channels=None, additional_line=None, timestamp=True): """Generate string to be used as file header for saved files @@ -734,9 +744,10 @@ def generate_file_header(self, channels=None, additional_line=None, timestamp=Tr channels = self.channel_nums if channels is None else channels ch_str = ",".join(channels) channels_line = f"time,{ch_str}" - return self.id+"\n"+mode_line+timestamp_line+add_line+channels_line + return self._id+"\n"+mode_line+timestamp_line+add_line+channels_line - def save_trace(self, fname=None, ext=None, savepng=None, showplot=None): + def save_trace(self, fname=None, ext=None, additional_header_info=None, + savepng=None, showplot=None): """Save the most recent trace to fname+ext. Will check if the filename exists, and let the user append to the fname if that is the case. @@ -751,25 +762,33 @@ def save_trace(self, fname=None, ext=None, savepng=None, showplot=None): showplot : bool, default :data:`~keyoscacquire.config._show_plot` Choose whether to show a plot of the trace """ - if fname is not None: - self.fname = fname - if ext is not None: - self.ext = ext - if savepng is not None: - self.savepng = savepng - if showplot is not None: - self.showplot = showplot - self.fname = chsy.check_file(self.fname, self.ext) - traceio.plot_trace(self.time, self.y, self.channel_nums, fname=self.fname, - showplot=self.showplot, savepng=self.savepng) - head = self.generate_file_header() - traceio.save_trace(self.fname, self.time, self.y, fileheader=head, ext=self.ext, - print_filename=self.verbose_acquistion) + if not self.time is None: + if fname is not None: + self.fname = fname + if ext is not None: + self.ext = ext + if savepng is not None: + self.savepng = savepng + if showplot is not None: + self.showplot = showplot + self.fname = auxiliary.check_file(self.fname, self.ext) + traceio.plot_trace(self.time, self.y, self.channel_nums, fname=self.fname, + showplot=self.showplot, savepng=self.savepng) + head = self.generate_file_header(additional_line=additional_header_info) + traceio.save_trace(self.fname, self.time, self.y, fileheader=head, ext=self.ext, + print_filename=self.verbose_acquistion) + else: + print("(!) No trace has been acquired yet, use get_trace()") + _log.info("(!) No trace has been acquired yet, use get_trace()") def plot_trace(self): """Plot and show the most recent trace""" - traceio.plot_trace(self.time, self.y, self.channel_nums, - savepng=False, show=True) + if not self.time is None: + traceio.plot_trace(self.time, self.y, self.channel_nums, + savepng=False, showplot=True) + else: + print("(!) No trace has been acquired yet, use get_trace()") + _log.info("(!) No trace has been acquired yet, use get_trace()") ##============================================================================## ## DATA PROCESSING ## diff --git a/keyoscacquire/traceio.py b/keyoscacquire/traceio.py index c1a7ee3..d73d671 100644 --- a/keyoscacquire/traceio.py +++ b/keyoscacquire/traceio.py @@ -5,9 +5,11 @@ Andreas Svela // 2020 """ +import os import logging; _log = logging.getLogger(__name__) import numpy as np import pandas as pd +import matplotlib.pyplot as plt import keyoscacquire.config as config import keyoscacquire.oscacq as oscacq @@ -46,7 +48,7 @@ def save_trace(fname, time, y, fileheader="", ext=config._filetype, if os.path.exists(fname+ext): raise RuntimeError(f"{fname+ext} already exists") if print_filename: - print(f"Saving trace to {fname+ext}\n") + print(f"Saving trace to: {fname+ext}\n") data = np.append(time, y, axis=1) # make one array with columns x y1 y2 .. if ext == ".npy": if fileheader or nowarn: @@ -77,7 +79,8 @@ def save_trace_npy(fname, time, y, print_filename=True, **kwargs): save_trace(fname, time, y, ext=".npy", nowarn=True, print_filename=print_filename) -def plot_trace(time, y, channel_nums, fname="", show=config._show_plot, savepng=config._export_png): +def plot_trace(time, y, channel_nums, fname="", showplot=config._show_plot, + savepng=config._export_png): """Plots the trace with oscilloscope channel screen colours according to the Keysight colourmap and saves as a png. @@ -103,7 +106,7 @@ def plot_trace(time, y, channel_nums, fname="", show=config._show_plot, savepng= plt.plot(time, vals, color=oscacq._screen_colors[channel_nums[i]]) if savepng: plt.savefig(fname+".png", bbox_inches='tight') - if show: + if showplot: plt.show() plt.close() From bf2744f37e7d463dc60a068774761a13a3b225ea Mon Sep 17 00:00:00 2001 From: asvela at 0011 Date: Fri, 18 Dec 2020 14:37:30 +0100 Subject: [PATCH 03/52] WIP further upgrades and bugfixes * moving away from string representation of channels * introducing some properties for the Oscilloscope * updating programmes and cli_programmes to reflect new changes (untested) * bugfixes, esp. saving --- .gitignore | 1 + docs/changelog.rst | 43 ++-- keyoscacquire/installed_cli_programmes.py | 23 ++- keyoscacquire/oscacq.py | 241 ++++++++++++---------- keyoscacquire/programmes.py | 52 +++-- keyoscacquire/traceio.py | 16 +- 6 files changed, 202 insertions(+), 174 deletions(-) diff --git a/.gitignore b/.gitignore index 6e70c19..9d3f22d 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ docs/_* # files generated when running the module and others __pycache__ *.csv +*.npy *.png Thumbs.db .DS_Store diff --git a/docs/changelog.rst b/docs/changelog.rst index 3b4263d..0895d39 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -8,16 +8,17 @@ in 2019 I had very limited Python development experience, so it was time to make a few better choices now to make the API easier to use. That means that there are quite a few non-compatible changes to previous versions, -all of which are detailed below. +all of which are detailed below. I am not planning further extensive revisions +like this. v4.0.0 (2020-12) - More attributes are used to make the information accessible not only through returns - * Captured data stored to ``Oscilloscope.time`` and ``Oscilloscope.y`` + * Captured data stored to ``Oscilloscope._time`` and ``Oscilloscope._values`` * The filename finally used when saving (which might not be the same as the the argument passed as a filename check happens to avoid overwrite) is stored in ``Oscilloscope.fname`` - * ``Oscilloscope.raw`` and ``Oscilloscope.metadata`` are now available + * ``Oscilloscope._raw`` and ``Oscilloscope._metadata`` with unprocessed data - More active use of attributes that are carried forward rather than always setting the arguments of methods in the ``Oscilloscope`` class. This @@ -30,11 +31,12 @@ v4.0.0 (2020-12) maximum number of points available. - New ``keyoscacquire.traceio.load_trace()`` function for loading saved a trace + from disk to pandas dataframe or numpy array - Moved save and plot functions to ``keyoscacquire.traceio``, but are imported in ``oscacq`` to keep compatibility - - ``Oscilloscope.read_and_capture()`` will now try to read the error from the + - ``Oscilloscope.query()`` will now try to read the error from the instrument if pyvisa fails - Importing ``keyoscacquire.programmes`` in module ``init.py`` to make it accessible @@ -51,22 +53,19 @@ v4.0.0 (2020-12) - PEP8 improvements - - *(New methods)*: + - *New methods*: * ``Oscilloscope.get_error()`` * ``Oscilloscope.set_waveform_export_options()`` * ``Oscilloscope.save_trace()`` (``Oscilloscope.savepng`` and - ``Oscilloscope.showplot`` can be set to contol its behaviour) + ``Oscilloscope.showplot`` can be set to control its behaviour) * ``Oscilloscope.plot_trace()`` - - *No compatibility*: Several functions no longer take ``sources`` and - ``sourcesstring`` as arguments, rather ``Oscilloscope.sources`` and - ``Oscilloscope.sourcesstring`` must be set by - ``Oscilloscope.set_channels_for_capture()`` - - * ``Oscilloscope.capture_and_read()`` and its ``Oscilloscope._read_ascii()`` - and ``Oscilloscope._read_binary()`` - * ``Oscilloscope.get_trace()`` + - *New properties*: + * ``Oscilloscope.active_channels`` can now be used to set and get the + currently active channels + * ``Oscilloscope.timeout`` this was previously just an attribute with no + set option - *No compatibility*: Name changes @@ -89,8 +88,20 @@ v4.0.0 (2020-12) * ``interpret_visa_id()`` from ``oscacq`` to ``auxiliary`` * ``check_file()`` from ``oscacq`` to ``auxiliary`` - - *No compatibility*: ``Oscilloscope.get_trace()`` now also returns - also ``Oscilloscope.num_channels`` + - *No compatibility*: Several functions no longer take ``sources`` and + ``sourcesstring`` as arguments, rather ``Oscilloscope._sources`` must be set by + ``Oscilloscope.set_channels_for_capture()`` and ``sourcesstring`` is not in + use anymore + + * ``Oscilloscope.capture_and_read()``, and its associated + ``Oscilloscope._read_ascii()`` and ``Oscilloscope._read_binary()`` + * ``Oscilloscope.get_trace()`` + + - *No compatibility*: Misc + * ``Oscilloscope.get_trace()`` now also returns + also ``Oscilloscope.num_channels`` + * ``Oscilloscope.get_active_channels()`` is now a property ``active_channels`` + and returns a list of ints, not chars diff --git a/keyoscacquire/installed_cli_programmes.py b/keyoscacquire/installed_cli_programmes.py index 59d8cc5..5f40194 100644 --- a/keyoscacquire/installed_cli_programmes.py +++ b/keyoscacquire/installed_cli_programmes.py @@ -17,7 +17,9 @@ Andreas Svela // 2019 """ -import sys, argparse +import sys +import argparse + import keyoscacquire.programmes as acqprog import keyoscacquire.config as config @@ -31,8 +33,7 @@ file_help = f"The filename base, (without extension, '{config._filetype}' is added). Defaults to '{config._filename}'." visa_help = f"Visa address of instrument. To find the visa addresses of the instruments connected to the computer run 'list_visa_devices' in the command line. Defaults to '{config._visa_address}." timeout_help = f"Milliseconds before timeout on the channel to the instrument. Defaults to {config._timeout}." -default_channels = " ".join(config._ch_nums) if isinstance(config._ch_nums, list) else config._ch_nums -channels_help = f"List of the channel numbers to be acquired, for example '1 3' (without ') or 'active' (without ') to capture all the currently active channels on the oscilloscope. Defaults to {default_channels}'." +channels_help = f"List of the channel numbers to be acquired, for example '1 3' (without ') or 'active' (without ') to capture all the currently active channels on the oscilloscope. Defaults to {config._ch_nums}'." points_help = f"Use 0 to get the maximum number of points, or set a smaller number to speed up the acquisition and transfer. Defaults to 0." delim_help = f"Delimiter used between filename and filenumber (before filetype). Defaults to '{config._file_delimiter}'." @@ -44,7 +45,7 @@ def connect_each_time_cli(): connection_gr.add_argument('-v', '--visa_address', nargs='?', help=visa_help, default=config._visa_address) connection_gr.add_argument('-t', '--timeout', nargs='?', help=timeout_help, default=config._timeout, type=int) acquire_gr = parser.add_argument_group('Acquiring settings') - acquire_gr.add_argument('-c', '--channels', nargs='*', help=channels_help, default=config._ch_nums) + acquire_gr.add_argument('-c', '--channels', nargs='*', type=int, help=channels_help, default=config._ch_nums) acquire_gr.add_argument('-a', '--acq_type', nargs='?', help=acq_help, default=config._acq_type) trans_gr = parser.add_argument_group('Transfer and storage settings') trans_gr.add_argument('-w', '--wav_format', nargs='?', help=wav_help, default=config._waveform_format) @@ -54,7 +55,7 @@ def connect_each_time_cli(): args = parser.parse_args() acqprog.get_traces_connect_each_time_loop(fname=args.filename, address=args.visa_address, timeout=args.timeout, wav_format=args.wav_format, - channel_nums=args.channels, acq_type=args.acq_type, num_points=args.num_points, file_delim=args.file_delimiter) + channels=args.channels, acq_type=args.acq_type, num_points=args.num_points, file_delim=args.file_delimiter) def single_connection_cli(): @@ -65,7 +66,7 @@ def single_connection_cli(): connection_gr.add_argument('-v', '--visa_address', nargs='?', help=visa_help, default=config._visa_address) connection_gr.add_argument('-t', '--timeout', nargs='?', help=timeout_help, default=config._timeout, type=int) acquire_gr = parser.add_argument_group('Acquiring settings') - acquire_gr.add_argument('-c', '--channels', nargs='*', help=channels_help, default=config._ch_nums) + acquire_gr.add_argument('-c', '--channels', nargs='*', type=int, help=channels_help, default=config._ch_nums) acquire_gr.add_argument('-a', '--acq_type', nargs='?', help=acq_help, default=config._acq_type) trans_gr = parser.add_argument_group('Transfer and storage settings') trans_gr.add_argument('-w', '--wav_format', nargs='?', help=wav_help, default=config._waveform_format) @@ -75,7 +76,7 @@ def single_connection_cli(): args = parser.parse_args() acqprog.get_traces_single_connection_loop(fname=args.filename, address=args.visa_address, timeout=args.timeout, wav_format=args.wav_format, - channel_nums=args.channels, acq_type=args.acq_type, num_points=args.num_points, file_delim=args.file_delimiter) + channels=args.channels, acq_type=args.acq_type, num_points=args.num_points, file_delim=args.file_delimiter) def single_trace_cli(): @@ -85,7 +86,7 @@ def single_trace_cli(): connection_gr.add_argument('-v', '--visa_address', nargs='?', help=visa_help, default=config._visa_address) connection_gr.add_argument('-t', '--timeout', nargs='?', help=timeout_help, default=config._timeout, type=int) acquire_gr = parser.add_argument_group('Acquiring settings') - acquire_gr.add_argument('-c', '--channels', nargs='*', help=channels_help, default=config._ch_nums) + acquire_gr.add_argument('-c', '--channels', nargs='*', type=int, help=channels_help, default=config._ch_nums) acquire_gr.add_argument('-a', '--acq_type', nargs='?', help=acq_help, default=config._acq_type) trans_gr = parser.add_argument_group('Transfer and storage settings') trans_gr.add_argument('-w', '--wav_format', nargs='?', help=wav_help, default=config._waveform_format) @@ -94,7 +95,7 @@ def single_trace_cli(): args = parser.parse_args() acqprog.get_single_trace(fname=args.filename, address=args.visa_address, timeout=args.timeout, wav_format=args.wav_format, - channel_nums=args.channels, acq_type=args.acq_type, num_points=args.num_points) + channels=args.channels, acq_type=args.acq_type, num_points=args.num_points) def num_traces_cli(): """Function installed on the command line: Obtains and stores a single trace.""" @@ -106,7 +107,7 @@ def num_traces_cli(): connection_gr.add_argument('-v', '--visa_address', nargs='?', help=visa_help, default=config._visa_address) connection_gr.add_argument('-t', '--timeout', nargs='?', help=timeout_help, default=config._timeout, type=int) acquire_gr = parser.add_argument_group('Acquiring settings') - acquire_gr.add_argument('-c', '--channels', nargs='*', help=channels_help, default=config._ch_nums) + acquire_gr.add_argument('-c', '--channels', nargs='*', type=int, help=channels_help, default=config._ch_nums) acquire_gr.add_argument('-a', '--acq_type', nargs='?', help=acq_help, default=config._acq_type) trans_gr = parser.add_argument_group('Transfer and storage settings') trans_gr.add_argument('-w', '--wav_format', nargs='?', help=wav_help, default=config._waveform_format) @@ -116,7 +117,7 @@ def num_traces_cli(): args = parser.parse_args() acqprog.get_num_traces(num=args.num, fname=args.filename, address=args.visa_address, timeout=args.timeout, wav_format=args.wav_format, - channel_nums=args.channels, acq_type=args.acq_type, num_points=args.num_points) + channels=args.channels, acq_type=args.acq_type, num_points=args.num_points) def list_visa_devices_cli(): """Function installed on the command line: Lists VISA devices""" diff --git a/keyoscacquire/oscacq.py b/keyoscacquire/oscacq.py index fbc67b3..5a8674a 100644 --- a/keyoscacquire/oscacq.py +++ b/keyoscacquire/oscacq.py @@ -29,7 +29,7 @@ #: Supported Keysight DSO/MSO InfiniiVision series _supported_series = ['1000', '2000', '3000', '4000', '6000'] #: Keysight colour map for the channels -_screen_colors = {'1':'C1', '2':'C2', '3':'C0', '4':'C3'} +_screen_colors = {1:'C1', 2:'C2', 3:'C0', 4:'C3'} #: Datatype is ``'h'`` for 16 bit signed int (``WORD``), ``'b'`` for 8 bit signed bit (``BYTE``). #: Same naming as for structs `docs.python.org/3/library/struct.html#format-characters` _datatypes = {'BYTE':'b', 'WORD':'h'} @@ -76,7 +76,7 @@ class Oscilloscope: _address : str Visa address of instrument timeout : int - Milliseconds before timeout on the channel to the instrument + Milliseconds before timeout on the communications with the instrument acq_type : {'HRESolution', 'NORMal', 'AVERage', 'AVER'} Acquisition mode of the oscilloscope. will be used as :attr:`num_averages` if supplied. @@ -117,21 +117,18 @@ class Oscilloscope: verbose_acquistion : bool ``True`` prints that the capturing starts and the number of points captured """ - raw = None - metadata = None - time = None - y = None - source_type = 'CHAN' + _raw = None + _metadata = None + _time = None + _values = None fname = config._filename ext = config._filetype savepng = config._export_png showplot = config._show_plot - channel_nums = config._ch_nums def __init__(self, address=config._visa_address, timeout=config._timeout, verbose=True): """See class docstring""" self._address = address - self.timeout = timeout self.verbose = verbose self.verbose_acquistion = verbose # Connect to the scope @@ -141,7 +138,7 @@ def __init__(self, address=config._visa_address, timeout=config._timeout, verbos except pyvisa.Error as err: print(f"\n\nCould not connect to '{address}', see traceback below:\n") raise - self._inst.timeout = self.timeout + self._timeout = timeout # For TCP/IP socket connections enable the read Termination Character, or reads will timeout if self._inst.resource_name.endswith('SOCKET'): self._inst.read_termination = '\n' @@ -166,7 +163,7 @@ def __init__(self, address=config._visa_address, timeout=config._timeout, verbos num_averages=config._num_avg, p_mode='RAW', num_points=0, verbose_acquistion=verbose) print(" ", end="") - self.set_channels_for_capture(channel_nums=config._ch_nums) + self.set_channels_for_capture(channels=config._ch_nums) def __enter__(self): return self @@ -201,33 +198,13 @@ def query(self, command, action=""): else: msg = f"query '{command}'" print(f"\nVisaError: {err}\n When trying {msg}.") - print(f" Have you checked that the timeout (currently {self.timeout:,d} ms) is sufficently long?") + print(f" Have you checked that the timeout (currently {self._timeout:,d} ms) is sufficently long?") try: print(f"Latest error from the oscilloscope: '{self.get_error()}'\n") except Exception: print("Could not retrieve error from the oscilloscope") raise - def run(self): - """Set the ocilloscope to running mode.""" - self._inst.write(':RUN') - - def stop(self): - """Stop the oscilloscope.""" - self._inst.write(':STOP') - - def is_running(self): - """Determine if the oscilloscope is running. - - Returns - ------- - bool - ``True`` if running, ``False`` otherwise - """ - # The third bit of the operation register is 1 if the instrument is running - reg = int(self.query(':OPERegister:CONDition?')) - return (reg & 8) == 8 - def close(self, set_running=True): """Closes the connection to the oscilloscope. @@ -254,22 +231,59 @@ def get_error(self): # Do not use self.query here as that can lead to infinite nesting! return self._inst.query(":SYSTem:ERRor?") - def get_active_channels(self): - """Get list of the currently active channels on the instrument + def run(self): + """Set the ocilloscope to running mode.""" + self._inst.write(':RUN') + + def stop(self): + """Stop the oscilloscope.""" + self._inst.write(':STOP') + + def is_running(self): + """Determine if the oscilloscope is running. Returns ------- - list of chars - list of the active channels, example ``['1', '3']`` + bool + ``True`` if running, ``False`` otherwise + """ + # The third bit of the operation register is 1 if the instrument is running + reg = int(self.query(':OPERegister:CONDition?')) + return (reg & 8) == 8 + + @property + def timeout(self): + return self._inst.timeout + + @timeout.setter + def timeout(self, val: int): + self._inst.timeout = val + + @property + def active_channels(self): + """List of the currently active channels on the instrument + + Returns + ------- + list of ints + list of the active channels, for example ``[1, 3]`` """ - channels = np.array(['1', '2', '3', '4']) # querying DISP for each channel to determine which channels are currently displayed - displayed_channels = [self.query(f":CHANnel{channel}:DISPlay?")[0] for channel in channels] - # get a mask of bools for the channels that are on [need the int() as bool('0') = True] - channel_mask = np.array([bool(int(i)) for i in displayed_channels]) - # apply mask to the channel list - self.channel_nums = channels[channel_mask] - return self.channel_nums + return [i for i in range(1, 5) if bool(int(self.query(f":CHAN{i}:DISP?")))] + + @active_channels.setter + def active_channels(self, channels): + """Get list of the currently active channels on the instrument + + Parameters + ---------- + list of ints + list of the channels to set active, for example ``[1, 3]`` + """ + if not isinstance(channels, list): + channels = [channels] + for i in range(1, 5): + self.write(f":CHAN{i}:DISP {int(i in channels)}") def set_acquiring_options(self, wav_format=None, acq_type=None, num_averages=None, p_mode=None, num_points=None, @@ -383,40 +397,35 @@ def set_waveform_export_options(self, wav_format=None, num_points=None, p_mode=N ## Capture and read functions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## - def set_channels_for_capture(self, source_types=None, channel_nums=None): + def set_channels_for_capture(self, channels=None): """Decide the channels to be acquired, or determine by checking active channels on the oscilloscope. - .. note:: Use ``channel_nums='active'`` to capture all the currently - active channels on the oscilloscope. - Parameters ---------- - source_type : str, default ``'CHANnel'`` - Selects the source type. Must be ``'CHANnel'`` in current implementation. - Future version might include {'MATH', 'FUNCtion'}. - channel_nums : list or ``'active'``, default :data:`~keyoscacquire.config._ch_nums` - list of the channel numbers to be acquired, example ``['1', '3']``. - Use ``'active'`` or ``['']`` to capture all the currently active + channels : list of ints or ``'active'``, default :data:`~keyoscacquire.config._ch_nums` + list of the channel numbers to be acquired, example ``[1, 3]``. + Use ``'active'`` or ``[]`` to capture all the currently active channels on the oscilloscope. Returns ------- - channel_nums : list of chars - list of the channels, example ``['1', '3']`` + list of ints + the channels that will be captured, example ``[1, 3]`` """ # If no channels specified, find the channels currently active and acquire from those - if channel_nums is not None: - self.channel_nums = channel_nums - if self.channel_nums in [[''], ['active'], 'active']: - self.get_active_channels() + if np.any(channels in [[], ['active'], 'active']) or self._capture_active: + self._capture_channels = self.active_channels + # Store that active channels are being used + self._capture_active = True + else: + self._capture_channels = channels + self._capture_active = False # Build list of sources - self.sources = [self.source_type+channel for channel in self.channel_nums] - # Make string of sources - self.sourcesstring = ", ".join(self.sources) + self._sources = [f"CHANnel{ch}" for ch in self._capture_channels] if self.verbose_acquistion: - print("Acquire from sources", self.sourcesstring) - return self.channel_nums + print(f"Acquire from channels {self._capture_channels}") + return self._capture_channels def capture_and_read(self, set_running=True): """Acquire raw data from selected channels according to acquring options @@ -456,14 +465,15 @@ def capture_and_read(self, set_running=True): # DIGitize is a specialized RUN command. # Waveforms are acquired according to the settings of the :ACQuire commands. # When acquisition is complete, the instrument is stopped. - self._inst.write(':DIGitize ' + self.sourcesstring) + self._inst.write(':DIGitize ' + ", ".join(self._sources)) ## Read from the scope if self.wav_format[:3] in ['WOR', 'BYT']: self._read_binary(datatype=_datatypes[self.wav_format]) elif self.wav_format[:3] == 'ASC': self._read_ascii() else: - raise ValueError(f"Could not capture and read data, waveform format '{self.wav_format}' is unknown.\n") + raise ValueError(f"Could not capture and read data, waveform format " + f"'{self.wav_format}' is unknown.\n") ## Print to log to_log = f"Elapsed time capture and read: {(time.time()-start_time)*1e3:.1f} ms" if self.verbose_acquistion: @@ -499,20 +509,22 @@ def _read_binary(self, datatype='standard'): set_running : bool, default ``True`` ``True`` leaves oscilloscope running after data capture """ - self.raw, self.metadata = [], [] + self._raw, self._metadata = [], [] # Loop through all the sources - for source in self.sources: + for source in self._sources: # Select the channel for which the succeeding WAVeform commands applies to self._inst.write(':WAVeform:SOURce ' + source) try: # obtain comma separated metadata values for processing of raw data for this source - self.metadata.append(self.query(':WAVeform:PREamble?')) + self._metadata.append(self.query(':WAVeform:PREamble?')) # obtain the data # read out data for this source - self.raw.append(self._inst.query_binary_values(':WAVeform:DATA?', datatype=datatype, container=np.array)) + self._raw.append(self._inst.query_binary_values(':WAVeform:DATA?', + datatype=datatype, + container=np.array)) except pyvisa.Error as err: print(f"\n\nVisaError: {err}\n When trying to obtain the waveform.") - print(f" Have you checked that the timeout (currently {self.timeout:,d} ms) is sufficently long?") + print(f" Have you checked that the timeout (currently {self._timeout:,d} ms) is sufficently long?") try: print(f"Latest error from the oscilloscope: '{self.get_error()}'\n") except Exception: @@ -540,17 +552,17 @@ def _read_ascii(self): set_running : bool, default ``True`` ``True`` leaves oscilloscope running after data capture """ - self.raw = [] + self._raw = [] # Loop through all the sources - for source in self.sources: + for source in self._sources: # Select the channel for which the succeeding WAVeform commands applies to self._inst.write(':WAVeform:SOURce ' + source) # Read out data for this source - self.raw.append(self.query(':WAVeform:DATA?', action="obtain the waveform")) + self._raw.append(self.query(':WAVeform:DATA?', action="obtain the waveform")) # Get the preamble (used for calculating time axis, which is the same # for all channels) preamble = self.query(':WAVeform:PREamble?') - self.metadata = (preamble, self._model_series) + self._metadata = (preamble, self._model_series) ## Building functions to get a trace and various option setting and processing ## @@ -571,9 +583,11 @@ def get_trace(self, verbose_acquistion=None): y : :class:`~numpy.ndarray` Voltage values, same sequence as sources input, each row represents one channel - channel_nums : list of chars - list of the channels obtained from, example ``['1', '3']`` + _capture_channels : list of ints + list of the channels obtaied from, example ``[1, 3]`` """ + if self._capture_active: + self.set_channels_for_capture() # Possibility to override verbose_acquistion if verbose_acquistion is not None: # Store current setting and set temporary setting @@ -581,25 +595,23 @@ def get_trace(self, verbose_acquistion=None): self.verbose_acquistion = verbose_acquistion # Capture, read and process data self.capture_and_read() - self.time, self.y = process_data(self.raw, self.metadata, self.wav_format, verbose_acquistion=self.verbose_acquistion) + self._time, self._values = process_data(self._raw, self._metadata, self.wav_format, + verbose_acquistion=self.verbose_acquistion) # Restore self.verbose_acquistion to previous setting if verbose_acquistion is not None: self.verbose_acquistion = temp - return self.time, self.y, self.channel_nums + return self._time, self._values, self._capture_channels - def set_options_get_trace(self, channel_nums=None, source_type=None, - wav_format=None, acq_type=None, + def set_options_get_trace(self, channels=None, wav_format=None, acq_type=None, num_averages=None, p_mode=None, num_points=None): """Set the options provided by the parameters and obtain one trace. Parameters ---------- - channel_nums : list or ``'active'``, default :data:`~keyoscacquire.config._ch_nums` - list of the channel numbers to be acquired from, example ``['1', '3']``. - Use ``'active'`` to capture all the currently active channels on the oscilloscope. - source_type : str, default ``'CHANnel'`` - Selects the source type. Must be ``'CHANnel'`` in current implementation. - Future version might include {'MATH', 'FUNCtion'}. + channels : list of ints or ``'active'``, default :data:`~keyoscacquire.config._ch_nums` + list of the channel numbers to be acquired, example ``[1, 3]``. + Use ``'active'`` or ``[]`` to capture all the currently active + channels on the oscilloscope. wav_format : {``'WORD'``, ``'BYTE'``, ``'ASCii'``}, default :data:`~keyoscacquire.config._waveform_format` Select the format of the communication of waveform from the oscilloscope, see :attr:`wav_format` @@ -620,22 +632,22 @@ def set_options_get_trace(self, channel_nums=None, source_type=None, time : :class:`~numpy.ndarray` Time axis for the measurement y : :class:`~numpy.ndarray` - Voltage values, same sequence as ``channel_nums``, each row represents one channel - channel_nums : list of chars - list of the channels obtained from, example ``['1', '3']`` + Voltage values, same sequence as ``channels``, each row represents one channel + _capture_channels : list of ints + list of the channels obtaied from, example ``[1, 3]`` """ ## Connect to instrument and specify acquiring settings self.set_acquiring_options(wav_format=wav_format, acq_type=acq_type, num_averages=num_averages, p_mode=p_mode, num_points=num_points) ## Select sources - self.set_channels_for_capture(channel_nums=channel_nums) + self.set_channels_for_capture(channels=channels) ## Capture, read and process data self.get_trace() - return self.time, self.y, self.channel_nums + return self._time, self._values, self._capture_channels def set_options_get_trace_save(self, fname=None, ext=None, - channel_nums=None, source_type=None, + channels=None, source_type=None, wav_format=None, acq_type=None, num_averages=None, p_mode=None, num_points=None, additional_header_info=None): @@ -655,9 +667,10 @@ def set_options_get_trace_save(self, fname=None, ext=None, Filename of trace ext : str, default :data:`~keyoscacquire.config._filetype` Choose the filetype of the saved trace - channel_nums : list or ``'active'``, default :data:`~keyoscacquire.config._ch_nums` - list of the channel numbers to be acquired from, example ``['1', '3']``. - Use ``'active'`` to capture all the currently active channels on the oscilloscope + channels : list of ints or ``'active'``, default :data:`~keyoscacquire.config._ch_nums` + list of the channel numbers to be acquired, example ``[1, 3]``. + Use ``'active'`` or ``[]`` to capture all the currently active + channels on the oscilloscope. source_type : str, default ``'CHANnel'`` Selects the source type. Must be ``'CHANnel'`` in current implementation. Future version might include {'MATH', 'FUNCtion'} @@ -677,7 +690,7 @@ def set_options_get_trace_save(self, fname=None, ext=None, Use 0 to let :attr:`p_mode` control the number of points, otherwise override with a lower number than maximum for the :attr:`p_mode` """ - self.set_options_get_trace(channel_nums=channel_nums, source_type=source_type, + self.set_options_get_trace(channels=channels, source_type=source_type, wav_format=wav_format, acq_type=acq_type, num_averages=num_averages, p_mode=p_mode, num_points=num_points) self.save_trace(fname, ext, additional_header_info=additional_header_info) @@ -705,7 +718,7 @@ def generate_file_header(self, channels=None, additional_line=None, timestamp=Tr Parameters ---------- - channels : list of str + channels : list of strs or ints Any list of identifies for the channels used for the measurement to be saved. additional_line : str or ``None``, default ``None`` No additional line if set to ``None``, otherwise the value of the argument will be used @@ -722,7 +735,7 @@ def generate_file_header(self, channels=None, additional_line=None, timestamp=Tr ------- If the oscilloscope is acquiring in ``'AVER'`` mode with eight averages:: - Oscilloscope.generate_file_header(['1', '3'], additional_line="my comment") + Oscilloscope.generate_file_header([1, 'piezo'], additional_line="my comment") gives:: @@ -730,24 +743,26 @@ def generate_file_header(self, channels=None, additional_line=None, timestamp=Tr # AVER,8 # 2019-09-06 20:01:15.187598 # my comment - # time,1,3 + # time,1,piezo """ # Set num averages only if AVERage mode - num_averages = str(self.num_averages) if self.acq_type[:3] == 'AVE' else "N/A" + num_averages = self.num_averages if self.acq_type[:3] == 'AVE' else "N/A" mode_line = f"{self.acq_type},{num_averages}\n" # Set timestamp if called for timestamp_line = str(dt.datetime.now())+"\n" if timestamp else "" # Set addtional line if called for add_line = additional_line+"\n" if additional_line is not None else "" - # Use channel_nums unless channel argument is not None - channels = self.channel_nums if channels is None else channels + # Use _capture_channels unless channel argument is not None + if channels is None: + channels = self._capture_channels + channels = [str(ch) for ch in channels] ch_str = ",".join(channels) channels_line = f"time,{ch_str}" return self._id+"\n"+mode_line+timestamp_line+add_line+channels_line def save_trace(self, fname=None, ext=None, additional_header_info=None, - savepng=None, showplot=None): + savepng=None, showplot=None, nowarn=False): """Save the most recent trace to fname+ext. Will check if the filename exists, and let the user append to the fname if that is the case. @@ -762,7 +777,7 @@ def save_trace(self, fname=None, ext=None, additional_header_info=None, showplot : bool, default :data:`~keyoscacquire.config._show_plot` Choose whether to show a plot of the trace """ - if not self.time is None: + if not self._time is None: if fname is not None: self.fname = fname if ext is not None: @@ -771,20 +786,24 @@ def save_trace(self, fname=None, ext=None, additional_header_info=None, self.savepng = savepng if showplot is not None: self.showplot = showplot + # Remove extenstion if provided in the fname + if self.fname[-4:] in ['.npy', '.csv']: + self.ext = self.fname[-4:] + self.fname = self.fname[:-4] self.fname = auxiliary.check_file(self.fname, self.ext) - traceio.plot_trace(self.time, self.y, self.channel_nums, fname=self.fname, + traceio.plot_trace(self._time, self._values, self._capture_channels, fname=self.fname, showplot=self.showplot, savepng=self.savepng) head = self.generate_file_header(additional_line=additional_header_info) - traceio.save_trace(self.fname, self.time, self.y, fileheader=head, ext=self.ext, - print_filename=self.verbose_acquistion) + traceio.save_trace(self.fname, self._time, self._values, fileheader=head, ext=self.ext, + print_filename=self.verbose_acquistion, nowarn=nowarn) else: print("(!) No trace has been acquired yet, use get_trace()") _log.info("(!) No trace has been acquired yet, use get_trace()") def plot_trace(self): """Plot and show the most recent trace""" - if not self.time is None: - traceio.plot_trace(self.time, self.y, self.channel_nums, + if not self._time is None: + traceio.plot_trace(self._time, self._values, self._capture_channels, savepng=False, showplot=True) else: print("(!) No trace has been acquired yet, use get_trace()") @@ -860,7 +879,7 @@ def _process_data_binary(raw, preambles, verbose_acquistion=True): y : :class:`~numpy.ndarray` Voltage values, each row represents one channel """ - # pick one preamle and use for calculating the time values (same for all channels) + # Pick one preamble and use for calculating the time values (same for all channels) preamble = preambles[0].split(',') # values separated by commas num_samples = int(float(preamble[2])) xIncr, xOrig, xRef = float(preamble[4]), float(preamble[5]), float(preamble[6]) diff --git a/keyoscacquire/programmes.py b/keyoscacquire/programmes.py index fa6ce36..d8287aa 100644 --- a/keyoscacquire/programmes.py +++ b/keyoscacquire/programmes.py @@ -99,19 +99,19 @@ def path_of_config(): def get_single_trace(fname=config._filename, ext=config._filetype, address=config._visa_address, timeout=config._timeout, wav_format=config._waveform_format, - channel_nums=config._ch_nums, source_type='CHANnel', acq_type=config._acq_type, + channels=config._ch_nums, source_type='CHANnel', acq_type=config._acq_type, num_averages=config._num_avg, p_mode='RAW', num_points=0): """This programme captures and stores a single trace.""" with acq.Oscilloscope(address=address, timeout=timeout) as scope: scope.set_options_get_trace_save(fname=fname, ext=ext, wav_format=wav_format, - channel_nums=channel_nums, source_type=source_type, acq_type=acq_type, + channels=channels, source_type=source_type, acq_type=acq_type, num_averages=num_averages, p_mode=p_mode, num_points=num_points) print("Done") def get_traces_connect_each_time_loop(fname=config._filename, ext=config._filetype, address=config._visa_address, timeout=config._timeout, wav_format=config._waveform_format, - channel_nums=config._ch_nums, source_type='CHANnel', acq_type=config._acq_type, + channels=config._ch_nums, source_type='CHANnel', acq_type=config._acq_type, num_averages=config._num_avg, p_mode='RAW', num_points=0, start_num=0, file_delim=config._file_delimiter): """This program consists of a loop in which the program connects to the oscilloscope, a trace from the active channels are captured and stored for each loop. @@ -124,26 +124,24 @@ def get_traces_connect_each_time_loop(fname=config._filename, ext=config._filety """ # Check that file does not exist from before, append to name if it does n = start_num - fnum = file_delim+str(n) - fname = acq.check_file(fname, ext, num=fnum) + fname = auxiliary.check_file(fname, ext, num=f"{file_delim}{n}") print("Running a loop where at every 'enter' oscilloscope traces will be saved as %s%s," % (fname, ext)) print("where increases by one for each captured trace. Press 'q'+'enter' to quit the programme.") while sys.stdin.read(1) != 'q': # breaks the loop if q+enter is given as input. For any other character (incl. enter) - fnum = file_delim+str(n) + fnum = f"{file_delim}{n}" with acq.Oscilloscope(address=address, timeout=timeout) as scope: - x, y, channels = scope.set_options_get_trace(wav_format=wav_format, - channel_nums=channel_nums, source_type=source_type, acq_type=acq_type, + scope.ext = ext + scope.set_options_get_trace(wav_format=wav_format, + channels=channels, source_type=source_type, acq_type=acq_type, num_averages=num_averages, p_mode=p_mode, num_points=num_points) - acq.plot_trace(x, y, channels, fname=fname+fnum) - fhead = scope.generate_file_header() - acq.save_trace(fname+fnum, x, y, fileheader=fhead, ext=ext) + scope.save_trace(fname+fnum) n += 1 print("Quit") def get_traces_single_connection_loop(fname=config._filename, ext=config._filetype, address=config._visa_address, timeout=config._timeout, wav_format=config._waveform_format, - channel_nums=config._ch_nums, source_type='CHANnel', acq_type=config._acq_type, + channels=config._ch_nums, source_type='CHANnel', acq_type=config._acq_type, num_averages=config._num_avg, p_mode='RAW', num_points=0, start_num=0, file_delim=config._file_delimiter): """This program connects to the oscilloscope, sets options for the acquisition and then enters a loop in which the program captures and stores traces each time 'enter' is pressed. @@ -159,27 +157,25 @@ def get_traces_single_connection_loop(fname=config._filename, ext=config._filety scope.set_acquiring_options(wav_format=wav_format, acq_type=acq_type, num_averages=num_averages, p_mode=p_mode, num_points=num_points) - ## Select sources - scope.set_channels_for_capture(channel_nums=channel_nums) - fhead = scope.generate_file_header() + scope.ext = ext + scope.set_channels_for_capture(channels=channels) # Check that file does not exist from before, append to name if it does n = start_num - fnum = file_delim+str(n) - fname = acq.check_file(fname, ext, num=fnum) + fname = auxiliary.check_file(fname, ext, num=f"{file_delim}{n}") print("Running a loop where at every 'enter' oscilloscope traces will be saved as %s%s," % (fname, ext)) print("where increases by one for each captured trace. Press 'q'+'enter' to quit the programme.") while sys.stdin.read(1) != 'q': # breaks the loop if q+enter is given as input. For any other character (incl. enter) - fnum = file_delim+str(n) - x, y, channel_nums = scope.get_trace() - acq.plot_trace(x, y, channel_nums, fname=fname+fnum) # plot trace and save png - acq.save_trace(fname+fnum, x, y, fileheader=fhead, ext=ext) # save trace to ext file + scope.verbose_acquistion = (i==n) + fnum = f"{file_delim}{n}" + scope.get_trace() + scope.save_trace(fname+fnum) n += 1 print("Quit") def get_num_traces(fname=config._filename, ext=config._filetype, num=1, address=config._visa_address, timeout=config._timeout, wav_format=config._waveform_format, - channel_nums=config._ch_nums, source_type='CHANnel', acq_type=config._acq_type, + channels=config._ch_nums, source_type='CHANnel', acq_type=config._acq_type, num_averages=config._num_avg, p_mode='RAW', num_points=0, start_num=0, file_delim=config._file_delimiter): """This program connects to the oscilloscope, sets options for the acquisition, and captures and stores 'num' traces. @@ -188,16 +184,16 @@ def get_num_traces(fname=config._filename, ext=config._filetype, num=1, address= scope.set_acquiring_options(wav_format=wav_format, acq_type=acq_type, num_averages=num_averages, p_mode=p_mode, num_points=num_points, acq_print=False) + scope.ext = ext ## Select sources - scope.set_channels_for_capture(channel_nums=channel_nums) - fhead = scope.generate_file_header() + scope.set_channels_for_capture(channels=channels) n = start_num fnum = file_delim+str(n) # Check that file does not exist from before, append to name if it does - fname = acq.check_file(fname, ext, num=fnum) + fname = auxiliary.check_file(fname, ext, num=fnum) for i in tqdm(range(n, n+num)): fnum = file_delim+str(i) - x, y, channel_nums = scope.get_trace(acquire_print=(i==n)) - #acq.plot_trace(x, y, channel_nums, fname=fname+fnum) # plot trace and save png - acq.save_trace(fname+fnum, x, y, fileheader=fhead, ext=ext, print_filename=(i==n)) # save trace to ext file + scope.verbose_acquistion = (i==n) + scope.get_trace() + scope.save_trace(fname+fnum) print("Done") diff --git a/keyoscacquire/traceio.py b/keyoscacquire/traceio.py index d73d671..16a3e26 100644 --- a/keyoscacquire/traceio.py +++ b/keyoscacquire/traceio.py @@ -34,7 +34,7 @@ def save_trace(fname, time, y, fileheader="", ext=config._filetype, y : ~numpy.ndarray Voltage values, same sequence as channel_nums fileheader : str, default ``""`` - Header of file, use :func:`generate_file_header` + Header of file, use for instance :meth:`Oscilloscope.generate_file_header` ext : str, default :data:`~keyoscacquire.config._filetype` Choose the filetype of the saved trace print_filename : bool, default ``True`` @@ -51,9 +51,9 @@ def save_trace(fname, time, y, fileheader="", ext=config._filetype, print(f"Saving trace to: {fname+ext}\n") data = np.append(time, y, axis=1) # make one array with columns x y1 y2 .. if ext == ".npy": - if fileheader or nowarn: - _log.warning(f"File header {fileheader} is not saved as file format npy is chosen. " - "To surpress this warning, use save_trace_npy() instead or the nowarn flag") + if fileheader and not nowarn: + _log.warning(f"(!) WARNING: The file header\n\n{fileheader}\n\nis not saved as file format npy is chosen. " + "\nTo suppress this warning, use the nowarn flag.") np.save(fname+".npy", data) else: np.savetxt(fname+ext, data, delimiter=",", header=fileheader) @@ -79,7 +79,7 @@ def save_trace_npy(fname, time, y, print_filename=True, **kwargs): save_trace(fname, time, y, ext=".npy", nowarn=True, print_filename=print_filename) -def plot_trace(time, y, channel_nums, fname="", showplot=config._show_plot, +def plot_trace(time, y, channels, fname="", showplot=config._show_plot, savepng=config._export_png): """Plots the trace with oscilloscope channel screen colours according to the Keysight colourmap and saves as a png. @@ -93,8 +93,8 @@ def plot_trace(time, y, channel_nums, fname="", showplot=config._show_plot, Time axis for the measurement y : ~numpy.ndarray Voltage values, same sequence as channel_nums - channel_nums : list of chars - list of the channels obtained, example ['1', '3'] + channels : list of ints + list of the channels obtained, example [1, 3] fname : str, default ``""`` Filename of possible exported png show : bool, default :data:`~keyoscacquire.config._show_plot` @@ -103,7 +103,7 @@ def plot_trace(time, y, channel_nums, fname="", showplot=config._show_plot, ``True`` exports the plot to ``fname``.png """ for i, vals in enumerate(np.transpose(y)): # for each channel - plt.plot(time, vals, color=oscacq._screen_colors[channel_nums[i]]) + plt.plot(time, vals, color=oscacq._screen_colors[channels[i]]) if savepng: plt.savefig(fname+".png", bbox_inches='tight') if showplot: From a4667078e715ef15060b2ab292e3077d7acc6f44 Mon Sep 17 00:00:00 2001 From: Andreas Svela Date: Sat, 19 Dec 2020 23:25:38 +0100 Subject: [PATCH 04/52] (docs) brushing up * improvements to how the Oscilloscope API is presented * updating docstrings and completing ones with missing content * finding method for documenting properties --- docs/changelog.rst | 15 ++-- docs/conf.py | 4 +- docs/contents/dataprocessing.rst | 18 ++-- docs/contents/osc-class.rst | 46 +++++++++- docs/contents/overview.rst | 4 +- docs/index.rst | 3 + keyoscacquire/VERSION | 2 +- keyoscacquire/auxiliary.py | 1 - keyoscacquire/oscacq.py | 143 ++++++++++++++++++------------- keyoscacquire/programmes.py | 16 ++-- keyoscacquire/traceio.py | 53 +++++++----- 11 files changed, 199 insertions(+), 106 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 0895d39..99a7f46 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -3,16 +3,19 @@ Changelog v4.0: Extreme (API) makeover ---------------------------- -Big makeover with many no compatible changes. When writing the base of this back -in 2019 I had very limited Python development experience, so it was time to make -a few better choices now to make the API easier to use. +Big makeover with many non-compatible changes (sorry). + +When writing the base of this package back in 2019, I had very limited Python +development experience, and some not so clever choices were made. It was time +to make clear these up and make the API easier to use. That means that there are quite a few non-compatible changes to previous versions, all of which are detailed below. I am not planning further extensive revisions like this. v4.0.0 (2020-12) - - More attributes are used to make the information accessible not only through returns + - More attributes are used to make the information accessible not only through + returns * Captured data stored to ``Oscilloscope._time`` and ``Oscilloscope._values`` * The filename finally used when saving (which might not be the same as the @@ -62,6 +65,7 @@ v4.0.0 (2020-12) * ``Oscilloscope.plot_trace()`` - *New properties*: + * ``Oscilloscope.active_channels`` can now be used to set and get the currently active channels * ``Oscilloscope.timeout`` this was previously just an attribute with no @@ -94,10 +98,11 @@ v4.0.0 (2020-12) use anymore * ``Oscilloscope.capture_and_read()``, and its associated - ``Oscilloscope._read_ascii()`` and ``Oscilloscope._read_binary()`` + ``Oscilloscope._read_ascii()`` and ``Oscilloscope._read_binary()`` * ``Oscilloscope.get_trace()`` - *No compatibility*: Misc + * ``Oscilloscope.get_trace()`` now also returns also ``Oscilloscope.num_channels`` * ``Oscilloscope.get_active_channels()`` is now a property ``active_channels`` diff --git a/docs/conf.py b/docs/conf.py index ae39d28..5e751a3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -26,8 +26,8 @@ author = 'Andreas Svela' # The full version, including alpha/beta/rc tags -version = ver -release = version +version = ver.rsplit(".", 1)[0] # get only major.minor +release = ver html_title = f"{project} v{version}" diff --git a/docs/contents/dataprocessing.rst b/docs/contents/dataprocessing.rst index d6e6f76..91d5096 100644 --- a/docs/contents/dataprocessing.rst +++ b/docs/contents/dataprocessing.rst @@ -5,7 +5,7 @@ Data processing, file saving & loading .. py:currentmodule:: keyoscacquire.oscacq -The :mod:`keyoscacquire.oscacq` module contains function for processing +The :mod:`keyoscacquire.oscacq` module contains a function for processing the raw data captured with :class:`Oscilloscope`, and :mod:`keyoscacquire.traceio` for saving the processed data to files and plots. @@ -16,16 +16,20 @@ The output from the :func:`Oscilloscope.capture_and_read` function is processed by :func:`process_data`, a wrapper function that sends the data to the respective binary or ascii processing function. +This function is kept outside the Oscilloscope class as one might want to +post-process data after capturing it. + .. autofunction:: process_data -File saving (:mod:`keyoscacquire.traceio`) ------------------------------------------- +File saving and loading (:mod:`keyoscacquire.traceio`) +------------------------------------------------------ + +The Oscilloscope class has the method :meth:`Oscilloscope.save_trace()` for +saving the most recently captured trace to disk. This method relies on the +``traceio`` module. -The package has built-in functions for saving traces to npy format -(see :mod:`numpy.lib.format`) files or ascii values (the latter is slower but will -give a header that can be customised, :func:`Oscilloscope.generate_file_header` -is used by default). +.. automodule:: keyoscacquire.traceio .. autofunction:: keyoscacquire.traceio.save_trace .. autofunction:: keyoscacquire.traceio.plot_trace diff --git a/docs/contents/osc-class.rst b/docs/contents/osc-class.rst index 6536aeb..981b44d 100644 --- a/docs/contents/osc-class.rst +++ b/docs/contents/osc-class.rst @@ -10,7 +10,47 @@ Oscilloscope API .. py:currentmodule:: keyoscacquire.oscacq .. autoclass:: Oscilloscope - :members: + +High-level functions +-------------------- + +.. automethod:: Oscilloscope.get_trace +.. automethod:: Oscilloscope.save_trace +.. automethod:: Oscilloscope.plot_trace +.. automethod:: Oscilloscope.set_options_get_trace +.. automethod:: Oscilloscope.set_options_get_trace_save + + +Connection and VISA commands +---------------------------- + +.. automethod:: Oscilloscope.close +.. autoproperty:: Oscilloscope.timeout +.. automethod:: Oscilloscope.write +.. automethod:: Oscilloscope.query +.. automethod:: Oscilloscope.get_error + +Oscilloscope state control +-------------------------- + +.. automethod:: Oscilloscope.run +.. automethod:: Oscilloscope.stop +.. automethod:: Oscilloscope.is_running +.. autoproperty:: Oscilloscope.active_channels + +Acquisition and transfer options +-------------------------------- + +.. automethod:: Oscilloscope.set_channels_for_capture +.. automethod:: Oscilloscope.set_acquiring_options +.. automethod:: Oscilloscope.set_waveform_export_options + + +-------------- + +.. automethod:: Oscilloscope.capture_and_read +.. automethod:: Oscilloscope.generate_file_header + Auxiliary to the class ====================== @@ -25,8 +65,8 @@ Auxiliary to the class The preamble ============ -The preamble returned by the capture_and_read functions (i.e. returned by the -oscilloscope when querying the VISA command ``:WAV:PREamble?``) is a string of +The preamble returned by the :meth:`capture_and_read` method (i.e. returned by the +oscilloscope when querying the VISA command ``:WAVeform:PREamble?``) is a string of comma separated values, the values have the following meaning:: 0. FORMAT : int16 - 0 = BYTE, 1 = WORD, 4 = ASCII. diff --git a/docs/contents/overview.rst b/docs/contents/overview.rst index 404987c..bf64b8e 100644 --- a/docs/contents/overview.rst +++ b/docs/contents/overview.rst @@ -8,7 +8,9 @@ interfacing in a class :class:`~keyoscacquire.oscacq.Oscilloscope`, and support functions for data processing. Programmes are located in :mod:`keyoscacquire.programmes`, and the same programmes can be run directly from the command line as they are installed in the Python path, -see :ref:`cli-programmes`. Default options are found in :mod:`keyoscacquire.config`. +see :ref:`cli-programmes`. Default options are found in :mod:`keyoscacquire.config`, +and the :mod:`keyoscacquire.traceio` provides functions for plotting, saving, +and loading traces from disk. Quick reference diff --git a/docs/index.rst b/docs/index.rst index 1f671e6..1bac65b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,6 +5,9 @@ :end-before: command-line-use-marker +Table of contents +----------------- + .. toctree:: :maxdepth: 2 :caption: User guide diff --git a/keyoscacquire/VERSION b/keyoscacquire/VERSION index fcdb2e1..c0de572 100644 --- a/keyoscacquire/VERSION +++ b/keyoscacquire/VERSION @@ -1 +1 @@ -4.0.0 +4.0.0-beta diff --git a/keyoscacquire/auxiliary.py b/keyoscacquire/auxiliary.py index c729db4..143488d 100644 --- a/keyoscacquire/auxiliary.py +++ b/keyoscacquire/auxiliary.py @@ -2,7 +2,6 @@ """ Auxiliary functions for the keyoscacquire package -Andreas Svela // 2020 """ import os diff --git a/keyoscacquire/oscacq.py b/keyoscacquire/oscacq.py index 5a8674a..e9972da 100644 --- a/keyoscacquire/oscacq.py +++ b/keyoscacquire/oscacq.py @@ -2,7 +2,7 @@ """ The PyVISA communication with the oscilloscope. -See Keysight's Programmer's Guide for reference. +See Keysight's Programmer's Guide for reference on the VISA commands. Andreas Svela // 2019 """ @@ -40,7 +40,11 @@ class Oscilloscope: """PyVISA communication with the oscilloscope. - Init opens a connection to an instrument and chooses settings for the connection. + Init opens a connection to an instrument and chooses default settings + for the connection and acquisition. + + Leading underscores indicate that an attribute or method is read-only or + suggested to be for interal use only. Parameters ---------- @@ -50,7 +54,7 @@ class Oscilloscope: Example address ``'USB0::1234::1234::MY1234567::INSTR'`` timeout : int, default :data:`~keyoscacquire.config._timeout` Milliseconds before timeout on the channel to the instrument - verbose : bool, default True + verbose : bool, default ``True`` If ``True``: prints when the connection to the device is opened etc, and sets attr:`verbose_acquistion` to ``True`` @@ -61,7 +65,21 @@ class Oscilloscope: Attributes ---------- - _inst : pyvisa.resources.Resource + verbose : bool + If ``True``: prints when the connection to the device is opened, the + acquistion mode, etc + verbose_acquistion : bool + If ``True``: prints that the capturing starts and the number of points + captured + fname : str, default :data:`~keyoscacquire.config._filename` + The filename to which the trace will be saved with :meth:`save_trace()` + ext : str, default :data:`~keyoscacquire.config._filetype` + The extension for saving traces, must include the period, e.g. ``.csv`` + savepng : bool, default :data:`~keyoscacquire.config._export_png` + If ``True``: will save a png of the plot when :meth:`save_trace()` + showplot : bool, default data:`~keyoscacquire.config._show_plot` + If ``True``: will show a matplotlib plot window when :meth:`save_trace()` + _inst : :class:`pyvisa.resources.Resource` The oscilloscope PyVISA resource _id : str The maker, model, serial and firmware version of the scope. Examples:: @@ -71,12 +89,14 @@ class Oscilloscope: _model : str The instrument model name - _model_series : str - The model series, e.g. '2000' for a DSO-X 2024A. See :func:`keyoscacquire.auxiliary.interpret_visa_id`. _address : str Visa address of instrument - timeout : int - Milliseconds before timeout on the communications with the instrument + _time : :class:`~numpy.ndarray` + The time axis of the most recent captured trace + _values : :class:`~numpy.ndarray` + The values for the most recent captured trace + _capture_channels : list of ints + The channels of captured for the most recent trace acq_type : {'HRESolution', 'NORMal', 'AVERage', 'AVER'} Acquisition mode of the oscilloscope. will be used as :attr:`num_averages` if supplied. @@ -113,9 +133,6 @@ class Oscilloscope: * ``'WORD'`` formatted data transfers signed 16-bit data as two bytes. * ``'BYTE'`` formatted data is transferred as signed 8-bit bytes. - - verbose_acquistion : bool - ``True`` prints that the capturing starts and the number of points captured """ _raw = None _metadata = None @@ -143,10 +160,10 @@ def __init__(self, address=config._visa_address, timeout=config._timeout, verbos if self._inst.resource_name.endswith('SOCKET'): self._inst.read_termination = '\n' # Clear the status data structures, the device-defined error queue, and the Request-for-OPC flag - self._inst.write('*CLS') + self.write('*CLS') # Make sure WORD and BYTE data is transeferred as signed ints and lease significant bit first - self._inst.write(':WAVeform:UNSigned OFF') - self._inst.write(':WAVeform:BYTeorder LSBFirst') # MSBF is default, must be overridden for WORD to work + self.write(':WAVeform:UNSigned OFF') + self.write(':WAVeform:BYTeorder LSBFirst') # MSBF is default, must be overridden for WORD to work # Get information about the connected device self._id = self.query('*IDN?') if self.verbose: @@ -158,7 +175,7 @@ def __init__(self, address=config._visa_address, timeout=config._timeout, verbos print(" InfiniiVision X-series oscilloscopes.") # Populate attributes and set standard settings if self.verbose: - print("Current settings:") + print("Using settings:") self.set_acquiring_options(wav_format=config._waveform_format, acq_type=config._acq_type, num_averages=config._num_avg, p_mode='RAW', num_points=0, verbose_acquistion=verbose) @@ -181,14 +198,16 @@ def write(self, command): self._inst.write(command) def query(self, command, action=""): - """Query a VISA command to the oscilloscope. + """Query a VISA command to the oscilloscope. Will ask the oscilloscope + for the latest error if the query times out. Parameters ---------- command : str VISA query action : str, default "" - + Optional argument used to customise the error message if there is a + timeout """ try: return self._inst.query(command).strip() @@ -233,11 +252,11 @@ def get_error(self): def run(self): """Set the ocilloscope to running mode.""" - self._inst.write(':RUN') + self.write(':RUN') def stop(self): """Stop the oscilloscope.""" - self._inst.write(':STOP') + self.write(':STOP') def is_running(self): """Determine if the oscilloscope is running. @@ -253,33 +272,38 @@ def is_running(self): @property def timeout(self): + """The timeout on the VISA communication with the instrument. The + timeout must be longer than the acquisition time. + + :getter: Returns the number of milliseconds before timeout of a query command + :setter: SeMilliseconds before timeout of a query command + :type: int + """ return self._inst.timeout @timeout.setter - def timeout(self, val: int): + def timeout(self, timeout: int): + """See getter""" self._inst.timeout = val @property def active_channels(self): - """List of the currently active channels on the instrument + """Find the currently active channels on the instrument - Returns - ------- - list of ints - list of the active channels, for example ``[1, 3]`` + .. note:: Changing the active channels will not affect with channels are + captured unless :meth:`set_channels_for_capture()` is subsequently run. + The :meth:`get_traces()` family of methods will make sure of this. + + :getter: Returns a list of the active channels, for example ``[1, 3]`` + :setter: list of the active channels, for example ``[1, 3]`` + :type: list of ints """ # querying DISP for each channel to determine which channels are currently displayed return [i for i in range(1, 5) if bool(int(self.query(f":CHAN{i}:DISP?")))] @active_channels.setter - def active_channels(self, channels): - """Get list of the currently active channels on the instrument - - Parameters - ---------- - list of ints - list of the channels to set active, for example ``[1, 3]`` - """ + def active_channels(self, channels: list): + """See getter""" if not isinstance(channels, list): channels = [channels] for i in range(1, 5): @@ -335,14 +359,14 @@ def set_acquiring_options(self, wav_format=None, acq_type=None, else: if num_averages is not None: self.num_averages = num_averages - self._inst.write(':ACQuire:TYPE ' + self.acq_type) + self.write(':ACQuire:TYPE ' + self.acq_type) if self.verbose: print(" Acquisition type:", self.acq_type) # Check that self.num_averages is within the acceptable range if self.acq_type == 'AVER': if not (1 <= self.num_averages <= 65536): raise ValueError(f"\nThe number of averages {self.num_averages} is out of range.") - self._inst.write(f":ACQuire:COUNt {self.num_averages}") + self.write(f":ACQuire:COUNt {self.num_averages}") if self.verbose: print(" # of averages: ", self.num_averages) # Set options for waveform export @@ -366,7 +390,7 @@ def set_waveform_export_options(self, wav_format=None, num_points=None, p_mode=N """ # Choose format for the transmitted waveform if wav_format is not None: - self._inst.write(f":WAVeform:FORMat {wav_format}") + self.write(f":WAVeform:FORMat {wav_format}") self.wav_format = wav_format if p_mode is not None: self.p_mode = p_mode @@ -376,22 +400,22 @@ def set_waveform_export_options(self, wav_format=None, num_points=None, p_mode=N self.p_mode = 'NORM' _log.debug(f":WAVeform:POINts:MODE overridden (from {p_mode}) to " "NORMal due to :ACQuire:TYPE:AVERage.") - self._inst.write(f":WAVeform:POINts:MODE {self.p_mode}") + self.write(f":WAVeform:POINts:MODE {self.p_mode}") # Set to maximum number of points if if num_points is not None: if num_points == 0: - self._inst.write(f":WAVeform:POINts MAXimum") + self.write(f":WAVeform:POINts MAXimum") _log.debug("Number of points set to: MAX") self.num_points = num_points # If number of points has been specified, tell the instrument to # use this number of points elif num_points > 0: if self._model_series in _supported_series: - self._inst.write(f":WAVeform:POINts {self.num_points}") + self.write(f":WAVeform:POINts {self.num_points}") elif self._model_series in ['9000']: - self._inst.write(f":ACQuire:POINts {self.num_points}") + self.write(f":ACQuire:POINts {self.num_points}") else: - self._inst.write(f":WAVeform:POINts {self.num_points}") + self.write(f":WAVeform:POINts {self.num_points}") _log.debug("Number of points set to: ", self.num_points) self.num_points = num_points @@ -465,7 +489,7 @@ def capture_and_read(self, set_running=True): # DIGitize is a specialized RUN command. # Waveforms are acquired according to the settings of the :ACQuire commands. # When acquisition is complete, the instrument is stopped. - self._inst.write(':DIGitize ' + ", ".join(self._sources)) + self.write(':DIGitize ' + ", ".join(self._sources)) ## Read from the scope if self.wav_format[:3] in ['WOR', 'BYT']: self._read_binary(datatype=_datatypes[self.wav_format]) @@ -513,7 +537,7 @@ def _read_binary(self, datatype='standard'): # Loop through all the sources for source in self._sources: # Select the channel for which the succeeding WAVeform commands applies to - self._inst.write(':WAVeform:SOURce ' + source) + self.write(':WAVeform:SOURce ' + source) try: # obtain comma separated metadata values for processing of raw data for this source self._metadata.append(self.query(':WAVeform:PREamble?')) @@ -556,7 +580,7 @@ def _read_ascii(self): # Loop through all the sources for source in self._sources: # Select the channel for which the succeeding WAVeform commands applies to - self._inst.write(':WAVeform:SOURce ' + source) + self.write(':WAVeform:SOURce ' + source) # Read out data for this source self._raw.append(self.query(':WAVeform:DATA?', action="obtain the waveform")) # Get the preamble (used for calculating time axis, which is the same @@ -568,19 +592,22 @@ def _read_ascii(self): ## Building functions to get a trace and various option setting and processing ## def get_trace(self, verbose_acquistion=None): - """Obtain one trace with current settings. + """Obtain one trace with current settings. Will return the values + of the traces, but alos populate a few attributes, including + ``_time``, ``_values`` and ``_capture_channels``. + + Use :meth:`save_trace()` to save the trace to disk. Parameters ---------- verbose_acquistion : bool or ``None``, default ``None`` - Possibility to override :attr:`verbose_acquistion` temporarily, - but the current setting will be restored afterwards + Optionally change :attr:`verbose_acquistion` Returns ------- - time : :class:`~numpy.ndarray` + _time : :class:`~numpy.ndarray` Time axis for the measurement - y : :class:`~numpy.ndarray` + _values : :class:`~numpy.ndarray` Voltage values, same sequence as sources input, each row represents one channel _capture_channels : list of ints @@ -590,16 +617,11 @@ def get_trace(self, verbose_acquistion=None): self.set_channels_for_capture() # Possibility to override verbose_acquistion if verbose_acquistion is not None: - # Store current setting and set temporary setting - temp = self.verbose_acquistion self.verbose_acquistion = verbose_acquistion # Capture, read and process data self.capture_and_read() self._time, self._values = process_data(self._raw, self._metadata, self.wav_format, - verbose_acquistion=self.verbose_acquistion) - # Restore self.verbose_acquistion to previous setting - if verbose_acquistion is not None: - self.verbose_acquistion = temp + verbose_acquistion=self.verbose_acquistion) return self._time, self._values, self._capture_channels def set_options_get_trace(self, channels=None, wav_format=None, acq_type=None, @@ -629,10 +651,11 @@ def set_options_get_trace(self, channels=None, wav_format=None, acq_type=None, Returns ------- - time : :class:`~numpy.ndarray` + _time : :class:`~numpy.ndarray` Time axis for the measurement - y : :class:`~numpy.ndarray` - Voltage values, same sequence as ``channels``, each row represents one channel + _values : :class:`~numpy.ndarray` + Voltage values, same sequence as sources input, each row + represents one channel _capture_channels : list of ints list of the channels obtaied from, example ``[1, 3]`` """ @@ -689,6 +712,8 @@ def set_options_get_trace_save(self, fname=None, ext=None, num_points : int, default 0 Use 0 to let :attr:`p_mode` control the number of points, otherwise override with a lower number than maximum for the :attr:`p_mode` + additional_header_info : str, default ```None`` + Will put this string as a separate line before the column headers """ self.set_options_get_trace(channels=channels, source_type=source_type, wav_format=wav_format, acq_type=acq_type, num_averages=num_averages, @@ -772,6 +797,8 @@ def save_trace(self, fname=None, ext=None, additional_header_info=None, Filename of trace ext : ``{'.csv', '.npy'}``, default :data:`~keyoscacquire.config._ext` Choose the filetype of the saved trace + additional_header_info : str, default ```None`` + Will put this string as a separate line before the column headers savepng : bool, default :data:`~keyoscacquire.config._export_png` Choose whether to also save a png with the same filename showplot : bool, default :data:`~keyoscacquire.config._show_plot` diff --git a/keyoscacquire/programmes.py b/keyoscacquire/programmes.py index d8287aa..bcd1478 100644 --- a/keyoscacquire/programmes.py +++ b/keyoscacquire/programmes.py @@ -1,12 +1,14 @@ # -*- coding: utf-8 -*- """ -Python backend for installed command line programmes. These can also be integrated in python scripts or used as examples. - - * :func:`list_visa_devices`: listing visa devices - * :func:`path_of_config`: finding path of config.py - * :func:`get_single_trace`: taking a single trace and saving it to csv and png - * :func:`get_traces_single_connection_loop` :func:`get_traces_connect_each_time_loop`: two programmes for taking multiple traces when a key is pressed, see descriptions for difference - * :func:`get_num_traces`: get a specific number of traces +Python backend for installed command line programmes. These can also be +integrated in python scripts or used as examples. + +* :func:`list_visa_devices`: listing visa devices +* :func:`path_of_config`: finding path of config.py to set default options +* :func:`get_single_trace`: taking a single trace and saving it to csv and png +* :func:`get_traces_single_connection_loop` :func:`get_traces_connect_each_time_loop`: + two programmes for taking multiple traces when a key is pressed, see descriptions for difference +* :func:`get_num_traces`: get a specific number of traces """ diff --git a/keyoscacquire/traceio.py b/keyoscacquire/traceio.py index 16a3e26..679e4ff 100644 --- a/keyoscacquire/traceio.py +++ b/keyoscacquire/traceio.py @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- """ -Trace input/output functions for the keyoscacquire package +This module provides functions for saving traces to ``npy`` format files +(see :mod:`numpy.lib.format`) or ascii files. The latter is slower but permits +a header with metadata for the measurement, see :func:`Oscilloscope.generate_file_header` +which is used when saving directly from the ``Oscilloscope`` class. -Andreas Svela // 2020 """ import os @@ -22,7 +24,7 @@ def save_trace(fname, time, y, fileheader="", ext=config._filetype, """Saves the trace with time values and y values to file. Current date and time is automatically added to the header. Saving to numpy - format with :func:`save_trace_npy` is faster, but does not include metadata + format with :func:`save_trace_npy()` is faster, but does not include metadata and header. Parameters @@ -99,7 +101,7 @@ def plot_trace(time, y, channels, fname="", showplot=config._show_plot, Filename of possible exported png show : bool, default :data:`~keyoscacquire.config._show_plot` True shows the plot (must be closed before the programme proceeds) - savepng : bool, default :data:`~keyoscacquire.`config._export_png` + savepng : bool, default :data:`~keyoscacquire.config._export_png` ``True`` exports the plot to ``fname``.png """ for i, vals in enumerate(np.transpose(y)): # for each channel @@ -113,9 +115,13 @@ def plot_trace(time, y, channels, fname="", showplot=config._show_plot, ## Trace loading ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## -def load_trace(fname, ext=config._filetype, column_names='auto', skip_lines='auto', return_df=True): +def load_trace(fname, ext=config._filetype, column_names='auto', skip_lines='auto', + return_as_df=True): """Load a trace saved with keyoscacquire.oscacq.save_file() + What is returned depends on the format of the file (.npy files contain no + headers), and if a dataframe format is chosen for the return. + Parameters ---------- fname : str @@ -127,14 +133,21 @@ def load_trace(fname, ext=config._filetype, column_names='auto', skip_lines='aut To infer df column names from the last line of the header, use ``'auto'`` (expecting '# ' as the last line of the header), or specify the column names manually - skip_lines : ``{'auto' or int}``, default ``'auto'`` - return_df : bool, default True + return_as_df : bool, default True + If the loaded trace is not a .npy file, decide to return the data as + a Pandas dataframe if ``True``, or as an ndarray otherwise Returns ------- - + data : :class:`~pandas.Dataframe` or :class:`~numpy.ndarray` + If ``return_as_df`` is ``True`` and the filetype is not ``.npy``, + a Pandas dataframe is returned. Otherwise ndarray. The first column + is time, then each column is a channel. + header : list or ``None`` + If ``.npy``, ``None`` is returned. Otherwise, a list of the lines at the + beginning of the file starting with ``'#'``, stripped off ``'# '`` is returned """ # Remove extenstion if provided in the fname if fname[-4:] in ['.npy', '.csv']: @@ -146,25 +159,22 @@ def load_trace(fname, ext=config._filetype, column_names='auto', skip_lines='aut else: return _load_trace_with_header(fname, ext, column_names=column_names, skip_lines=skip_lines, - return_df=return_df) + return_as_df=return_as_df) -def _load_trace_with_header(fname, ext, skip_lines='auto', column_names='auto', return_df=True): - """ +def _load_trace_with_header(fname, ext, skip_lines='auto', column_names='auto', + return_as_df=True): + """Read a trace file that has a header (i.e. not ``.npy`` files). - Parameters - ---------- - fname : str - Filename of trace, with or without extension - ext : str, default :data:`~keyoscacquire.config._filetype` - The filetype of the saved trace (with the period, e.g. ``'.csv'``) + See parameter description for :func:`load_trace()`. Returns ------- - data : - :class:`~pandas.Dataframe` or :class:`~numpy.ndarray` + data : :class:`~pandas.Dataframe` or :class:`~numpy.ndarray` + Pandas dataframe if ``return_as_df`` is ``True``, ndarray otherwise header : list - Lines at the beginning of the file starting with ``'#'``, stripped off ``'# '`` + Lines at the beginning of the file starting with ``'#'``, stripped + off ``'# '`` """ # Load header header = load_header(fname, ext) @@ -194,7 +204,8 @@ def load_header(fname, ext=config._filetype): Returns ------- header : list - Lines at the beginning of the file starting with ``'#'``, stripped off ``'# '`` + Lines at the beginning of the file starting with ``'#'``, stripped + off ``'# '`` """ if fname[-4:] in ['.csv']: ext = fname[-4:] From 9ecae37722fafd777bcdf2d0783329ac35ea2f74 Mon Sep 17 00:00:00 2001 From: asvela at 0011 Date: Sat, 19 Dec 2020 23:36:48 +0100 Subject: [PATCH 05/52] sorting bugs and improving docs --- README.rst | 16 +++++++++------- docs/changelog.rst | 2 ++ docs/contents/overview.rst | 2 +- docs/requirements.txt | 1 + keyoscacquire/oscacq.py | 34 +++++++++++++++------------------- keyoscacquire/traceio.py | 2 +- 6 files changed, 29 insertions(+), 28 deletions(-) diff --git a/README.rst b/README.rst index 49f7215..e225f7a 100644 --- a/README.rst +++ b/README.rst @@ -19,6 +19,14 @@ using a USB connection. need to be running on the computer. Installation of Keysight Connection Expert might also be necessary. + +Documentation +------------- + +Available at `keyoscacquire.rtfd.io `_. +A few examples below, but formatting and links are broken as the snippet is intended +for the documentation parser. + .. command-line-use-marker Command line use @@ -77,13 +85,7 @@ array which columns contain the data from the active channels listed in Explore the Reference section (particularly :ref:`osc-class`) to get more information about the API. -.. documentation-marker - -Documentation -------------- - -Available at `keyoscacquire.rtfd.io `_. - +.. contribute-marker Contribute ---------- diff --git a/docs/changelog.rst b/docs/changelog.rst index 0895d39..41c1e8d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -62,6 +62,7 @@ v4.0.0 (2020-12) * ``Oscilloscope.plot_trace()`` - *New properties*: + * ``Oscilloscope.active_channels`` can now be used to set and get the currently active channels * ``Oscilloscope.timeout`` this was previously just an attribute with no @@ -98,6 +99,7 @@ v4.0.0 (2020-12) * ``Oscilloscope.get_trace()`` - *No compatibility*: Misc + * ``Oscilloscope.get_trace()`` now also returns also ``Oscilloscope.num_channels`` * ``Oscilloscope.get_active_channels()`` is now a property ``active_channels`` diff --git a/docs/contents/overview.rst b/docs/contents/overview.rst index 404987c..df5e425 100644 --- a/docs/contents/overview.rst +++ b/docs/contents/overview.rst @@ -16,7 +16,7 @@ Quick reference .. include:: ../../README.rst :start-after: command-line-use-marker - :end-before: documentation-marker + :end-before: contribute-marker Installation diff --git a/docs/requirements.txt b/docs/requirements.txt index a4223ef..4cbda3d 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -4,6 +4,7 @@ furo recommonmark pyvisa numpy +pandas argparse matplotlib tqdm diff --git a/keyoscacquire/oscacq.py b/keyoscacquire/oscacq.py index 5a8674a..5606706 100644 --- a/keyoscacquire/oscacq.py +++ b/keyoscacquire/oscacq.py @@ -357,9 +357,8 @@ def set_waveform_export_options(self, wav_format=None, num_points=None, p_mode=N wav_format : {``'WORD'``, ``'BYTE'``, ``'ASCii'``}, default :data:`~keyoscacquire.config._waveform_format` Select the format of the communication of waveform from the oscilloscope, see :attr:`wav_format` - p_mode : {``'NORMal'``, ``'RAW'``, ``'MAXimum'``}, default ``'RAW'`` + p_mode : {``'NORMal'``, ``'RAW'``}, default ``'RAW'`` ``'NORMal'`` is limited to 62,500 points, whereas ``'RAW'`` gives up to 1e6 points. - Use ``'MAXimum'`` for sources that are not analogue or digital num_points : int, default 0 Use 0 to get the maximum amount of points, otherwise override with a lower number than maximum for the :attr:`p_mode` @@ -424,7 +423,7 @@ def set_channels_for_capture(self, channels=None): # Build list of sources self._sources = [f"CHANnel{ch}" for ch in self._capture_channels] if self.verbose_acquistion: - print(f"Acquire from channels {self._capture_channels}") + print(f"Acquire from channels: {self._capture_channels}") return self._capture_channels def capture_and_read(self, set_running=True): @@ -567,11 +566,15 @@ def _read_ascii(self): ## Building functions to get a trace and various option setting and processing ## - def get_trace(self, verbose_acquistion=None): + def get_trace(self, channels=None, verbose_acquistion=None): """Obtain one trace with current settings. Parameters ---------- + channels : list of ints or ``'active'``, default :data:`~keyoscacquire.config._ch_nums` + list of the channel numbers to be acquired, example ``[1, 3]``. + Use ``'active'`` or ``[]`` to capture all the currently active + channels on the oscilloscope. verbose_acquistion : bool or ``None``, default ``None`` Possibility to override :attr:`verbose_acquistion` temporarily, but the current setting will be restored afterwards @@ -586,8 +589,7 @@ def get_trace(self, verbose_acquistion=None): _capture_channels : list of ints list of the channels obtaied from, example ``[1, 3]`` """ - if self._capture_active: - self.set_channels_for_capture() + self.set_channels_for_capture(channels=channels) # Possibility to override verbose_acquistion if verbose_acquistion is not None: # Store current setting and set temporary setting @@ -640,15 +642,12 @@ def set_options_get_trace(self, channels=None, wav_format=None, acq_type=None, self.set_acquiring_options(wav_format=wav_format, acq_type=acq_type, num_averages=num_averages, p_mode=p_mode, num_points=num_points) - ## Select sources - self.set_channels_for_capture(channels=channels) ## Capture, read and process data self.get_trace() return self._time, self._values, self._capture_channels def set_options_get_trace_save(self, fname=None, ext=None, - channels=None, source_type=None, - wav_format=None, acq_type=None, + channels=None, wav_format=None, acq_type=None, num_averages=None, p_mode=None, num_points=None, additional_header_info=None): """Get trace and save the trace to a file and plot to png. @@ -671,9 +670,6 @@ def set_options_get_trace_save(self, fname=None, ext=None, list of the channel numbers to be acquired, example ``[1, 3]``. Use ``'active'`` or ``[]`` to capture all the currently active channels on the oscilloscope. - source_type : str, default ``'CHANnel'`` - Selects the source type. Must be ``'CHANnel'`` in current implementation. - Future version might include {'MATH', 'FUNCtion'} wav_format : {``'WORD'``, ``'BYTE'``, ``'ASCii'``}, default :data:`~keyoscacquire.config._waveform_format` Select the format of the communication of waveform from the oscilloscope, see :attr:`wav_format` @@ -690,8 +686,8 @@ def set_options_get_trace_save(self, fname=None, ext=None, Use 0 to let :attr:`p_mode` control the number of points, otherwise override with a lower number than maximum for the :attr:`p_mode` """ - self.set_options_get_trace(channels=channels, source_type=source_type, - wav_format=wav_format, acq_type=acq_type, num_averages=num_averages, + self.set_options_get_trace(channels=channels, wav_format=wav_format, + acq_type=acq_type, num_averages=num_averages, p_mode=p_mode, num_points=num_points) self.save_trace(fname, ext, additional_header_info=additional_header_info) @@ -886,8 +882,8 @@ def _process_data_binary(raw, preambles, verbose_acquistion=True): time = np.array([(np.arange(num_samples)-xRef)*xIncr + xOrig]) # compute x-values time = time.T # make x values vertical if verbose_acquistion: - print("Points captured per channel: ", num_samples) - _log.info("Points captured per channel: ", num_samples) + print(f"Points captured per channel: {num_samples:,d}") + _log.info(f"Points captured per channel: {num_samples:,d}") y = np.empty((len(raw), num_samples)) for i, data in enumerate(raw): # process each channel individually preamble = preambles[i].split(',') @@ -927,8 +923,8 @@ def _process_data_ascii(raw, metadata, verbose_acquistion=True): time = np.array([(np.arange(num_samples)-xRef)*xIncr + xOrig]) time = time.T # Make list vertical if verbose_acquistion: - print("Points captured per channel: ", num_samples) - _log.info("Points captured per channel: ", num_samples) + print(f"Points captured per channel: {num_samples:,d}") + _log.info(f"Points captured per channel: {num_samples:,d}") y = [] for data in raw: if model_series in ['2000']: diff --git a/keyoscacquire/traceio.py b/keyoscacquire/traceio.py index 16a3e26..a2763cb 100644 --- a/keyoscacquire/traceio.py +++ b/keyoscacquire/traceio.py @@ -48,7 +48,7 @@ def save_trace(fname, time, y, fileheader="", ext=config._filetype, if os.path.exists(fname+ext): raise RuntimeError(f"{fname+ext} already exists") if print_filename: - print(f"Saving trace to: {fname+ext}\n") + print(f"Saving trace to: {fname+ext}\n") data = np.append(time, y, axis=1) # make one array with columns x y1 y2 .. if ext == ".npy": if fileheader and not nowarn: From 64a94661559c08e1b75cafe42ce4d5d8b845f07b Mon Sep 17 00:00:00 2001 From: asvela at 0011 Date: Sun, 20 Dec 2020 02:08:05 +0100 Subject: [PATCH 06/52] acquistition and transform attributes are now properties --- README.rst | 1 + docs/changelog.rst | 4 - docs/contents/usage.rst | 15 +-- docs/index.rst | 2 +- keyoscacquire/config.py | 2 +- keyoscacquire/oscacq.py | 287 ++++++++++++++++++++++++++-------------- 6 files changed, 201 insertions(+), 110 deletions(-) diff --git a/README.rst b/README.rst index e225f7a..99a610f 100644 --- a/README.rst +++ b/README.rst @@ -19,6 +19,7 @@ using a USB connection. need to be running on the computer. Installation of Keysight Connection Expert might also be necessary. +.. documentation-marker Documentation ------------- diff --git a/docs/changelog.rst b/docs/changelog.rst index de5ccfc..99a7f46 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -102,11 +102,7 @@ v4.0.0 (2020-12) * ``Oscilloscope.get_trace()`` - *No compatibility*: Misc -<<<<<<< HEAD - -======= ->>>>>>> a4667078e715ef15060b2ab292e3077d7acc6f44 * ``Oscilloscope.get_trace()`` now also returns also ``Oscilloscope.num_channels`` * ``Oscilloscope.get_active_channels()`` is now a property ``active_channels`` diff --git a/docs/contents/usage.rst b/docs/contents/usage.rst index 37153d7..18550e0 100644 --- a/docs/contents/usage.rst +++ b/docs/contents/usage.rst @@ -109,12 +109,12 @@ The package provides an API for use with your Python code. For example .. literalinclude :: ../../keyoscacquire/scripts/example.py :linenos: -.. todo :: Expand examples - See :ref:`osc-class` and :ref:`data-proc` for more. The command line programmes -have a python backend that can integrated in Python scripts or used as +have a Python backend that can integrated in Python scripts or used as examples, see :ref:`py-programmes`. +.. todo :: Expand examples + Note on obtaining traces when the scope is running vs when stopped @@ -143,8 +143,8 @@ The package takes its default options from :mod:`keyoscacquire.config` .. literalinclude :: ../../keyoscacquire/config.py :linenos: -.. note:: None of the functions access the global variables directly, but they - are feed them as default arguments. +.. note:: Changing these after importing the module with an ``import`` statement + will not have any effect. The command line programmes will save traces in the folder from where they are ran as ``_filename+_file_delimiter++_filetype``, i.e. by default as @@ -190,9 +190,8 @@ the executable, e.g. get_single_trace -f "fname" -a "AVER" - -Scripts in ./scripts --------------------- +Scripts in ``./scripts`` +------------------------ These can be ran as command line programmes from the scripts folder with ``$ python [script].py [options]``, where the options are as for the installed diff --git a/docs/index.rst b/docs/index.rst index 1bac65b..465e07a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2,7 +2,7 @@ sphinx-quickstart on Mon Oct 21 05:06:26 2019. .. include:: ../README.rst - :end-before: command-line-use-marker + :end-before: documentation-marker Table of contents diff --git a/keyoscacquire/config.py b/keyoscacquire/config.py index 56584cf..401cde9 100644 --- a/keyoscacquire/config.py +++ b/keyoscacquire/config.py @@ -13,7 +13,7 @@ #: {HRESolution, NORMal, AVER} where is the number of averages in range [2, 65536] _acq_type = "HRESolution" #: default number of averages used if only AVER is given as acquisition type -_num_avg = 2 +_num_avg = None #: default base filename of all traces and pngs exported, a number is appended to the base _filename = "data" #: delimiter used between :attr:`_filename` and filenumber (before _filetype) diff --git a/keyoscacquire/oscacq.py b/keyoscacquire/oscacq.py index 5c4c0d9..ec21ab2 100644 --- a/keyoscacquire/oscacq.py +++ b/keyoscacquire/oscacq.py @@ -32,7 +32,7 @@ _screen_colors = {1:'C1', 2:'C2', 3:'C0', 4:'C3'} #: Datatype is ``'h'`` for 16 bit signed int (``WORD``), ``'b'`` for 8 bit signed bit (``BYTE``). #: Same naming as for structs `docs.python.org/3/library/struct.html#format-characters` -_datatypes = {'BYTE':'b', 'WORD':'h'} +_datatypes = {'BYT':'b', 'WOR':'h', 'BYTE':'b', 'WORD':'h'} ## ========================================================================= ## @@ -97,42 +97,6 @@ class Oscilloscope: The values for the most recent captured trace _capture_channels : list of ints The channels of captured for the most recent trace - acq_type : {'HRESolution', 'NORMal', 'AVERage', 'AVER'} - Acquisition mode of the oscilloscope. will be used as :attr:`num_averages` if supplied. - - * ``'NORMal'`` — sets the oscilloscope in the normal mode. - - * ``'AVERage'`` — sets the oscilloscope in the averaging mode. - You can set the count by :attr:`num_averages`. - - * ``'HRESolution'`` — sets the oscilloscope in the high-resolution mode - (also known as smoothing). This mode is used to reduce noise at slower - sweep speeds where the digitizer samples faster than needed to fill memory for the displayed time range. - - For example, if the digitizer samples at 200 MSa/s, but the effective sample rate is 1 MSa/s - (because of a slower sweep speed), only 1 out of every 200 samples needs to be stored. - Instead of storing one sample (and throwing others away), the 200 samples are averaged - together to provide the value for one display point. The slower the sweep speed, the greater - the number of samples that are averaged together for each display point. - num_averages : int, 2 to 65536 - The number of averages applied (applies only to the ``'AVERage'`` :attr:`acq_type`) - p_mode : {``'NORMal'``, ``'RAW'``, ``'MAXimum'``} - ``'NORMal'`` is limited to 62,500 points, whereas ``'RAW'`` gives up to - 1e6 points. Use ``'MAXimum'`` for sources that are not analogue or digital. - num_points : int - Use 0 to let :attr:`p_mode` control the number of points, otherwise - override with a lower number than maximum for the :attr:`p_mode` - wav_format : {'WORD', 'BYTE', 'ASCii'} - Select the data transmission mode for waveform data points, i.e. how - the data is formatted when sent from the oscilloscope. - - * ``'ASCii'`` formatted data converts the internal integer data values - to real Y-axis values. Values are transferred as ascii digits in - floating point notation, separated by commas. - - * ``'WORD'`` formatted data transfers signed 16-bit data as two bytes. - - * ``'BYTE'`` formatted data is transferred as signed 8-bit bytes. """ _raw = None _metadata = None @@ -248,7 +212,7 @@ def get_error(self): error number,description """ # Do not use self.query here as that can lead to infinite nesting! - return self._inst.query(":SYSTem:ERRor?") + return self._inst.query(":SYSTem:ERRor?").strip() def run(self): """Set the ocilloscope to running mode.""" @@ -276,7 +240,7 @@ def timeout(self): timeout must be longer than the acquisition time. :getter: Returns the number of milliseconds before timeout of a query command - :setter: SeMilliseconds before timeout of a query command + :setter: Set the number of milliseconds before timeout of a query command :type: int """ return self._inst.timeout @@ -309,6 +273,181 @@ def active_channels(self, channels: list): for i in range(1, 5): self.write(f":CHAN{i}:DISP {int(i in channels)}") + @property + def acq_type(self): + """Acquisition mode of the oscilloscope + + Can be either + + * ``'NORMal'`` — sets the oscilloscope in the normal mode. + * ``'AVERage'`` or ``'AVER'`` — sets the oscilloscope in the averaging mode. + The number of averages can be set with :property:`num_averages`, or + will be used as :property:`num_averages` if supplied. + can be in the range 2 to 65,536 + * ``'HRESolution'`` — sets the oscilloscope in the high-resolution mode + (also known as smoothing). This mode is used to reduce noise at slower + sweep speeds where the digitizer samples faster than needed to fill memory for the displayed time range. + + For example, if the digitizer samples at 200 MSa/s, but the effective sample rate is 1 MSa/s + (because of a slower sweep speed), only 1 out of every 200 samples needs to be stored. + Instead of storing one sample (and throwing others away), the 200 samples are averaged + together to provide the value for one display point. The slower the sweep speed, the greater + the number of samples that are averaged together for each display point. + + :getter: Returns the current mode (will not return ```` for ``AVER``) + :setter: Sets the mode, for example ``AVER8``, if :attr:`verbose` will + print the type and the number of averages number + :type: ``{'NORMal', 'AVERage', 'AVER', 'HRES'} + + Raises + ------ + ValueError + If ```` in cannot be converted to an int (or is out of range) + """ + return self.query(":ACQuire:TYPE?") + + @acq_type.setter + def acq_type(self, type: str): + """See getter""" + acq_type = type[:4].upper() + self.write(f":ACQuire:TYPE {acq_type}") + if self.verbose: + print(f" Acquisition type: {acq_type}") + # Handle AVER expressions + if acq_type == 'AVER': + if len(type) > 4 and not type[4:].lower() == 'age': + try: + self.num_averages = int(type[4:]) + except ValueError: + ValueError(f"\nValueError: Failed to convert '{type[4:]}' to an integer, " + "check that acquisition type is on the form AVER or AVER " + f"where is an integer (currently acq. type is '{type}').\n") + else: + num = self.num_averages + if self.verbose: + print(f" # of averages: {num}") + + @property + def num_averages(self): + """The number of averages taken if the scope is in the ``'AVERage'`` + :property:`acq_type` + + :getter: Returns the current number of averages + :setter: Set the number, will print the number if :attr:`verbose` + :type: int, 2 to 65,536 + + Raises + ------ + ValueError + If the number is is out of range + """ + return self.query(":ACQuire:COUNt?") + + @num_averages.setter + def num_averages(self, num: int): + """See getter""" + if not (2 <= num <= 65536): + raise ValueError(f"\nThe number of averages {num} is out of range.") + self.write(f":ACQuire:COUNt {num}") + if self.verbose and self.acq_type == 'AVER': + print(f" # of averages: {num}") + + @property + def p_mode(self): + """The points mode of the acquistion + + ``'NORMal'`` is limited to 62,500 points, whereas ``'RAW'`` gives up to + 1e6 points. Use ``'MAXimum'`` for sources that are not analogue or digital. + + :getter: Returns the current mode + :setter: Set the mode, will check if compatible with the :property:`acq_type` + :type: ``{'NORMal', 'RAW', 'MAXimum'}`` + """ + return self.query(":WAVeform:POINts:MODE?") + + @p_mode.setter + def p_mode(self, p_mode: str): + """See getter""" + if (not p_mode[:4] == 'NORM') and self.acq_type == 'AVER': + p_mode = 'NORM' + _log.info(f":WAVeform:POINts:MODE overridden (from {p_mode}) to " + "NORMal due to :ACQuire:TYPE:AVERage.") + self.write(f":WAVeform:POINts:MODE {p_mode}") + + @property + def num_points(self): + """The number of points to be acquired for each channel. Use 0 to let + get the maximum number given the :property:`p_mode`, otherwise + override with a lower number than maximum for the :property:`p_mode` + + .. warning:: If the exact number of points is crucial, always check the + number of points with the getter after performing the setter. + + .. note:: The scope must be stopped to get the number of points that + will be transferred when it is in the *stopped* state. As this package + always stops the scope when getting a trace, the getter will also + do this to get the actual number of points that will be + transferred (otherwise the returned number will be capped by the + :property:`p_mode` ``NORMal`` (which can be transferred without + stopping the scope)). + + :getter: Returns the number of points that will be acquired (stopping + and re-running the scope as explained in the note above) + :setter: Set the number, but beware that the scope might change the + number depending on memory depth, time axis settings, etc. + :type: int + """ + # Must stop the scope to be able to read the actual number of points + # that will be transferred in the RAW or MAX mode + self.stop() + points = int(self.query(":WAVeform:POINTs?")) + self.run() + return points + + @num_points.setter + def num_points(self, num_points: int): + """See getter""" + if num_points == 0: + self.write(f":WAVeform:POINts MAXimum") + _log.debug("Number of points set to: MAX") + # If number of points has been specified, tell the instrument to + # use this number of points + elif num_points > 0: + if self._model_series in ['9000']: + self.write(f":ACQuire:POINts {num_points}") + else: + # Must stop the scope to set the number of points to avoid + # getting an error in the scopes' log (however, it seems to + # be working regardless, only the get_error() will return -222) + self.stop() + self.write(f":WAVeform:POINts {num_points}") + self.run() + _log.debug(f"Number of points set to: {num_points}") + + @property + def wav_format(self): + """Data transmission mode for waveform data points, i.e. how + the data is formatted when sent from the oscilloscope. + + * ``'ASCii'`` formatted data converts the internal integer data values + to real Y-axis values. Values are transferred as ascii digits in + floating point notation, separated by commas. + * ``'WORD'`` formatted data transfers signed 16-bit data as two bytes. + * ``'BYTE'`` formatted data is transferred as signed 8-bit bytes. + + :getter: Returns the number of points that will be acquired, however + it does not seem to be fully stable + :setter: Set the number, but beware that the scope might change the + number depending on memory depth, time axis settings, etc. + :type: ``{'WORD', 'BYTE', 'ASCii'}`` + """ + return self.query(":WAVeform:FORMat?") + + @wav_format.setter + def wav_format(self, wav_format: str): + """See getter""" + self.write(f":WAVeform:FORMat {wav_format}") + def set_acquiring_options(self, wav_format=None, acq_type=None, num_averages=None, p_mode=None, num_points=None, verbose_acquistion=None): @@ -341,34 +480,12 @@ def set_acquiring_options(self, wav_format=None, acq_type=None, If num_averages are outside of the range or in acq_type cannot be converted to int """ - # Set verbose_acquistion only if not None if verbose_acquistion is not None: self.verbose_acquistion = verbose_acquistion - # Set the acquistion type if acq_type is not None: - self.acq_type = acq_type[:4].upper() - # Handle AVER expressions - if self.acq_type == 'AVER' and not acq_type[4:].lower() == 'age': - if len(acq_type) > 4: - try: - self.num_averages = int(acq_type[4:]) - except ValueError: - ValueError(f"\nValueError: Failed to convert '{acq_type[4:]}' to an integer, " - "check that acquisition type is on the form AVER or AVER " - f"where is an integer (currently acq. type is '{acq_type}').\n") - else: - if num_averages is not None: - self.num_averages = num_averages - self.write(':ACQuire:TYPE ' + self.acq_type) - if self.verbose: - print(" Acquisition type:", self.acq_type) - # Check that self.num_averages is within the acceptable range - if self.acq_type == 'AVER': - if not (1 <= self.num_averages <= 65536): - raise ValueError(f"\nThe number of averages {self.num_averages} is out of range.") - self.write(f":ACQuire:COUNt {self.num_averages}") - if self.verbose: - print(" # of averages: ", self.num_averages) + self.acq_type = acq_type + if num_averages is not None: + self.num_averages = num_averages # Set options for waveform export self.set_waveform_export_options(wav_format, num_points, p_mode) @@ -389,34 +506,11 @@ def set_waveform_export_options(self, wav_format=None, num_points=None, p_mode=N """ # Choose format for the transmitted waveform if wav_format is not None: - self.write(f":WAVeform:FORMat {wav_format}") self.wav_format = wav_format if p_mode is not None: self.p_mode = p_mode - a_isaver = self.acq_type == 'AVER' - p_isnorm = self.p_mode[:4] == 'NORM' - if a_isaver and not p_isnorm: - self.p_mode = 'NORM' - _log.debug(f":WAVeform:POINts:MODE overridden (from {p_mode}) to " - "NORMal due to :ACQuire:TYPE:AVERage.") - self.write(f":WAVeform:POINts:MODE {self.p_mode}") - # Set to maximum number of points if if num_points is not None: - if num_points == 0: - self.write(f":WAVeform:POINts MAXimum") - _log.debug("Number of points set to: MAX") - self.num_points = num_points - # If number of points has been specified, tell the instrument to - # use this number of points - elif num_points > 0: - if self._model_series in _supported_series: - self.write(f":WAVeform:POINts {self.num_points}") - elif self._model_series in ['9000']: - self.write(f":ACQuire:POINts {self.num_points}") - else: - self.write(f":WAVeform:POINts {self.num_points}") - _log.debug("Number of points set to: ", self.num_points) - self.num_points = num_points + self.num_points = num_points ## Capture and read functions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## @@ -437,7 +531,7 @@ def set_channels_for_capture(self, channels=None): the channels that will be captured, example ``[1, 3]`` """ # If no channels specified, find the channels currently active and acquire from those - if np.any(channels in [[], ['active'], 'active']) or self._capture_active: + if np.any(channels in [[], ['active'], 'active']) or (self._capture_active and channels is None): self._capture_channels = self.active_channels # Store that active channels are being used self._capture_active = True @@ -445,7 +539,7 @@ def set_channels_for_capture(self, channels=None): self._capture_channels = channels self._capture_active = False # Build list of sources - self._sources = [f"CHANnel{ch}" for ch in self._capture_channels] + self._sources = [f"CHAN{ch}" for ch in self._capture_channels] if self.verbose_acquistion: print(f"Acquire from channels: {self._capture_channels}") return self._capture_channels @@ -490,13 +584,14 @@ def capture_and_read(self, set_running=True): # When acquisition is complete, the instrument is stopped. self.write(':DIGitize ' + ", ".join(self._sources)) ## Read from the scope - if self.wav_format[:3] in ['WOR', 'BYT']: - self._read_binary(datatype=_datatypes[self.wav_format]) - elif self.wav_format[:3] == 'ASC': + wav_format = self.wav_format[:3] + if wav_format in ['WOR', 'BYT']: + self._read_binary(datatype=_datatypes[wav_format]) + elif wav_format[:3] == 'ASC': self._read_ascii() else: raise ValueError(f"Could not capture and read data, waveform format " - f"'{self.wav_format}' is unknown.\n") + f"'{wav_format}' is unknown.\n") ## Print to log to_log = f"Elapsed time capture and read: {(time.time()-start_time)*1e3:.1f} ms" if self.verbose_acquistion: @@ -536,7 +631,7 @@ def _read_binary(self, datatype='standard'): # Loop through all the sources for source in self._sources: # Select the channel for which the succeeding WAVeform commands applies to - self.write(':WAVeform:SOURce ' + source) + self.write(f":WAVeform:SOURce {source}") try: # obtain comma separated metadata values for processing of raw data for this source self._metadata.append(self.query(':WAVeform:PREamble?')) @@ -579,7 +674,7 @@ def _read_ascii(self): # Loop through all the sources for source in self._sources: # Select the channel for which the succeeding WAVeform commands applies to - self.write(':WAVeform:SOURce ' + source) + self.write(f":WAVeform:SOURce {source}") # Read out data for this source self._raw.append(self.query(':WAVeform:DATA?', action="obtain the waveform")) # Get the preamble (used for calculating time axis, which is the same From f56648c72273df6288bb19fc6e250bc82d1241dc Mon Sep 17 00:00:00 2001 From: Andreas Svela Date: Sun, 20 Dec 2020 02:40:05 +0100 Subject: [PATCH 07/52] (docs) clearing up new Oscilloscope properties documentation --- README.rst | 48 ++++++++++++++---------- docs/contents/osc-class.rst | 26 +++++++++++-- keyoscacquire/oscacq.py | 75 ++++++++++++++++++------------------- 3 files changed, 88 insertions(+), 61 deletions(-) diff --git a/README.rst b/README.rst index 99a610f..f441ab7 100644 --- a/README.rst +++ b/README.rst @@ -28,7 +28,35 @@ Available at `keyoscacquire.rtfd.io >> import keyoscacquire.oscacq as koa + >>> scope = koa.Oscilloscope(address='USB0::1234::1234::MY1234567::INSTR') + >>> scope.acq_type = 'AVER8' + >>> print(scope.num_points) + 7680 + >>> time, y, channel_numbers = scope.get_trace(channels=[2, 1, 4]) + Acquire from channels: [1, 2, 4] + Start acquisition.. + Points captured per channel: 7,680 + >>> print(channel_numbers) + [1, 2, 4] + >>> scope.save_trace(showplot=True) + Saving trace to: data.csv + >>> scope.close() + +where ``time`` is a vertical numpy (2D) array of time values and ``y`` is a numpy +array which columns contain the data from the active channels listed in +``channel_numbers``. The trace saved to ``data.csv`` contains metadata such as +a timestamp, acquisition type, the channels used etc. Command line use ---------------- @@ -68,24 +96,6 @@ The package installs the following command line programmes in the Python path See more under :ref:`cli-programmes-short`. - -Python console/API ------------------- - -In the Python console:: - - >>> import keyoscacquire.oscacq as koa - >>> osc = koa.Oscilloscope(address='USB0::1234::1234::MY1234567::INSTR') - >>> time, y, channel_numbers = osc.set_options_get_trace() - >>> osc.close() - -where ``time`` is a vertical numpy (2D) array of time values and ``y`` is a numpy -array which columns contain the data from the active channels listed in -``channel_numbers``. - -Explore the Reference section (particularly :ref:`osc-class`) to get more -information about the API. - .. contribute-marker Contribute diff --git a/docs/contents/osc-class.rst b/docs/contents/osc-class.rst index 981b44d..d8a6fee 100644 --- a/docs/contents/osc-class.rst +++ b/docs/contents/osc-class.rst @@ -38,15 +38,33 @@ Oscilloscope state control .. automethod:: Oscilloscope.is_running .. autoproperty:: Oscilloscope.active_channels -Acquisition and transfer options --------------------------------- +Acquisition and transfer properties +----------------------------------- + +These are properties, meaning that they can be used like this:: + + with Oscilloscope() as scope: + scope.acq_type = 'AVER8' + print(f"Number of points that will be captured {scope.num_points}") + scope.num_points = 1500 + print(f"Changed the number of points to be captured to {scope.num_points}") + +.. autoproperty:: Oscilloscope.acq_type +.. autoproperty:: Oscilloscope.num_averages +.. autoproperty:: Oscilloscope.p_mode +.. autoproperty:: Oscilloscope.num_points +.. autoproperty:: Oscilloscope.wav_format + + +Multiple acquisition and transfer options setting functions +----------------------------------------------------------- .. automethod:: Oscilloscope.set_channels_for_capture .. automethod:: Oscilloscope.set_acquiring_options .. automethod:: Oscilloscope.set_waveform_export_options - --------------- +Other +----- .. automethod:: Oscilloscope.capture_and_read .. automethod:: Oscilloscope.generate_file_header diff --git a/keyoscacquire/oscacq.py b/keyoscacquire/oscacq.py index ec21ab2..6519946 100644 --- a/keyoscacquire/oscacq.py +++ b/keyoscacquire/oscacq.py @@ -71,13 +71,13 @@ class Oscilloscope: verbose_acquistion : bool If ``True``: prints that the capturing starts and the number of points captured - fname : str, default :data:`~keyoscacquire.config._filename` + fname : str, default :data:`keyoscacquire.config._filename` The filename to which the trace will be saved with :meth:`save_trace()` - ext : str, default :data:`~keyoscacquire.config._filetype` + ext : str, default :data:`keyoscacquire.config._filetype` The extension for saving traces, must include the period, e.g. ``.csv`` - savepng : bool, default :data:`~keyoscacquire.config._export_png` + savepng : bool, default :data:`keyoscacquire.config._export_png` If ``True``: will save a png of the plot when :meth:`save_trace()` - showplot : bool, default data:`~keyoscacquire.config._show_plot` + showplot : bool, default :data:`keyoscacquire.config._show_plot` If ``True``: will show a matplotlib plot window when :meth:`save_trace()` _inst : :class:`pyvisa.resources.Resource` The oscilloscope PyVISA resource @@ -277,27 +277,26 @@ def active_channels(self, channels: list): def acq_type(self): """Acquisition mode of the oscilloscope - Can be either - - * ``'NORMal'`` — sets the oscilloscope in the normal mode. - * ``'AVERage'`` or ``'AVER'`` — sets the oscilloscope in the averaging mode. - The number of averages can be set with :property:`num_averages`, or - will be used as :property:`num_averages` if supplied. - can be in the range 2 to 65,536 - * ``'HRESolution'`` — sets the oscilloscope in the high-resolution mode - (also known as smoothing). This mode is used to reduce noise at slower - sweep speeds where the digitizer samples faster than needed to fill memory for the displayed time range. - - For example, if the digitizer samples at 200 MSa/s, but the effective sample rate is 1 MSa/s - (because of a slower sweep speed), only 1 out of every 200 samples needs to be stored. - Instead of storing one sample (and throwing others away), the 200 samples are averaged - together to provide the value for one display point. The slower the sweep speed, the greater - the number of samples that are averaged together for each display point. + Choose between + + * ``'NORMal'`` — sets the oscilloscope in the normal mode. + * ``'AVERage'`` or ``'AVER'`` — sets the oscilloscope in the averaging mode. + The number of averages can be set with :attr:`num_averages`, or + will be used as :attr:`num_averages` if supplied. + can be in the range 2 to 65,536 + * ``'HRESolution'`` — sets the oscilloscope in the high-resolution mode + (also known as smoothing). This mode is used to reduce noise at slower + sweep speeds where the digitizer samples faster than needed to fill memory for the displayed time range. + For example, if the digitizer samples at 200 MSa/s, but the effective sample rate is 1 MSa/s + (because of a slower sweep speed), only 1 out of every 200 samples needs to be stored. + Instead of storing one sample (and throwing others away), the 200 samples are averaged + together to provide the value for one display point. The slower the sweep speed, the greater + the number of samples that are averaged together for each display point. :getter: Returns the current mode (will not return ```` for ``AVER``) :setter: Sets the mode, for example ``AVER8``, if :attr:`verbose` will print the type and the number of averages number - :type: ``{'NORMal', 'AVERage', 'AVER', 'HRES'} + :type: ``{'NORMal', 'AVERage', 'AVER', 'HRES'}`` Raises ------ @@ -330,7 +329,7 @@ def acq_type(self, type: str): @property def num_averages(self): """The number of averages taken if the scope is in the ``'AVERage'`` - :property:`acq_type` + :attr:`acq_type` :getter: Returns the current number of averages :setter: Set the number, will print the number if :attr:`verbose` @@ -360,7 +359,7 @@ def p_mode(self): 1e6 points. Use ``'MAXimum'`` for sources that are not analogue or digital. :getter: Returns the current mode - :setter: Set the mode, will check if compatible with the :property:`acq_type` + :setter: Set the mode, will check if compatible with the :attr:`acq_type` :type: ``{'NORMal', 'RAW', 'MAXimum'}`` """ return self.query(":WAVeform:POINts:MODE?") @@ -376,9 +375,9 @@ def p_mode(self, p_mode: str): @property def num_points(self): - """The number of points to be acquired for each channel. Use 0 to let - get the maximum number given the :property:`p_mode`, otherwise - override with a lower number than maximum for the :property:`p_mode` + """The number of points to be acquired for each channel. Use 0 to + get the maximum number given the :attr:`p_mode`, or override with a + lower number than maximum for the given :attr:`p_mode` .. warning:: If the exact number of points is crucial, always check the number of points with the getter after performing the setter. @@ -388,7 +387,7 @@ def num_points(self): always stops the scope when getting a trace, the getter will also do this to get the actual number of points that will be transferred (otherwise the returned number will be capped by the - :property:`p_mode` ``NORMal`` (which can be transferred without + :attr:`p_mode` ``NORMal`` (which can be transferred without stopping the scope)). :getter: Returns the number of points that will be acquired (stopping @@ -455,13 +454,13 @@ def set_acquiring_options(self, wav_format=None, acq_type=None, Parameters ---------- - wav_format : {``'WORD'``, ``'BYTE'``, ``'ASCii'``}, default :data:`~keyoscacquire.config._waveform_format` + wav_format : {``'WORD'``, ``'BYTE'``, ``'ASCii'``}, default :data:`keyoscacquire.config._waveform_format` Select the format of the communication of waveform from the oscilloscope, see :attr:`wav_format` - acq_type : {``'HRESolution'``, ``'NORMal'``, ``'AVERage'``, ``'AVER'``}, default :data:`~keyoscacquire.config._acq_type` + acq_type : {``'HRESolution'``, ``'NORMal'``, ``'AVERage'``, ``'AVER'``}, default :data:`keyoscacquire.config._acq_type` Acquisition mode of the oscilloscope. will be used as num_averages if supplied, see :attr:`acq_type` - num_averages : int, 2 to 65536, default :data:`~keyoscacquire.config._num_avg` + num_averages : int, 2 to 65536, default :data:`keyoscacquire.config._num_avg` Applies only to the ``'AVERage'`` mode: The number of averages applied p_mode : {``'NORMal'``, ``'RAW'``, ``'MAXimum'``}, default ``'RAW'`` ``'NORMal'`` is limited to 62,500 points, whereas ``'RAW'`` gives up to 1e6 points. @@ -555,7 +554,7 @@ def capture_and_read(self, set_running=True): raw : :class:`~numpy.ndarray` An ndarray of ints that can be converted to voltage values using the preamble. metadata - depends on the wav_format + depends on the :attr:`wav_format` Parameters ---------- @@ -565,7 +564,7 @@ def capture_and_read(self, set_running=True): Raises ------ ValueError - If :attr:`wav_format` is not {'BYTE', 'WORD', 'ASCii'} + If :attr:`wav_format` is not one of ``{'BYTE', 'WORD', 'ASCii'}`` See also -------- @@ -695,9 +694,9 @@ def get_trace(self, channels=None, verbose_acquistion=None): Parameters ---------- channels : list of ints or ``'active'``, default :data:`~keyoscacquire.config._ch_nums` - list of the channel numbers to be acquired, example ``[1, 3]``. - Use ``'active'`` or ``[]`` to capture all the currently active - channels on the oscilloscope. + Optionally change the list of the channel numbers to be acquired, + example ``[1, 3]``. Use ``'active'`` or ``[]`` to capture all the + currently active channels on the oscilloscope. verbose_acquistion : bool or ``None``, default ``None`` Optionally change :attr:`verbose_acquistion` @@ -798,8 +797,8 @@ def set_options_get_trace_save(self, fname=None, ext=None, Applies only to the ``'AVERage'`` mode: The number of averages applied p_mode : {``'NORMal'``, ``'RAW'``, ``'MAXimum'``}, default ``'RAW'`` ``'NORMal'`` is limited to 62,500 points, whereas ``'RAW'`` gives up - to 1e6 points. Use ``'MAXimum'`` for sources that are not analogue - or digital + to 1e6 points. Use ``'MAXimum'`` for sources that are not analogue + or digital num_points : int, default 0 Use 0 to let :attr:`p_mode` control the number of points, otherwise override with a lower number than maximum for the :attr:`p_mode` @@ -879,7 +878,7 @@ def generate_file_header(self, channels=None, additional_line=None, timestamp=Tr def save_trace(self, fname=None, ext=None, additional_header_info=None, savepng=None, showplot=None, nowarn=False): - """Save the most recent trace to fname+ext. Will check if the filename + """Save the most recent trace to ``fname+ext``. Will check if the filename exists, and let the user append to the fname if that is the case. Parameters From 2eff0964a2e6cfd2855bff0ef536932fe7d98bb0 Mon Sep 17 00:00:00 2001 From: Andreas Svela Date: Sun, 20 Dec 2020 03:02:23 +0100 Subject: [PATCH 08/52] (docs) minor changes --- docs/changelog.rst | 22 ++++++++++++++-------- docs/contents/overview.rst | 2 +- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 99a7f46..a768426 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -30,8 +30,9 @@ v4.0.0 (2020-12) can now be used to change attributes of the ``Oscilloscope`` instance. - Bugfixes and docfixes for the number of points to be transferred from the - instrument (``num_points`` argument). Zero will set the to the - maximum number of points available. + instrument (previously ``num_points`` argument, now a property). Zero will + set the to the maximum number of points available, and the number of + points can be queried. - New ``keyoscacquire.traceio.load_trace()`` function for loading saved a trace from disk to pandas dataframe or numpy array @@ -64,12 +65,17 @@ v4.0.0 (2020-12) ``Oscilloscope.showplot`` can be set to control its behaviour) * ``Oscilloscope.plot_trace()`` - - *New properties*: - - * ``Oscilloscope.active_channels`` can now be used to set and get the - currently active channels - * ``Oscilloscope.timeout`` this was previously just an attribute with no - set option + - *New properties*: New properties getters querying the instrument for the + current state and setters to change the state + + * ``Oscilloscope.active_channels`` + * ``Oscilloscope.acq_type`` + * ``Oscilloscope.num_averages`` + * ``Oscilloscope.p_mode`` + * ``Oscilloscope.num_points`` + * ``Oscilloscope.wav_format`` + * ``Oscilloscope.timeout`` (this affects the pyvisa resource, not the scope + itself) - *No compatibility*: Name changes diff --git a/docs/contents/overview.rst b/docs/contents/overview.rst index 6f7367a..fb9c788 100644 --- a/docs/contents/overview.rst +++ b/docs/contents/overview.rst @@ -17,7 +17,7 @@ Quick reference =============== .. include:: ../../README.rst - :start-after: command-line-use-marker + :start-after: API-use-marker :end-before: contribute-marker From 8c6cbf2b11f2dff3eceb0658c3a87090c1cd13a3 Mon Sep 17 00:00:00 2001 From: asvela Date: Mon, 21 Dec 2020 01:45:07 +0100 Subject: [PATCH 09/52] mainly sorting out the cli programmes and example.py --- docs/changelog.rst | 21 ++- docs/contents/osc-class.rst | 1 + docs/known-issues.rst | 13 +- keyoscacquire/config.py | 18 ++- keyoscacquire/installed_cli_programmes.py | 38 ++--- keyoscacquire/oscacq.py | 167 ++++++++++++---------- keyoscacquire/programmes.py | 70 +++++---- keyoscacquire/scripts/example.py | 44 ++++-- keyoscacquire/traceio.py | 9 +- 9 files changed, 225 insertions(+), 156 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index a768426..6f7b3c3 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -29,6 +29,11 @@ v4.0.0 (2020-12) for most functions the arguments stay the same as before. The arguments can now be used to change attributes of the ``Oscilloscope`` instance. + - ``Oscilloscope.__init__`` and other functions will no longer use default + settings in ``keyoscacquire.config`` that changes the settings of the + *Oscilloscope*, like active channels and acquisition type, but only set + default connection and transfer settings + - Bugfixes and docfixes for the number of points to be transferred from the instrument (previously ``num_points`` argument, now a property). Zero will set the to the maximum number of points available, and the number of @@ -43,12 +48,13 @@ v4.0.0 (2020-12) - ``Oscilloscope.query()`` will now try to read the error from the instrument if pyvisa fails - - Importing ``keyoscacquire.programmes`` in module ``init.py`` to make it accessible + - Importing ``keyoscacquire.programmes`` in module ``init.py`` to make it + accessible after importing the module - Changes in ``list_visa_devices`` and cli programme: now displaying different errors more clearly; cli programme now has ``-n`` flag that can be set to not ask for instrument IDNs; and the cli programme will display the instrument's - firmware rather than Keysight model series. + serial rather than Keysight model series. - Indicating functions for internal use only and read only attributes with prefix ``_``, see name changes below @@ -90,7 +96,7 @@ v4.0.0 (2020-12) * ``Oscilloscope.inst`` -> ``Oscilloscope._inst`` * ``Oscilloscope.id`` -> ``Oscilloscope._id`` * ``Oscilloscope.address`` -> ``Oscilloscope._address`` - * ``Oscilloscope._model`` -> ``Oscilloscope._model`` + * ``Oscilloscope.model`` -> ``Oscilloscope._model`` * ``Oscilloscope.model_series`` -> ``Oscilloscope._model_series`` - *No compatibility*: Moved functions @@ -98,7 +104,7 @@ v4.0.0 (2020-12) * ``interpret_visa_id()`` from ``oscacq`` to ``auxiliary`` * ``check_file()`` from ``oscacq`` to ``auxiliary`` - - *No compatibility*: Several functions no longer take ``sources`` and + - *No compatibility*: Some functions no longer take ``sources`` and ``sourcesstring`` as arguments, rather ``Oscilloscope._sources`` must be set by ``Oscilloscope.set_channels_for_capture()`` and ``sourcesstring`` is not in use anymore @@ -109,10 +115,13 @@ v4.0.0 (2020-12) - *No compatibility*: Misc - * ``Oscilloscope.get_trace()`` now also returns - also ``Oscilloscope.num_channels`` + * ``Oscilloscope.get_trace()`` now also returns ``Oscilloscope.num_channels`` * ``Oscilloscope.get_active_channels()`` is now a property ``active_channels`` and returns a list of ints, not chars + * ``keyoscacquire.config`` does not have the ``_acq_type``, ``_num_avg``, + and ``_ch_nums`` static variables anymore as these will not be used + * ``keyoscacquire.config`` has two new static variables, ``_num_points`` + and ``_p_mode`` diff --git a/docs/contents/osc-class.rst b/docs/contents/osc-class.rst index d8a6fee..c0d77d6 100644 --- a/docs/contents/osc-class.rst +++ b/docs/contents/osc-class.rst @@ -68,6 +68,7 @@ Other .. automethod:: Oscilloscope.capture_and_read .. automethod:: Oscilloscope.generate_file_header +.. automethod:: Oscilloscope.print_acq_settings Auxiliary to the class diff --git a/docs/known-issues.rst b/docs/known-issues.rst index 21c0232..72b196d 100644 --- a/docs/known-issues.rst +++ b/docs/known-issues.rst @@ -12,10 +12,11 @@ Known issues and suggested improvements * Improvements: - - (feature) capture MATH waveform - - (feature) measurements provided by the scope - - (feature) Build functionality for pickling measurement to disk and then - post-processing for speed-up in consecutive measurements + - (feature) include capture of MATH waveform + - (feature) expand API to include + * waveform measurements + * trigger settings + * time and voltage axes settings + - (feature) pickling trace to disk for later post-processing to give speed-up + in consecutive measurements - (instrument support) expand support for Infiniium oscilloscopes - - (docs) Write tutorial page for documentation - - (housekeeping) PEP8 compliance and code audit diff --git a/keyoscacquire/config.py b/keyoscacquire/config.py index 401cde9..75f8bc5 100644 --- a/keyoscacquire/config.py +++ b/keyoscacquire/config.py @@ -3,20 +3,24 @@ #: VISA address of instrument _visa_address = 'USB0::1234::1234::MY1234567::INSTR' -#: waveform format transferred from the oscilloscope to the computer +#: Waveform format transferred from the oscilloscope to the computer #: WORD formatted data is transferred as 16-bit uint. #: BYTE formatted data is transferred as 8-bit uint. #: ASCii formatted data converts the internal integer data values to real Y-axis values. Values are transferred as ASCii digits in floating point notation, separated by commas. _waveform_format = 'WORD' -#: list of chars, e.g. ['1', '3'], or 'active' to capture all currently displayed channels -_ch_nums = 'active' +#: The acqusition type #: {HRESolution, NORMal, AVER} where is the number of averages in range [2, 65536] _acq_type = "HRESolution" -#: default number of averages used if only AVER is given as acquisition type -_num_avg = None -#: default base filename of all traces and pngs exported, a number is appended to the base +#: Points mode of the oscilloscope: ``'NORMal'`` is limited to 62,500 points, +#: whereas ``'RAW'`` gives up to 1e6 points. +#: {RAW, MAX, NORMal} +_p_mode = 'RAW' +#: Number of points to transfer to the computer +#: zero gives maximum +_num_points = 0 +#: Default base filename of all traces and pngs exported, a number is appended to the base _filename = "data" -#: delimiter used between :attr:`_filename` and filenumber (before _filetype) +#: Delimiter used between :attr:`_filename` and filenumber (before :attr:`_filetype`) _file_delimiter = " n" #: filetype of exported data, can also be txt/dat etc. _filetype = ".csv" diff --git a/keyoscacquire/installed_cli_programmes.py b/keyoscacquire/installed_cli_programmes.py index 5f40194..a230884 100644 --- a/keyoscacquire/installed_cli_programmes.py +++ b/keyoscacquire/installed_cli_programmes.py @@ -31,10 +31,10 @@ acq_help = f"The acquire type: {{HRESolution, NORMal, AVER}} where is the number of averages in range [2, 65536]. Defaults to '{config._acq_type}'." wav_help = f"The waveform format: {{BYTE, WORD, ASCii}}. \nDefaults to '{config._waveform_format}'." file_help = f"The filename base, (without extension, '{config._filetype}' is added). Defaults to '{config._filename}'." -visa_help = f"Visa address of instrument. To find the visa addresses of the instruments connected to the computer run 'list_visa_devices' in the command line. Defaults to '{config._visa_address}." +visa_help = f"Visa address of instrument. To find the visa addresses of the instruments connected to the computer run 'list_visa_devices' in the command line. Defaults to '{config._visa_address}'." timeout_help = f"Milliseconds before timeout on the channel to the instrument. Defaults to {config._timeout}." -channels_help = f"List of the channel numbers to be acquired, for example '1 3' (without ') or 'active' (without ') to capture all the currently active channels on the oscilloscope. Defaults to {config._ch_nums}'." -points_help = f"Use 0 to get the maximum number of points, or set a smaller number to speed up the acquisition and transfer. Defaults to 0." +channels_help = f"List of the channel numbers to be acquired, for example '1 3' (without ') or 'active' (without ') to capture all the currently active channels on the oscilloscope. Defaults to the currently active channels." +points_help = f"Use 0 to get the maximum number of points, or set a specific number (the scope might change it slightly). Defaults to '{config._num_points}." delim_help = f"Delimiter used between filename and filenumber (before filetype). Defaults to '{config._file_delimiter}'." def connect_each_time_cli(): @@ -44,12 +44,12 @@ def connect_each_time_cli(): connection_gr = parser.add_argument_group('Connection settings') connection_gr.add_argument('-v', '--visa_address', nargs='?', help=visa_help, default=config._visa_address) connection_gr.add_argument('-t', '--timeout', nargs='?', help=timeout_help, default=config._timeout, type=int) - acquire_gr = parser.add_argument_group('Acquiring settings') - acquire_gr.add_argument('-c', '--channels', nargs='*', type=int, help=channels_help, default=config._ch_nums) - acquire_gr.add_argument('-a', '--acq_type', nargs='?', help=acq_help, default=config._acq_type) + acquire_gr = parser.add_argument_group('Acquisition settings') + acquire_gr.add_argument('-c', '--channels', nargs='*', type=int, help=channels_help, default=None) + acquire_gr.add_argument('-a', '--acq_type', nargs='?', help=acq_help, default=None) trans_gr = parser.add_argument_group('Transfer and storage settings') trans_gr.add_argument('-w', '--wav_format', nargs='?', help=wav_help, default=config._waveform_format) - trans_gr.add_argument('-p', '--num_points', nargs='?', help=points_help, default=0, type=int) + trans_gr.add_argument('-p', '--num_points', nargs='?', help=points_help, default=config._num_points, type=int) trans_gr.add_argument('-f', '--filename', nargs='?', help=file_help, default=config._filename) trans_gr.add_argument('--file_delimiter', nargs='?', help=delim_help, default=config._file_delimiter) args = parser.parse_args() @@ -65,12 +65,12 @@ def single_connection_cli(): connection_gr = parser.add_argument_group('Connection settings') connection_gr.add_argument('-v', '--visa_address', nargs='?', help=visa_help, default=config._visa_address) connection_gr.add_argument('-t', '--timeout', nargs='?', help=timeout_help, default=config._timeout, type=int) - acquire_gr = parser.add_argument_group('Acquiring settings') - acquire_gr.add_argument('-c', '--channels', nargs='*', type=int, help=channels_help, default=config._ch_nums) - acquire_gr.add_argument('-a', '--acq_type', nargs='?', help=acq_help, default=config._acq_type) + acquire_gr = parser.add_argument_group('Acquisition settings') + acquire_gr.add_argument('-c', '--channels', nargs='*', type=int, help=channels_help, default=None) + acquire_gr.add_argument('-a', '--acq_type', nargs='?', help=acq_help, default=None) trans_gr = parser.add_argument_group('Transfer and storage settings') trans_gr.add_argument('-w', '--wav_format', nargs='?', help=wav_help, default=config._waveform_format) - trans_gr.add_argument('-p', '--num_points', nargs='?', help=points_help, default=0, type=int) + trans_gr.add_argument('-p', '--num_points', nargs='?', help=points_help, default=config._num_points, type=int) trans_gr.add_argument('-f', '--filename', nargs='?', help=file_help, default=config._filename) trans_gr.add_argument('--file_delimiter', nargs='?', help=delim_help, default=config._file_delimiter) args = parser.parse_args() @@ -85,12 +85,12 @@ def single_trace_cli(): connection_gr = parser.add_argument_group('Connection settings') connection_gr.add_argument('-v', '--visa_address', nargs='?', help=visa_help, default=config._visa_address) connection_gr.add_argument('-t', '--timeout', nargs='?', help=timeout_help, default=config._timeout, type=int) - acquire_gr = parser.add_argument_group('Acquiring settings') - acquire_gr.add_argument('-c', '--channels', nargs='*', type=int, help=channels_help, default=config._ch_nums) - acquire_gr.add_argument('-a', '--acq_type', nargs='?', help=acq_help, default=config._acq_type) + acquire_gr = parser.add_argument_group('Acquisition settings') + acquire_gr.add_argument('-c', '--channels', nargs='*', type=int, help=channels_help, default=None) + acquire_gr.add_argument('-a', '--acq_type', nargs='?', help=acq_help, default=None) trans_gr = parser.add_argument_group('Transfer and storage settings') trans_gr.add_argument('-w', '--wav_format', nargs='?', help=wav_help, default=config._waveform_format) - trans_gr.add_argument('-p', '--num_points', nargs='?', help=points_help, default=0, type=int) + trans_gr.add_argument('-p', '--num_points', nargs='?', help=points_help, default=config._num_points, type=int) trans_gr.add_argument('-f', '--filename', nargs='?', help=file_help, default=config._filename) args = parser.parse_args() @@ -106,12 +106,12 @@ def num_traces_cli(): connection_gr = parser.add_argument_group('Connection settings') connection_gr.add_argument('-v', '--visa_address', nargs='?', help=visa_help, default=config._visa_address) connection_gr.add_argument('-t', '--timeout', nargs='?', help=timeout_help, default=config._timeout, type=int) - acquire_gr = parser.add_argument_group('Acquiring settings') - acquire_gr.add_argument('-c', '--channels', nargs='*', type=int, help=channels_help, default=config._ch_nums) - acquire_gr.add_argument('-a', '--acq_type', nargs='?', help=acq_help, default=config._acq_type) + acquire_gr = parser.add_argument_group('Acquisition settings') + acquire_gr.add_argument('-c', '--channels', nargs='*', type=int, help=channels_help, default=None) + acquire_gr.add_argument('-a', '--acq_type', nargs='?', help=acq_help, default=None) trans_gr = parser.add_argument_group('Transfer and storage settings') trans_gr.add_argument('-w', '--wav_format', nargs='?', help=wav_help, default=config._waveform_format) - trans_gr.add_argument('-p', '--num_points', nargs='?', help=points_help, default=0, type=int) + trans_gr.add_argument('-p', '--num_points', nargs='?', help=points_help, default=config._num_points, type=int) trans_gr.add_argument('-f', '--filename', nargs='?', help=file_help, default=config._filename) trans_gr.add_argument('--file_delimiter', nargs='?', help=delim_help, default=config._file_delimiter) args = parser.parse_args() diff --git a/keyoscacquire/oscacq.py b/keyoscacquire/oscacq.py index 6519946..0ac08e4 100644 --- a/keyoscacquire/oscacq.py +++ b/keyoscacquire/oscacq.py @@ -41,7 +41,7 @@ class Oscilloscope: """PyVISA communication with the oscilloscope. Init opens a connection to an instrument and chooses default settings - for the connection and acquisition. + for the connection and acquisition as specified in :mod:`keyoscacquire.config`. Leading underscores indicate that an attribute or method is read-only or suggested to be for interal use only. @@ -65,12 +65,12 @@ class Oscilloscope: Attributes ---------- - verbose : bool + verbose : bool, default ``True`` If ``True``: prints when the connection to the device is opened, the acquistion mode, etc - verbose_acquistion : bool - If ``True``: prints that the capturing starts and the number of points - captured + verbose_acquistion : bool, defaulting to ``self.verbose`` + If ``True``: prints that the capturing starts, the channels + acquired from and the number of points captured fname : str, default :data:`keyoscacquire.config._filename` The filename to which the trace will be saved with :meth:`save_trace()` ext : str, default :data:`keyoscacquire.config._filetype` @@ -98,6 +98,8 @@ class Oscilloscope: _capture_channels : list of ints The channels of captured for the most recent trace """ + _capture_active = True + _capture_channels = None _raw = None _metadata = None _time = None @@ -106,12 +108,12 @@ class Oscilloscope: ext = config._filetype savepng = config._export_png showplot = config._show_plot + verbose_acquistion = False def __init__(self, address=config._visa_address, timeout=config._timeout, verbose=True): """See class docstring""" self._address = address self.verbose = verbose - self.verbose_acquistion = verbose # Connect to the scope try: rm = pyvisa.ResourceManager() @@ -119,7 +121,7 @@ def __init__(self, address=config._visa_address, timeout=config._timeout, verbos except pyvisa.Error as err: print(f"\n\nCould not connect to '{address}', see traceback below:\n") raise - self._timeout = timeout + self.timeout = timeout # For TCP/IP socket connections enable the read Termination Character, or reads will timeout if self._inst.resource_name.endswith('SOCKET'): self._inst.read_termination = '\n' @@ -130,27 +132,36 @@ def __init__(self, address=config._visa_address, timeout=config._timeout, verbos self.write(':WAVeform:BYTeorder LSBFirst') # MSBF is default, must be overridden for WORD to work # Get information about the connected device self._id = self.query('*IDN?') - if self.verbose: - print(f"Connected to '{self._id}'") - _, self._model, _, _, self._model_series = auxiliary.interpret_visa_id(self._id) + try: + maker, self._model, self._serial, _, self._model_series = auxiliary.interpret_visa_id(self._id) + if self.verbose: + print(f"Connected to:") + print(f" {maker}") + print(f" {self._model} (serial {self._serial})") + except Exception: + if self.verbose: + print(f"Connected to '{self._id}'") + print("(!) Failed to intepret the VISA IDN string") if not self._model_series in _supported_series: - print("(!) WARNING: This model (%s) is not yet fully supported by keyoscacquire," % self._model) - print(" but might work to some extent. keyoscacquire supports Keysight's") - print(" InfiniiVision X-series oscilloscopes.") - # Populate attributes and set standard settings - if self.verbose: - print("Using settings:") - self.set_acquiring_options(wav_format=config._waveform_format, acq_type=config._acq_type, - num_averages=config._num_avg, p_mode='RAW', num_points=0, - verbose_acquistion=verbose) - print(" ", end="") - self.set_channels_for_capture(channels=config._ch_nums) + print(f"(!) WARNING: This model ({self._model}) is not yet fully supported by keyoscacquire,") + print( " but might work to some extent. keyoscacquire supports Keysight's") + print( " InfiniiVision X-series oscilloscopes.") + # Set standard settings + self.set_acquiring_options(wav_format=config._waveform_format, p_mode=config._p_mode, + num_points=config._num_points) + # Will set channels to the active channels + self.set_channels_for_capture() + self.verbose_acquistion = verbose def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): - self.close() + if exc_type is not None: + # Do not try to set running if an exception ocurred + self.close(set_running=False) + else: + self.close() def write(self, command): """Write a VISA command to the oscilloscope. @@ -181,11 +192,11 @@ def query(self, command, action=""): else: msg = f"query '{command}'" print(f"\nVisaError: {err}\n When trying {msg}.") - print(f" Have you checked that the timeout (currently {self._timeout:,d} ms) is sufficently long?") + print(f" Have you checked that the timeout (currently {self.timeout:,d} ms) is sufficently long?") try: print(f"Latest error from the oscilloscope: '{self.get_error()}'\n") except Exception: - print("Could not retrieve error from the oscilloscope") + print("Could not retrieve error from the oscilloscope\n") raise def close(self, set_running=True): @@ -248,7 +259,7 @@ def timeout(self): @timeout.setter def timeout(self, timeout: int): """See getter""" - self._inst.timeout = val + self._inst.timeout = timeout @property def active_channels(self): @@ -310,8 +321,6 @@ def acq_type(self, type: str): """See getter""" acq_type = type[:4].upper() self.write(f":ACQuire:TYPE {acq_type}") - if self.verbose: - print(f" Acquisition type: {acq_type}") # Handle AVER expressions if acq_type == 'AVER': if len(type) > 4 and not type[4:].lower() == 'age': @@ -321,10 +330,6 @@ def acq_type(self, type: str): ValueError(f"\nValueError: Failed to convert '{type[4:]}' to an integer, " "check that acquisition type is on the form AVER or AVER " f"where is an integer (currently acq. type is '{type}').\n") - else: - num = self.num_averages - if self.verbose: - print(f" # of averages: {num}") @property def num_averages(self): @@ -348,8 +353,14 @@ def num_averages(self, num: int): if not (2 <= num <= 65536): raise ValueError(f"\nThe number of averages {num} is out of range.") self.write(f":ACQuire:COUNt {num}") - if self.verbose and self.acq_type == 'AVER': - print(f" # of averages: {num}") + + def print_acq_settings(self): + """Print the current settings for acquistion from the scope""" + acq_type = self.acq_type + print(f"Acquisition type: {acq_type}") + if acq_type == 'AVER': + print(f"# of averages: {self.num_averages}") + print(f"From channels: {self._capture_channels}") @property def p_mode(self): @@ -372,6 +383,7 @@ def p_mode(self, p_mode: str): _log.info(f":WAVeform:POINts:MODE overridden (from {p_mode}) to " "NORMal due to :ACQuire:TYPE:AVERage.") self.write(f":WAVeform:POINts:MODE {p_mode}") + _log.debug(f"Points mode set to: {p_mode}") @property def num_points(self): @@ -395,6 +407,11 @@ def num_points(self): :setter: Set the number, but beware that the scope might change the number depending on memory depth, time axis settings, etc. :type: int + + Raises + ------ + ValueError + If a negative integer or other datatypes are given. """ # Must stop the scope to be able to read the actual number of points # that will be transferred in the RAW or MAX mode @@ -415,6 +432,8 @@ def num_points(self, num_points: int): if self._model_series in ['9000']: self.write(f":ACQuire:POINts {num_points}") else: + if num_points > 7680: + self.p_mode = 'RAW' # Must stop the scope to set the number of points to avoid # getting an error in the scopes' log (however, it seems to # be working regardless, only the get_error() will return -222) @@ -422,6 +441,9 @@ def num_points(self, num_points: int): self.write(f":WAVeform:POINts {num_points}") self.run() _log.debug(f"Number of points set to: {num_points}") + else: + ValueError(f"Cannot set points mode ('{num_points}' is not a " + "non-negative integer)") @property def wav_format(self): @@ -446,6 +468,7 @@ def wav_format(self): def wav_format(self, wav_format: str): """See getter""" self.write(f":WAVeform:FORMat {wav_format}") + _log.debug(f"Waveform format set to: {wav_format}") def set_acquiring_options(self, wav_format=None, acq_type=None, num_averages=None, p_mode=None, num_points=None, @@ -460,14 +483,14 @@ def set_acquiring_options(self, wav_format=None, acq_type=None, acq_type : {``'HRESolution'``, ``'NORMal'``, ``'AVERage'``, ``'AVER'``}, default :data:`keyoscacquire.config._acq_type` Acquisition mode of the oscilloscope. will be used as num_averages if supplied, see :attr:`acq_type` - num_averages : int, 2 to 65536, default :data:`keyoscacquire.config._num_avg` + num_averages : int, 2 to 65536 Applies only to the ``'AVERage'`` mode: The number of averages applied - p_mode : {``'NORMal'``, ``'RAW'``, ``'MAXimum'``}, default ``'RAW'`` + p_mode : {``'NORMal'``, ``'RAW'``, ``'MAXimum'``}, default :data:`keyoscacquire.config._p_mode` ``'NORMal'`` is limited to 62,500 points, whereas ``'RAW'`` gives up to 1e6 points. Use ``'MAXimum'`` for sources that are not analogue or digital - num_points : int, default 0 - Use 0 to get the maximum amount of points, otherwise - override with a lower number than maximum for the :attr:`p_mode` + num_points : int, default :data:`keyoscacquire.config._num_points` + Use 0 to get the maximum amount of points for the current :attr:`p_mode`, + otherwise override with a lower number than maximum for the :attr:`p_mode` verbose_acquistion : bool or ``None``, default ``None`` Temporarily control attribute which decides whether to print information while acquiring: bool sets it to the bool value, @@ -497,9 +520,9 @@ def set_waveform_export_options(self, wav_format=None, num_points=None, p_mode=N wav_format : {``'WORD'``, ``'BYTE'``, ``'ASCii'``}, default :data:`~keyoscacquire.config._waveform_format` Select the format of the communication of waveform from the oscilloscope, see :attr:`wav_format` - p_mode : {``'NORMal'``, ``'RAW'``}, default ``'RAW'`` + p_mode : {``'NORMal'``, ``'RAW'``}, default :data:`keyoscacquire.config._p_mode` ``'NORMal'`` is limited to 62,500 points, whereas ``'RAW'`` gives up to 1e6 points. - num_points : int, default 0 + num_points : int, default :data:`keyoscacquire.config._num_points` Use 0 to get the maximum amount of points, otherwise override with a lower number than maximum for the :attr:`p_mode` """ @@ -539,8 +562,6 @@ def set_channels_for_capture(self, channels=None): self._capture_active = False # Build list of sources self._sources = [f"CHAN{ch}" for ch in self._capture_channels] - if self.verbose_acquistion: - print(f"Acquire from channels: {self._capture_channels}") return self._capture_channels def capture_and_read(self, set_running=True): @@ -570,9 +591,10 @@ def capture_and_read(self, set_running=True): -------- :func:`process_data` """ - ## Capture data + wav_format = self.wav_format if self.verbose_acquistion: - print("Start acquisition..") + self.print_acq_settings() + print(f"Acquiring (format '{wav_format}').. ", end="", flush=True) start_time = time.time() # time the acquiring process # If the instrument is not running, we presumably want the data # on the screen and hence don't want to use DIGitize as digitize @@ -583,20 +605,18 @@ def capture_and_read(self, set_running=True): # When acquisition is complete, the instrument is stopped. self.write(':DIGitize ' + ", ".join(self._sources)) ## Read from the scope - wav_format = self.wav_format[:3] + wav_format = wav_format[:3] if wav_format in ['WOR', 'BYT']: self._read_binary(datatype=_datatypes[wav_format]) elif wav_format[:3] == 'ASC': self._read_ascii() else: - raise ValueError(f"Could not capture and read data, waveform format " + raise ValueError(f"\nCould not capture and read data, waveform format " f"'{wav_format}' is unknown.\n") - ## Print to log - to_log = f"Elapsed time capture and read: {(time.time()-start_time)*1e3:.1f} ms" if self.verbose_acquistion: - _log.info(to_log) - else: - _log.debug(to_log) + print("done") + to_log = f"Elapsed time capture and read: {(time.time()-start_time)*1e3:.1f} ms" + _log.debug(to_log) if set_running: self.run() @@ -631,9 +651,9 @@ def _read_binary(self, datatype='standard'): for source in self._sources: # Select the channel for which the succeeding WAVeform commands applies to self.write(f":WAVeform:SOURce {source}") + # obtain comma separated metadata values for processing of raw data for this source + self._metadata.append(self.query(':WAVeform:PREamble?')) try: - # obtain comma separated metadata values for processing of raw data for this source - self._metadata.append(self.query(':WAVeform:PREamble?')) # obtain the data # read out data for this source self._raw.append(self._inst.query_binary_values(':WAVeform:DATA?', @@ -641,7 +661,7 @@ def _read_binary(self, datatype='standard'): container=np.array)) except pyvisa.Error as err: print(f"\n\nVisaError: {err}\n When trying to obtain the waveform.") - print(f" Have you checked that the timeout (currently {self._timeout:,d} ms) is sufficently long?") + print(f" Have you checked that the timeout (currently {self.timeout:,d} ms) is sufficently long?") try: print(f"Latest error from the oscilloscope: '{self.get_error()}'\n") except Exception: @@ -693,7 +713,7 @@ def get_trace(self, channels=None, verbose_acquistion=None): Parameters ---------- - channels : list of ints or ``'active'``, default :data:`~keyoscacquire.config._ch_nums` + channels : list of ints or ``'active'``, uses oscilloscope setting by default Optionally change the list of the channel numbers to be acquired, example ``[1, 3]``. Use ``'active'`` or ``[]`` to capture all the currently active channels on the oscilloscope. @@ -710,10 +730,10 @@ def get_trace(self, channels=None, verbose_acquistion=None): _capture_channels : list of ints list of the channels obtaied from, example ``[1, 3]`` """ - self.set_channels_for_capture(channels=channels) # Possibility to override verbose_acquistion if verbose_acquistion is not None: self.verbose_acquistion = verbose_acquistion + self.set_channels_for_capture(channels=channels) # Capture, read and process data self.capture_and_read() self._time, self._values = process_data(self._raw, self._metadata, self.wav_format, @@ -726,7 +746,7 @@ def set_options_get_trace(self, channels=None, wav_format=None, acq_type=None, Parameters ---------- - channels : list of ints or ``'active'``, default :data:`~keyoscacquire.config._ch_nums` + channels : list of ints or ``'active'``, uses active channels by default list of the channel numbers to be acquired, example ``[1, 3]``. Use ``'active'`` or ``[]`` to capture all the currently active channels on the oscilloscope. @@ -736,14 +756,14 @@ def set_options_get_trace(self, channels=None, wav_format=None, acq_type=None, acq_type : {``'HRESolution'``, ``'NORMal'``, ``'AVERage'``, ``'AVER'``}, default :data:`~keyoscacquire.config._acq_type` Acquisition mode of the oscilloscope. will be used as num_averages if supplied, see :attr:`acq_type` - num_averages : int, 2 to 65536, default :data:`~keyoscacquire.config._num_avg` + num_averages : int, 2 to 65536, uses oscilloscope setting by default Applies only to the ``'AVERage'`` mode: The number of averages applied - p_mode : {``'NORMal'``, ``'RAW'``, ``'MAXimum'``}, default ``'RAW'`` + p_mode : {``'NORMal'``, ``'RAW'``, ``'MAXimum'``}, default :data:`keyoscacquire.config._p_mode` ``'NORMal'`` is limited to 62,500 points, whereas ``'RAW'`` gives up to 1e6 points. Use ``'MAXimum'`` for sources that are not analogue or digital - num_points : int, default 0 - Use 0 to let :attr:`p_mode` control the number of points, otherwise - override with a lower number than maximum for the :attr:`p_mode` + num_points : int, default :data:`keyoscacquire.config._num_points` + Use 0 to get the maximum amount of points for the current :attr:`p_mode`, + otherwise override with a lower number than maximum for the :attr:`p_mode` Returns ------- @@ -755,11 +775,10 @@ def set_options_get_trace(self, channels=None, wav_format=None, acq_type=None, _capture_channels : list of ints list of the channels obtaied from, example ``[1, 3]`` """ - ## Connect to instrument and specify acquiring settings + self.set_channels_for_capture(channels=channels) self.set_acquiring_options(wav_format=wav_format, acq_type=acq_type, num_averages=num_averages, p_mode=p_mode, num_points=num_points) - ## Capture, read and process data self.get_trace() return self._time, self._values, self._capture_channels @@ -783,7 +802,7 @@ def set_options_get_trace_save(self, fname=None, ext=None, Filename of trace ext : str, default :data:`~keyoscacquire.config._filetype` Choose the filetype of the saved trace - channels : list of ints or ``'active'``, default :data:`~keyoscacquire.config._ch_nums` + channels : list of ints or ``'active'``, uses active channels by default list of the channel numbers to be acquired, example ``[1, 3]``. Use ``'active'`` or ``[]`` to capture all the currently active channels on the oscilloscope. @@ -793,15 +812,15 @@ def set_options_get_trace_save(self, fname=None, ext=None, acq_type : {``'HRESolution'``, ``'NORMal'``, ``'AVERage'``, ``'AVER'``}, default :data:`~keyoscacquire.config._acq_type` Acquisition mode of the oscilloscope. will be used as num_averages if supplied, see :attr:`acq_type` - num_averages : int, 2 to 65536, default :data:`~keyoscacquire.config._num_avg` + num_averages : int, 2 to 65536, uses oscilloscope setting by default Applies only to the ``'AVERage'`` mode: The number of averages applied - p_mode : {``'NORMal'``, ``'RAW'``, ``'MAXimum'``}, default ``'RAW'`` + p_mode : {``'NORMal'``, ``'RAW'``, ``'MAXimum'``}, default :data:`keyoscacquire.config._p_mode` ``'NORMal'`` is limited to 62,500 points, whereas ``'RAW'`` gives up to 1e6 points. Use ``'MAXimum'`` for sources that are not analogue or digital - num_points : int, default 0 - Use 0 to let :attr:`p_mode` control the number of points, otherwise - override with a lower number than maximum for the :attr:`p_mode` + num_points : int, default :data:`keyoscacquire.config._num_points` + Use 0 to get the maximum amount of points for the current :attr:`p_mode`, + otherwise override with a lower number than maximum for the :attr:`p_mode` additional_header_info : str, default ```None`` Will put this string as a separate line before the column headers """ @@ -1002,9 +1021,9 @@ def _process_data_binary(raw, preambles, verbose_acquistion=True): xIncr, xOrig, xRef = float(preamble[4]), float(preamble[5]), float(preamble[6]) time = np.array([(np.arange(num_samples)-xRef)*xIncr + xOrig]) # compute x-values time = time.T # make x values vertical + _log.debug(f"Points captured per channel: {num_samples:,d}") if verbose_acquistion: - print(f"Points captured per channel: {num_samples:,d}") - _log.info(f"Points captured per channel: {num_samples:,d}") + print(f"Points captured per channel: {num_samples:,d}") y = np.empty((len(raw), num_samples)) for i, data in enumerate(raw): # process each channel individually preamble = preambles[i].split(',') @@ -1043,9 +1062,9 @@ def _process_data_ascii(raw, metadata, verbose_acquistion=True): # Compute time axis and wrap in extra [] to make it 2D time = np.array([(np.arange(num_samples)-xRef)*xIncr + xOrig]) time = time.T # Make list vertical + _log.debug(f"Points captured per channel: {num_samples:,d}") if verbose_acquistion: - print(f"Points captured per channel: {num_samples:,d}") - _log.info(f"Points captured per channel: {num_samples:,d}") + print(f"Points captured per channel: {num_samples:,d}") y = [] for data in raw: if model_series in ['2000']: diff --git a/keyoscacquire/programmes.py b/keyoscacquire/programmes.py index bcd1478..f178f91 100644 --- a/keyoscacquire/programmes.py +++ b/keyoscacquire/programmes.py @@ -22,7 +22,7 @@ # local file with default options: import keyoscacquire.config as config -import keyoscacquire.config as auxiliary +import keyoscacquire.auxiliary as auxiliary def list_visa_devices(ask_idn=True): @@ -63,7 +63,7 @@ def list_visa_devices(ask_idn=True): try: current_resource_info.extend(auxiliary.interpret_visa_id(id)) except Exception as ex: - print(f"Instrument #{i}: Could not interpret VISA id, got" + print(f"Instrument #{i}: Could not interpret VISA id, got " f"exception {ex.__class__.__name__}: VISA id returned was '{id}'") could_not_connect.append(i) current_resource_info.extend(["failed to interpret"]*5) @@ -72,9 +72,9 @@ def list_visa_devices(ask_idn=True): # transpose to lists of property nums, addrs, aliases, makers, models, serials, firmwares, model_series = (list(category) for category in zip(*information)) # select what properties to list - selection = (nums, addrs, makers, models, firmware, aliases) + selection = (nums, addrs, makers, models, serials, aliases) # name columns - header_fields = (' #', 'address', 'maker', 'model', 'firmware', 'alias') + header_fields = (' #', 'address', 'maker', 'model', 'serial', 'alias') row_format = "{:>{p[0]}s} {:{p[1]}s} {:{p[2]}s} {:{p[3]}s} {:{p[4]}s} {:{p[5]}s}" else: selection = [list(category) for category in zip(*information)] @@ -101,20 +101,22 @@ def path_of_config(): def get_single_trace(fname=config._filename, ext=config._filetype, address=config._visa_address, timeout=config._timeout, wav_format=config._waveform_format, - channels=config._ch_nums, source_type='CHANnel', acq_type=config._acq_type, - num_averages=config._num_avg, p_mode='RAW', num_points=0): + channels=None, acq_type=config._acq_type, num_averages=None, + p_mode=config._p_mode, num_points=config._num_points): """This programme captures and stores a single trace.""" with acq.Oscilloscope(address=address, timeout=timeout) as scope: scope.set_options_get_trace_save(fname=fname, ext=ext, wav_format=wav_format, - channels=channels, source_type=source_type, acq_type=acq_type, - num_averages=num_averages, p_mode=p_mode, num_points=num_points) + channels=channels, acq_type=acq_type, + num_averages=num_averages, p_mode=p_mode, + num_points=num_points) print("Done") def get_traces_connect_each_time_loop(fname=config._filename, ext=config._filetype, address=config._visa_address, timeout=config._timeout, wav_format=config._waveform_format, - channels=config._ch_nums, source_type='CHANnel', acq_type=config._acq_type, - num_averages=config._num_avg, p_mode='RAW', num_points=0, start_num=0, file_delim=config._file_delimiter): + channels=None, acq_type=config._acq_type, num_averages=None, + p_mode=config._p_mode, num_points=config._num_points, + start_num=0, file_delim=config._file_delimiter): """This program consists of a loop in which the program connects to the oscilloscope, a trace from the active channels are captured and stored for each loop. @@ -127,24 +129,27 @@ def get_traces_connect_each_time_loop(fname=config._filename, ext=config._filety # Check that file does not exist from before, append to name if it does n = start_num fname = auxiliary.check_file(fname, ext, num=f"{file_delim}{n}") - print("Running a loop where at every 'enter' oscilloscope traces will be saved as %s%s," % (fname, ext)) + print(f"Running a loop where at every 'enter' oscilloscope traces will be saved as {fname}{ext},") print("where increases by one for each captured trace. Press 'q'+'enter' to quit the programme.") while sys.stdin.read(1) != 'q': # breaks the loop if q+enter is given as input. For any other character (incl. enter) fnum = f"{file_delim}{n}" with acq.Oscilloscope(address=address, timeout=timeout) as scope: scope.ext = ext scope.set_options_get_trace(wav_format=wav_format, - channels=channels, source_type=source_type, acq_type=acq_type, - num_averages=num_averages, p_mode=p_mode, num_points=num_points) + channels=channels, acq_type=acq_type, + num_averages=num_averages, p_mode=p_mode, + num_points=num_points) scope.save_trace(fname+fnum) n += 1 print("Quit") -def get_traces_single_connection_loop(fname=config._filename, ext=config._filetype, address=config._visa_address, - timeout=config._timeout, wav_format=config._waveform_format, - channels=config._ch_nums, source_type='CHANnel', acq_type=config._acq_type, - num_averages=config._num_avg, p_mode='RAW', num_points=0, start_num=0, file_delim=config._file_delimiter): +def get_traces_single_connection_loop(fname=config._filename, ext=config._filetype, + address=config._visa_address, timeout=config._timeout, + wav_format=config._waveform_format, + channels=None, acq_type=config._acq_type, + num_averages=None, p_mode=config._p_mode, num_points=config._num_points, + start_num=0, file_delim=config._file_delimiter): """This program connects to the oscilloscope, sets options for the acquisition and then enters a loop in which the program captures and stores traces each time 'enter' is pressed. @@ -155,19 +160,18 @@ def get_traces_single_connection_loop(fname=config._filename, ext=config._filety changing thoughout the measurements. """ with acq.Oscilloscope(address=address, timeout=timeout) as scope: - ## Initialise scope.set_acquiring_options(wav_format=wav_format, acq_type=acq_type, num_averages=num_averages, p_mode=p_mode, num_points=num_points) scope.ext = ext scope.set_channels_for_capture(channels=channels) + scope.print_acq_settings() # Check that file does not exist from before, append to name if it does n = start_num fname = auxiliary.check_file(fname, ext, num=f"{file_delim}{n}") - print("Running a loop where at every 'enter' oscilloscope traces will be saved as %s%s," % (fname, ext)) + print(f"Running a loop where at every 'enter' oscilloscope traces will be saved as {fname}{ext},") print("where increases by one for each captured trace. Press 'q'+'enter' to quit the programme.") while sys.stdin.read(1) != 'q': # breaks the loop if q+enter is given as input. For any other character (incl. enter) - scope.verbose_acquistion = (i==n) fnum = f"{file_delim}{n}" scope.get_trace() scope.save_trace(fname+fnum) @@ -175,27 +179,33 @@ def get_traces_single_connection_loop(fname=config._filename, ext=config._filety print("Quit") -def get_num_traces(fname=config._filename, ext=config._filetype, num=1, address=config._visa_address, - timeout=config._timeout, wav_format=config._waveform_format, - channels=config._ch_nums, source_type='CHANnel', acq_type=config._acq_type, - num_averages=config._num_avg, p_mode='RAW', num_points=0, start_num=0, file_delim=config._file_delimiter): +def get_num_traces(fname=config._filename, ext=config._filetype, num=1, + address=config._visa_address, timeout=config._timeout, + wav_format=config._waveform_format, channels=None, + acq_type=config._acq_type, num_averages=None, + p_mode=config._p_mode, num_points=config._num_points, + start_num=0, file_delim=config._file_delimiter): """This program connects to the oscilloscope, sets options for the acquisition, and captures and stores 'num' traces. """ with acq.Oscilloscope(address=address, timeout=timeout) as scope: scope.set_acquiring_options(wav_format=wav_format, acq_type=acq_type, num_averages=num_averages, p_mode=p_mode, - num_points=num_points, acq_print=False) + num_points=num_points) scope.ext = ext - ## Select sources + scope.verbose_acquistion = False scope.set_channels_for_capture(channels=channels) + scope.print_acq_settings() n = start_num fnum = file_delim+str(n) # Check that file does not exist from before, append to name if it does fname = auxiliary.check_file(fname, ext, num=fnum) for i in tqdm(range(n, n+num)): - fnum = file_delim+str(i) - scope.verbose_acquistion = (i==n) - scope.get_trace() - scope.save_trace(fname+fnum) + try: + fnum = file_delim+str(i) + scope.get_trace() + scope.save_trace(fname+fnum) + except KeyboardInterrupt: + print("Stopping the programme") + return print("Done") diff --git a/keyoscacquire/scripts/example.py b/keyoscacquire/scripts/example.py index 3a857e2..9c8c080 100644 --- a/keyoscacquire/scripts/example.py +++ b/keyoscacquire/scripts/example.py @@ -1,13 +1,37 @@ -import keyoscacquire.oscacq as koa +import keyoscacquire as koa import matplotlib.pyplot as plt -def get_averaged(osc_address, averages=8): - with koa.Oscilloscope(address=osc_address) as scope: - time, volts, channels = scope.set_options_get_trace(acq_type='AVER'+str(averages)) - return time, volts, channels +def averaged_trace(scope, measurement_number, averages=8): + # Set the number of averages and get a trace + time, voltages, _ = scope.set_options_get_trace(acq_type=f"AVER{averages}") + # Save the trace data as a csv and a png plot, without showing the plot + # (the averaging mode and the number of averages is also automatically + # saved inside the file, together with a timestamp and more) + scope.save_trace(fname=f"measurement{measurement_number}_AVER{averages}", + showplot=False) + return time, voltages -time, volts, channels = get_averaged('USB0::1234::1234::MY1234567::INSTR') -for y, ch in zip(volts.T, channels): # need to transpose volts: each row is one channel - plt.plot(time, y, label=ch, color=koa._screen_colors[ch]) -plt.legend() -plt.show() + +def different_averaging(visa_address, measurement_number): + # Connect to the scope + with koa.Oscilloscope(address=visa_address) as scope: + # Set the channels to view on the scope + scope.active_channels = [1, 3] + # Prepare a two panel plot + fig, ax = plt.subplots(nrows=2, sharex=True) + # Obtain traces for different numbers of averages + for averages in [2, 4, 8, 16, 32]: + time, voltages = averaged_trace(scope, measurement_number, averages=averages) + # Plot channel 1 to ax[0] and ch 3 to ax[1] + for a, ch in zip(ax, voltages.T): + a.plot(time, ch, label=f"{averages}", alpha=0.5) + # Add legends to and labels to both plots + for a, ch_num in zip(ax, scope.active_channels): + a.set_xlabel("Time [s]") + a.set_ylabel(f"Channel {ch_num} [V]") + a.legend() + plt.show() + + +different_averaging(visa_address="USB0::1234::1234::MY1234567::INSTR", + measurement_number=1) diff --git a/keyoscacquire/traceio.py b/keyoscacquire/traceio.py index c61ee1d..bcd281a 100644 --- a/keyoscacquire/traceio.py +++ b/keyoscacquire/traceio.py @@ -104,13 +104,14 @@ def plot_trace(time, y, channels, fname="", showplot=config._show_plot, savepng : bool, default :data:`~keyoscacquire.config._export_png` ``True`` exports the plot to ``fname``.png """ + fig, ax = plt.subplots() for i, vals in enumerate(np.transpose(y)): # for each channel - plt.plot(time, vals, color=oscacq._screen_colors[channels[i]]) + ax.plot(time, vals, color=oscacq._screen_colors[channels[i]]) if savepng: - plt.savefig(fname+".png", bbox_inches='tight') + fig.savefig(fname+".png", bbox_inches='tight') if showplot: - plt.show() - plt.close() + plt.show(fig) + plt.close(fig) ## Trace loading ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## From 11be52832ca9b14437fcc66f9a4ff200e42b26d5 Mon Sep 17 00:00:00 2001 From: asvela at 0011 Date: Mon, 21 Dec 2020 02:49:07 +0100 Subject: [PATCH 10/52] sorting the channels argument --- keyoscacquire/installed_cli_programmes.py | 120 ++++++++++++---------- keyoscacquire/oscacq.py | 11 +- 2 files changed, 72 insertions(+), 59 deletions(-) diff --git a/keyoscacquire/installed_cli_programmes.py b/keyoscacquire/installed_cli_programmes.py index a230884..9d59367 100644 --- a/keyoscacquire/installed_cli_programmes.py +++ b/keyoscacquire/installed_cli_programmes.py @@ -37,65 +37,82 @@ points_help = f"Use 0 to get the maximum number of points, or set a specific number (the scope might change it slightly). Defaults to '{config._num_points}." delim_help = f"Delimiter used between filename and filenumber (before filetype). Defaults to '{config._file_delimiter}'." +def standard_arguments(parser): + connection_gr = parser.add_argument_group('Connection settings') + connection_gr.add_argument('-v', '--visa_address', + nargs='?', default=config._visa_address, help=visa_help) + connection_gr.add_argument('-t', '--timeout', + nargs='?', type=int, default=config._timeout, help=timeout_help) + acquire_gr = parser.add_argument_group('Acquisition settings') + acquire_gr.add_argument('-c', '--channels', + nargs='*', type=int, default=None, help=channels_help) + acquire_gr.add_argument('-a', '--acq_type', + nargs='?',default=None, help=acq_help) + trans_gr = parser.add_argument_group('Transfer and storage settings') + trans_gr.add_argument('-w', '--wav_format', + nargs='?', default=config._waveform_format, help=wav_help) + trans_gr.add_argument('-p', '--num_points', + nargs='?', type=int, default=config._num_points, help=points_help) + trans_gr.add_argument('-f', '--filename', + nargs='?', default=config._filename, help=file_help) + return trans_gr + + def connect_each_time_cli(): """Function installed on the command line: Obtains and stores multiple traces, connecting to the oscilloscope each time.""" parser = argparse.ArgumentParser(description=acqprog.get_traces_connect_each_time_loop.__doc__) - connection_gr = parser.add_argument_group('Connection settings') - connection_gr.add_argument('-v', '--visa_address', nargs='?', help=visa_help, default=config._visa_address) - connection_gr.add_argument('-t', '--timeout', nargs='?', help=timeout_help, default=config._timeout, type=int) - acquire_gr = parser.add_argument_group('Acquisition settings') - acquire_gr.add_argument('-c', '--channels', nargs='*', type=int, help=channels_help, default=None) - acquire_gr.add_argument('-a', '--acq_type', nargs='?', help=acq_help, default=None) - trans_gr = parser.add_argument_group('Transfer and storage settings') - trans_gr.add_argument('-w', '--wav_format', nargs='?', help=wav_help, default=config._waveform_format) - trans_gr.add_argument('-p', '--num_points', nargs='?', help=points_help, default=config._num_points, type=int) - trans_gr.add_argument('-f', '--filename', nargs='?', help=file_help, default=config._filename) + trans_gr = standard_arguments(parser) trans_gr.add_argument('--file_delimiter', nargs='?', help=delim_help, default=config._file_delimiter) args = parser.parse_args() - - acqprog.get_traces_connect_each_time_loop(fname=args.filename, address=args.visa_address, timeout=args.timeout, wav_format=args.wav_format, - channels=args.channels, acq_type=args.acq_type, num_points=args.num_points, file_delim=args.file_delimiter) + # Convert channels arg to ints + if args.channels is not None: + args.channels = [int(c) for c in args.channels] + acqprog.get_traces_connect_each_time_loop(fname=args.filename, + address=args.visa_address, + timeout=args.timeout, + wav_format=args.wav_format, + channels=args.channels, + acq_type=args.acq_type, + num_points=args.num_points, + file_delim=args.file_delimiter) def single_connection_cli(): """Function installed on the command line: Obtains and stores multiple traces, keeping a the same connection to the oscilloscope open all the time.""" parser = argparse.ArgumentParser(description=acqprog.get_traces_single_connection_loop.__doc__) - connection_gr = parser.add_argument_group('Connection settings') - connection_gr.add_argument('-v', '--visa_address', nargs='?', help=visa_help, default=config._visa_address) - connection_gr.add_argument('-t', '--timeout', nargs='?', help=timeout_help, default=config._timeout, type=int) - acquire_gr = parser.add_argument_group('Acquisition settings') - acquire_gr.add_argument('-c', '--channels', nargs='*', type=int, help=channels_help, default=None) - acquire_gr.add_argument('-a', '--acq_type', nargs='?', help=acq_help, default=None) - trans_gr = parser.add_argument_group('Transfer and storage settings') - trans_gr.add_argument('-w', '--wav_format', nargs='?', help=wav_help, default=config._waveform_format) - trans_gr.add_argument('-p', '--num_points', nargs='?', help=points_help, default=config._num_points, type=int) - trans_gr.add_argument('-f', '--filename', nargs='?', help=file_help, default=config._filename) + trans_gr = standard_arguments(parser) trans_gr.add_argument('--file_delimiter', nargs='?', help=delim_help, default=config._file_delimiter) args = parser.parse_args() - - acqprog.get_traces_single_connection_loop(fname=args.filename, address=args.visa_address, timeout=args.timeout, wav_format=args.wav_format, - channels=args.channels, acq_type=args.acq_type, num_points=args.num_points, file_delim=args.file_delimiter) + # Convert channels arg to ints + if args.channels is not None: + args.channels = [int(c) for c in args.channels] + acqprog.get_traces_single_connection_loop(fname=args.filename, + address=args.visa_address, + timeout=args.timeout, + wav_format=args.wav_format, + channels=args.channels, + acq_type=args.acq_type, + num_points=args.num_points, + file_delim=args.file_delimiter) def single_trace_cli(): """Function installed on the command line: Obtains and stores a single trace.""" parser = argparse.ArgumentParser(description=acqprog.get_single_trace.__doc__) - connection_gr = parser.add_argument_group('Connection settings') - connection_gr.add_argument('-v', '--visa_address', nargs='?', help=visa_help, default=config._visa_address) - connection_gr.add_argument('-t', '--timeout', nargs='?', help=timeout_help, default=config._timeout, type=int) - acquire_gr = parser.add_argument_group('Acquisition settings') - acquire_gr.add_argument('-c', '--channels', nargs='*', type=int, help=channels_help, default=None) - acquire_gr.add_argument('-a', '--acq_type', nargs='?', help=acq_help, default=None) - trans_gr = parser.add_argument_group('Transfer and storage settings') - trans_gr.add_argument('-w', '--wav_format', nargs='?', help=wav_help, default=config._waveform_format) - trans_gr.add_argument('-p', '--num_points', nargs='?', help=points_help, default=config._num_points, type=int) - trans_gr.add_argument('-f', '--filename', nargs='?', help=file_help, default=config._filename) + standard_arguments(parser) args = parser.parse_args() - - acqprog.get_single_trace(fname=args.filename, address=args.visa_address, timeout=args.timeout, wav_format=args.wav_format, - channels=args.channels, acq_type=args.acq_type, num_points=args.num_points) + # Convert channels arg to ints + if args.channels is not None: + args.channels = [int(c) for c in args.channels] + acqprog.get_single_trace(fname=args.filename, + address=args.visa_address, + timeout=args.timeout, + wav_format=args.wav_format, + channels=args.channels, + acq_type=args.acq_type, + num_points=args.num_points) def num_traces_cli(): """Function installed on the command line: Obtains and stores a single trace.""" @@ -103,21 +120,20 @@ def num_traces_cli(): # postitional arg parser.add_argument('num', help='The number of successive traces to obtain.', type=int) # optional args - connection_gr = parser.add_argument_group('Connection settings') - connection_gr.add_argument('-v', '--visa_address', nargs='?', help=visa_help, default=config._visa_address) - connection_gr.add_argument('-t', '--timeout', nargs='?', help=timeout_help, default=config._timeout, type=int) - acquire_gr = parser.add_argument_group('Acquisition settings') - acquire_gr.add_argument('-c', '--channels', nargs='*', type=int, help=channels_help, default=None) - acquire_gr.add_argument('-a', '--acq_type', nargs='?', help=acq_help, default=None) - trans_gr = parser.add_argument_group('Transfer and storage settings') - trans_gr.add_argument('-w', '--wav_format', nargs='?', help=wav_help, default=config._waveform_format) - trans_gr.add_argument('-p', '--num_points', nargs='?', help=points_help, default=config._num_points, type=int) - trans_gr.add_argument('-f', '--filename', nargs='?', help=file_help, default=config._filename) + trans_gr = standard_arguments(parser) trans_gr.add_argument('--file_delimiter', nargs='?', help=delim_help, default=config._file_delimiter) args = parser.parse_args() - - acqprog.get_num_traces(num=args.num, fname=args.filename, address=args.visa_address, timeout=args.timeout, wav_format=args.wav_format, - channels=args.channels, acq_type=args.acq_type, num_points=args.num_points) + # Convert channels arg to ints + if args.channels is not None: + args.channels = [int(c) for c in args.channels] + acqprog.get_num_traces(num=args.num, + fname=args.filename, + address=args.visa_address, + timeout=args.timeout, + wav_format=args.wav_format, + channels=args.channels, + acq_type=args.acq_type, + num_points=args.num_points) def list_visa_devices_cli(): """Function installed on the command line: Lists VISA devices""" diff --git a/keyoscacquire/oscacq.py b/keyoscacquire/oscacq.py index 0ac08e4..079ec48 100644 --- a/keyoscacquire/oscacq.py +++ b/keyoscacquire/oscacq.py @@ -98,7 +98,6 @@ class Oscilloscope: _capture_channels : list of ints The channels of captured for the most recent trace """ - _capture_active = True _capture_channels = None _raw = None _metadata = None @@ -542,7 +541,7 @@ def set_channels_for_capture(self, channels=None): Parameters ---------- - channels : list of ints or ``'active'``, default :data:`~keyoscacquire.config._ch_nums` + channels : list of ints or ``'active'``, default list of the channel numbers to be acquired, example ``[1, 3]``. Use ``'active'`` or ``[]`` to capture all the currently active channels on the oscilloscope. @@ -553,13 +552,10 @@ def set_channels_for_capture(self, channels=None): the channels that will be captured, example ``[1, 3]`` """ # If no channels specified, find the channels currently active and acquire from those - if np.any(channels in [[], ['active'], 'active']) or (self._capture_active and channels is None): + if channels is None or np.any(channels in [[], ['active'], 'active']): self._capture_channels = self.active_channels - # Store that active channels are being used - self._capture_active = True else: self._capture_channels = channels - self._capture_active = False # Build list of sources self._sources = [f"CHAN{ch}" for ch in self._capture_channels] return self._capture_channels @@ -733,7 +729,8 @@ def get_trace(self, channels=None, verbose_acquistion=None): # Possibility to override verbose_acquistion if verbose_acquistion is not None: self.verbose_acquistion = verbose_acquistion - self.set_channels_for_capture(channels=channels) + if channels is not None: + self.set_channels_for_capture(channels=channels) # Capture, read and process data self.capture_and_read() self._time, self._values = process_data(self._raw, self._metadata, self.wav_format, From d0d7bf2d8745f9f115ec3c938d8181036dc2ed5a Mon Sep 17 00:00:00 2001 From: asvela at 0011 Date: Mon, 21 Dec 2020 02:49:31 +0100 Subject: [PATCH 11/52] updating format_comparision.py --- tests/format_comparison.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/format_comparison.py b/tests/format_comparison.py index 0dc559c..da1bcea 100644 --- a/tests/format_comparison.py +++ b/tests/format_comparison.py @@ -6,7 +6,7 @@ """ visa_path = 'C:\\Program Files\\IVI Foundation\\VISA\\Win64\\agvisa\\agbin\\visa32.dll' -visa_address = 'USB0::0x0957::0x1796::MY59125372::INSTR' +visa_address = 'USB0::0x0957::0x1796::MY56272971::INSTR' import pyvisa @@ -19,19 +19,19 @@ formats = ['BYTE', 'WORD', 'ASCii'] -print("\n## ~~~~~~~~~~~~~~~~~ KEYOSCAQUIRE ~~~~~~~~~~~~~~~~~~ ##") +print("\n## ~~~~~~~~~~~~~~~~~ KEYOSCACQUIRE ~~~~~~~~~~~~~~~~~~ ##") scope = koa.Oscilloscope(address=visa_address) -scope.set_acquiring_options(num_points=2000) -scope.set_channels_for_capture(channel_nums=['1']) -scope.stop() +scope.num_points = 2000 +scope.set_channels_for_capture(channels=[1]) +# scope.stop() times, values = [[], []], [[], []] for wav_format in formats: - print("\nWaveform format", wav_format) - scope.set_acquiring_options(wav_format=wav_format) + print("\nWaveform format: ", wav_format) + scope.wav_format = wav_format scope.capture_and_read(set_running=False) - time, vals = koa.process_data(scope.raw, scope.metadata, wav_format, acquire_print=True) + time, vals = koa.oscacq.process_data(scope._raw, scope._metadata, wav_format, verbose_acquistion=True) times[0].append(time) values[0].append(vals) @@ -41,7 +41,7 @@ print("\n## ~~~~~~~~~~~~~~~~~~~ PYVISA ~~~~~~~~~~~~~~~~~~~~~ ##") # use Keysight VISA and connect to instrument -rm = pyvisa.ResourceManager(visa_path) +rm = pyvisa.ResourceManager()#visa_path) inst = rm.open_resource(visa_address) inst.write('*CLS') # clears the status data structures, the device-defined error queue, and the Request-for-OPC flag id = inst.query('*IDN?').strip() # get the id of the connected device From fb3d058203dc6b404ff36e4061c8ab6dd4e75531 Mon Sep 17 00:00:00 2001 From: Andreas Svela Date: Mon, 21 Dec 2020 03:05:02 +0100 Subject: [PATCH 12/52] (docs) minor --- docs/index.rst | 8 -------- keyoscacquire/oscacq.py | 22 ++++++++++++---------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 465e07a..fdf91d6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -41,11 +41,3 @@ License ------- The project is licensed under the MIT license, see :ref:`license`. - - -Indices and tables ------------------- - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/keyoscacquire/oscacq.py b/keyoscacquire/oscacq.py index 6519946..3a27412 100644 --- a/keyoscacquire/oscacq.py +++ b/keyoscacquire/oscacq.py @@ -88,7 +88,9 @@ class Oscilloscope: 'KEYSIGHT TECHNOLOGIES,MSO9104A,MY12345678,06.30.00609' _model : str - The instrument model name + The instrument's model name + _serial : str + The instrument's serial number _address : str Visa address of instrument _time : :class:`~numpy.ndarray` @@ -494,7 +496,7 @@ def set_waveform_export_options(self, wav_format=None, num_points=None, p_mode=N Parameters ---------- - wav_format : {``'WORD'``, ``'BYTE'``, ``'ASCii'``}, default :data:`~keyoscacquire.config._waveform_format` + wav_format : {``'WORD'``, ``'BYTE'``, ``'ASCii'``}, default :data:`keyoscacquire.config._waveform_format` Select the format of the communication of waveform from the oscilloscope, see :attr:`wav_format` p_mode : {``'NORMal'``, ``'RAW'``}, default ``'RAW'`` @@ -519,7 +521,7 @@ def set_channels_for_capture(self, channels=None): Parameters ---------- - channels : list of ints or ``'active'``, default :data:`~keyoscacquire.config._ch_nums` + channels : list of ints or ``'active'``, default :data:`keyoscacquire.config._ch_nums` list of the channel numbers to be acquired, example ``[1, 3]``. Use ``'active'`` or ``[]`` to capture all the currently active channels on the oscilloscope. @@ -821,9 +823,9 @@ def generate_file_header(self, channels=None, additional_line=None, timestamp=Tr additional_line time, - Where ```` is the :attr:`~keyoscacquire.oscacq.Oscilloscope.id` of - the oscilloscope, ```` is the :attr:`~keyoscacquire.oscacq.Oscilloscope.acq_type`, - ```` :attr:`~keyoscacquire.oscacq.Oscilloscope.num_averages` + Where ```` is the :attr:`_id` of + the oscilloscope, ```` is the :attr:`acq_type`, + ```` :attr:`num_averages` (``"N/A"`` if not applicable) and ```` are the comma separated channels used. @@ -883,15 +885,15 @@ def save_trace(self, fname=None, ext=None, additional_header_info=None, Parameters ---------- - fname : str, default :data:`~keyoscacquire.config._filename` + fname : str, default :data:`keyoscacquire.config._filename` Filename of trace - ext : ``{'.csv', '.npy'}``, default :data:`~keyoscacquire.config._ext` + ext : ``{'.csv', '.npy'}``, default :data:`keyoscacquire.config._filetype` Choose the filetype of the saved trace additional_header_info : str, default ```None`` Will put this string as a separate line before the column headers - savepng : bool, default :data:`~keyoscacquire.config._export_png` + savepng : bool, default :data:`keyoscacquire.config._export_png` Choose whether to also save a png with the same filename - showplot : bool, default :data:`~keyoscacquire.config._show_plot` + showplot : bool, default :data:`keyoscacquire.config._show_plot` Choose whether to show a plot of the trace """ if not self._time is None: From 9f8de764b01975cc9f120d15e333462f380a1166 Mon Sep 17 00:00:00 2001 From: Andreas Svela Date: Mon, 21 Dec 2020 03:39:55 +0100 Subject: [PATCH 13/52] (docs) adding load from file to overview --- README.rst | 58 +++++++++++++++++++++++++++++------------ keyoscacquire/oscacq.py | 2 +- 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/README.rst b/README.rst index f441ab7..f0a0eed 100644 --- a/README.rst +++ b/README.rst @@ -38,25 +38,51 @@ information about the API. As an example of API usage/use in the Python console:: - >>> import keyoscacquire.oscacq as koa - >>> scope = koa.Oscilloscope(address='USB0::1234::1234::MY1234567::INSTR') - >>> scope.acq_type = 'AVER8' - >>> print(scope.num_points) - 7680 - >>> time, y, channel_numbers = scope.get_trace(channels=[2, 1, 4]) - Acquire from channels: [1, 2, 4] - Start acquisition.. - Points captured per channel: 7,680 - >>> print(channel_numbers) - [1, 2, 4] - >>> scope.save_trace(showplot=True) - Saving trace to: data.csv - >>> scope.close() + >>> import keyoscacquire.oscacq as koa + >>> scope = koa.Oscilloscope(address='USB0::1234::1234::MY1234567::INSTR') + Connected to: + AGILENT TECHNOLOGIES + DSO-X 2024A (serial MY1234567) + >>> scope.acq_type = 'AVER8' + >>> print(scope.num_points) + 7680 + >>> time, y, channel_numbers = scope.get_trace(channels=[2, 1, 4]) + Acquisition type: AVER + # of averages: 8 + From channels: [1, 2, 4] + Acquiring ('WORD').. done + Points captured per channel: 7,680 + >>> print(channel_numbers) + [1, 2, 4] + >>> scope.save_trace(showplot=True) + Saving trace to: data.csv + >>> scope.close() where ``time`` is a vertical numpy (2D) array of time values and ``y`` is a numpy array which columns contain the data from the active channels listed in -``channel_numbers``. The trace saved to ``data.csv`` contains metadata such as -a timestamp, acquisition type, the channels used etc. +``channel_numbers``. The trace saved to ``data.csv`` also contains metadata +(can be further customised) in the first lines:: + + # AGILENT TECHNOLOGIES,DSO-X 2024A,MY1234567,02.50.2019022736 + # AVER,8 + # 2020-12-21 03:13:18.184028 + # time,1,2,4 + -5.000063390000000080e-03,-4.853398528000013590e-03,-5.247737759999995810e-03,-5.247737759999995810e-03 + ... + +The trace can be easily loaded from disk to a Pandas dataframe with:: + + >>> df, metadata = koa.traceio.load_trace("data") + >>> df.head() + time 1 2 4 + 0 -0.005 -0.004853 -0.005248 -0.005248 + 1 -0.005 -0.005406 -0.005017 -0.005248 + 2 -0.005 -0.004964 -0.005190 -0.005248 + 3 -0.005 -0.005185 -0.005363 -0.005248 + 4 -0.005 -0.005517 -0.005074 -0.005248 + >>> metadata + ['AGILENT TECHNOLOGIES,DSO-X 2024A,MY1234567,02.50.2019022736', 'AVER,8', '2020-12-21 03:13:18.184028', 'time,1,2,4'] + Command line use ---------------- diff --git a/keyoscacquire/oscacq.py b/keyoscacquire/oscacq.py index 0c73d4b..d955ede 100644 --- a/keyoscacquire/oscacq.py +++ b/keyoscacquire/oscacq.py @@ -592,7 +592,7 @@ def capture_and_read(self, set_running=True): wav_format = self.wav_format if self.verbose_acquistion: self.print_acq_settings() - print(f"Acquiring (format '{wav_format}').. ", end="", flush=True) + print(f"Acquiring (format '{wav_format}').. ", end="", flush=True) start_time = time.time() # time the acquiring process # If the instrument is not running, we presumably want the data # on the screen and hence don't want to use DIGitize as digitize From b27d79ac5f3f3d1fe8c1602a7cb17c083ed14a48 Mon Sep 17 00:00:00 2001 From: asvela at 0011 Date: Mon, 21 Dec 2020 03:40:47 +0100 Subject: [PATCH 14/52] (traceio) bugfix --- keyoscacquire/traceio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keyoscacquire/traceio.py b/keyoscacquire/traceio.py index bcd281a..6d5c676 100644 --- a/keyoscacquire/traceio.py +++ b/keyoscacquire/traceio.py @@ -187,7 +187,7 @@ def _load_trace_with_header(fname, ext, skip_lines='auto', column_names='auto', # Load the file df = pd.read_csv(fname+ext, delimiter=",", skiprows=skip_lines, names=column_names) # Return df or array - if return_df: + if return_as_df: return df, header else: return np.array([df[col].values for col in df.columns]), header From e81bc25efa85e43dc6fb4ff14c37d113d2eb8746 Mon Sep 17 00:00:00 2001 From: asvela at 0011 Date: Tue, 22 Dec 2020 18:04:13 +0100 Subject: [PATCH 15/52] (traceio) improving loading capabilites --- keyoscacquire/traceio.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/keyoscacquire/traceio.py b/keyoscacquire/traceio.py index 6d5c676..53b15f0 100644 --- a/keyoscacquire/traceio.py +++ b/keyoscacquire/traceio.py @@ -129,13 +129,21 @@ def load_trace(fname, ext=config._filetype, column_names='auto', skip_lines='aut Filename of trace, with or without extension ext : str, default :data:`~keyoscacquire.config._filetype` The filetype of the saved trace (with the period, e.g. '.csv') - column_names : ``{'auto' or list-like}``, default ``'auto'`` - Only useful if using with ``return_df=True``: - To infer df column names from the last line of the header, use ``'auto'`` - (expecting '# ' as the last line of the - header), or specify the column names manually skip_lines : ``{'auto' or int}``, default ``'auto'`` - + Number of lines from the top of the files to skip before parsing as + dataframe. Essentially the ``pandas.read_csv()`` ``skiprows`` argument. + ``'auto'`` will count the number of lines starting with ``'#'`` and + skip these lines + column_names : ``{'auto', 'header', 'first line of data', or list-like}``, default ``'auto'`` + Only useful if using with ``return_df=True``: + * ``'header'``: Infer df column names from the last line of the header + (expecting '# ' as the last line of the + header) + * 'first line of data': Will use the first line that is parsed as names, + i.e. the first line after ``skip_lines`` lines in the file + * ``'auto'``: Equivalent to ``'header'`` if there is more than zero lines + of header, otherwise ``'first line of data'`` + * list-like: Specify the column names manually return_as_df : bool, default True If the loaded trace is not a .npy file, decide to return the data as a Pandas dataframe if ``True``, or as an ndarray otherwise @@ -183,7 +191,15 @@ def _load_trace_with_header(fname, ext, skip_lines='auto', column_names='auto', if skip_lines == 'auto': skip_lines = len(header) if column_names == 'auto': + # Use the header if it is not empty + if len(header) > 0: + column_names = 'header' + else: + column_names = 'first line of data' + if column_names == 'header': column_names = header[-1].split(",") + elif column_names =='first line of data': + column_names = None # Load the file df = pd.read_csv(fname+ext, delimiter=",", skiprows=skip_lines, names=column_names) # Return df or array From 39d627c8c1f53f98d53f9206216b7ccb72fc425a Mon Sep 17 00:00:00 2001 From: asvela at 0011 Date: Tue, 22 Dec 2020 18:10:56 +0100 Subject: [PATCH 16/52] (init) importing _screen_colors --- keyoscacquire/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keyoscacquire/__init__.py b/keyoscacquire/__init__.py index b9c0fb0..1d50f82 100644 --- a/keyoscacquire/__init__.py +++ b/keyoscacquire/__init__.py @@ -21,4 +21,4 @@ import keyoscacquire.auxiliary as auxiliary -from keyoscacquire.oscacq import Oscilloscope +from keyoscacquire.oscacq import Oscilloscope, _screen_colors From c6e178f80e05d7f735204b58cd31df8105548b64 Mon Sep 17 00:00:00 2001 From: asvela at 0011 Date: Tue, 22 Dec 2020 18:13:00 +0100 Subject: [PATCH 17/52] cosmetic --- keyoscacquire/auxiliary.py | 1 + keyoscacquire/oscacq.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/keyoscacquire/auxiliary.py b/keyoscacquire/auxiliary.py index 143488d..a6b8a89 100644 --- a/keyoscacquire/auxiliary.py +++ b/keyoscacquire/auxiliary.py @@ -6,6 +6,7 @@ import os import logging; _log = logging.getLogger(__name__) + import keyoscacquire.config as config diff --git a/keyoscacquire/oscacq.py b/keyoscacquire/oscacq.py index 079ec48..5e21ce0 100644 --- a/keyoscacquire/oscacq.py +++ b/keyoscacquire/oscacq.py @@ -23,7 +23,7 @@ import keyoscacquire.auxiliary as auxiliary import keyoscacquire.traceio as traceio -# for compatibility (discouraged to use) +# for backwards compatibility (but rather use the Oscilloscope methods) from keyoscacquire.traceio import save_trace, save_trace_npy, plot_trace #: Supported Keysight DSO/MSO InfiniiVision series From dd44ef6d275d23cd90c935a31c5d95f8a34606c0 Mon Sep 17 00:00:00 2001 From: asvela at 0011 Date: Tue, 22 Dec 2020 18:13:33 +0100 Subject: [PATCH 18/52] (init) importing save and load trace --- keyoscacquire/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keyoscacquire/__init__.py b/keyoscacquire/__init__.py index 1d50f82..f9e5809 100644 --- a/keyoscacquire/__init__.py +++ b/keyoscacquire/__init__.py @@ -20,5 +20,5 @@ import keyoscacquire.programmes as programmes import keyoscacquire.auxiliary as auxiliary - from keyoscacquire.oscacq import Oscilloscope, _screen_colors +from keyoscacquire.traceio import save_trace, load_trace From 2ff004bbefd26dc817d970e9ee5fdda6909ee57a Mon Sep 17 00:00:00 2001 From: Andreas Svela Date: Tue, 22 Dec 2020 23:35:05 +0100 Subject: [PATCH 19/52] (PEP8) code review --- README.rst | 9 ++++ keyoscacquire/__init__.py | 3 +- keyoscacquire/auxiliary.py | 67 ++++++++++++++++++++++++-- keyoscacquire/oscacq.py | 22 +++++---- keyoscacquire/programmes.py | 93 +++++++++++++------------------------ keyoscacquire/traceio.py | 13 +++--- tests/format_comparison.py | 24 +++++----- 7 files changed, 137 insertions(+), 94 deletions(-) diff --git a/README.rst b/README.rst index f0a0eed..2d9a94f 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,15 @@ keyoscacquire: Keysight oscilloscope acquire ============================================ +.. image:: https://img.shields.io/codefactor/grade/github/asvela/keyoscacquire?style=flat-square + :alt: CodeFactor Grade + +.. image:: https://img.shields.io/readthedocs/keyoscacquire?style=flat-square + :alt: Read the Docs Building + +.. image:: https://img.shields.io/pypi/l/keyoscacquire?style=flat-square + :alt: License + keyoscacquire is a Python package for acquiring traces from Keysight InfiniiVision oscilloscopes through a VISA interface. diff --git a/keyoscacquire/__init__.py b/keyoscacquire/__init__.py index f9e5809..8717cb3 100644 --- a/keyoscacquire/__init__.py +++ b/keyoscacquire/__init__.py @@ -12,7 +12,8 @@ with open(os.path.join(current_dir, 'VERSION')) as version_file: __version__ = version_file.read().strip() -import logging; _log = logging.getLogger(__name__) +import logging +_log = logging.getLogger(__name__) import keyoscacquire.oscacq as oscacq import keyoscacquire.traceio as traceio diff --git a/keyoscacquire/auxiliary.py b/keyoscacquire/auxiliary.py index a6b8a89..f6edbd5 100644 --- a/keyoscacquire/auxiliary.py +++ b/keyoscacquire/auxiliary.py @@ -5,17 +5,20 @@ """ import os -import logging; _log = logging.getLogger(__name__) +import pyvisa +import logging +_log = logging.getLogger(__name__) import keyoscacquire.config as config -def interpret_visa_id(id): - """Interprets VISA ID, finds oscilloscope model series if applicable +def interpret_visa_id(idn): + """Interprets a VISA ID, including finding a oscilloscope model series + if applicable Parameters ---------- - id : str + idn : str VISA ID as returned by the ``*IDN?`` command Returns @@ -32,7 +35,7 @@ def interpret_visa_id(id): "N/A" unless the instrument is a Keysight/Agilent DSO and MSO oscilloscope. Returns the model series, e.g. '2000'. Returns "not found" if the model name cannot be interpreted. """ - maker, model, serial, firmware = id.split(",") + maker, model, serial, firmware = idn.split(",") # Find model_series if applicable if model[:3] in ['DSO', 'MSO']: # Find the numbers in the model string @@ -44,6 +47,60 @@ def interpret_visa_id(id): return maker, model, serial, firmware, model_series +def obtain_instrument_information(resource_manager, address, ask_idn=True): + """Obtain more information about a VISA resource + + Parameters + ---------- + resource_manager : :class:`pyvisa.resource_manager` + address : str + VISA address of the instrument to be investigated + ask_idn : bool + If ``True``: will query the instrument's IDN and interpret it + if possible + + Returns + ------- + resource_info : list + List of information:: + + [address, alias, maker, model, serial, firmware, model_series] + + when ``ask_idn`` is ``True``, otherwise:: + + [address, alias] + + """ + resource_info = [] + info_object = resource_manager.resource_info(address) + alias = info_object.alias if info_object.alias is not None else "N/A" + resource_info.extend((address, alias)) + if ask_idn: + # Open the instrument and get the identity string + try: + error_flag = False + instrument = rm.open_resource(address) + idn = instrument.query("*IDN?").strip() + instrument.close() + except pyvisa.Error as e: + error_flag = True + resource_info.extend(["no IDN response"]*5) + print(f"Instrument #{i}: Did not respond to *IDN?: {e}") + except Exception as ex: + error_flag = True + print(f"Instrument #{i}: Got exception {ex.__class__.__name__} " + f"when asking for its identity.") + resource_info.extend(["Error"]*5) + if not error_flag: + try: + resource_info.extend(interpret_visa_id(idn)) + except Exception as ex: + print(f"Instrument #{i}: Could not interpret VISA id, got " + f"exception {ex.__class__.__name__}: VISA id returned was '{idn}'") + resource_info.extend(["failed to interpret"]*5) + return resource_info + + def check_file(fname, ext=config._filetype, num=""): """Checking if file ``fname+num+ext`` exists. If it does, the user is prompted for a string to append to fname until a unique fname is found. diff --git a/keyoscacquire/oscacq.py b/keyoscacquire/oscacq.py index 5694ec6..94d2865 100644 --- a/keyoscacquire/oscacq.py +++ b/keyoscacquire/oscacq.py @@ -13,10 +13,10 @@ import sys import pyvisa import time +import logging import datetime as dt import numpy as np import matplotlib.pyplot as plt -import logging; _log = logging.getLogger(__name__) # local file with default options: import keyoscacquire.config as config @@ -26,6 +26,8 @@ # for backwards compatibility (but rather use the Oscilloscope methods) from keyoscacquire.traceio import save_trace, save_trace_npy, plot_trace +_log = logging.getLogger(__name__) + #: Supported Keysight DSO/MSO InfiniiVision series _supported_series = ['1000', '2000', '3000', '4000', '6000'] #: Keysight colour map for the channels @@ -318,19 +320,19 @@ def acq_type(self): return self.query(":ACQuire:TYPE?") @acq_type.setter - def acq_type(self, type: str): + def acq_type(self, a_type: str): """See getter""" - acq_type = type[:4].upper() + acq_type = a_type[:4].upper() self.write(f":ACQuire:TYPE {acq_type}") # Handle AVER expressions if acq_type == 'AVER': - if len(type) > 4 and not type[4:].lower() == 'age': + if len(a_type) > 4 and not a_type[4:].lower() == 'age': try: - self.num_averages = int(type[4:]) + self.num_averages = int(a_type[4:]) except ValueError: - ValueError(f"\nValueError: Failed to convert '{type[4:]}' to an integer, " + ValueError(f"\nValueError: Failed to convert '{a_type[4:]}' to an integer, " "check that acquisition type is on the form AVER or AVER " - f"where is an integer (currently acq. type is '{type}').\n") + f"where is an integer (currently acq. type is '{a_type}').\n") @property def num_averages(self): @@ -985,11 +987,13 @@ def process_data(raw, metadata, wav_format, verbose_acquistion=True): :func:`Oscilloscope.capture_and_read` """ if wav_format[:3] in ['WOR', 'BYT']: - return _process_data_binary(raw, metadata, verbose_acquistion) + process_fn = _process_data_binary elif wav_format[:3] == 'ASC': - return _process_data_ascii(raw, metadata, verbose_acquistion) + processing_fn = _process_data_ascii else: raise ValueError("Could not process data, waveform format \'{}\' is unknown.".format(wav_format)) + return processing_fn(raw, metadata, verbose_acquistion) + def _process_data_binary(raw, preambles, verbose_acquistion=True): """Process raw 8/16-bit data to time values and y voltage values as received diff --git a/keyoscacquire/programmes.py b/keyoscacquire/programmes.py index f178f91..7dec607 100644 --- a/keyoscacquire/programmes.py +++ b/keyoscacquire/programmes.py @@ -15,15 +15,15 @@ import os import sys import pyvisa -import logging; _log = logging.getLogger(__name__) +import logging import keyoscacquire.oscacq as acq import numpy as np from tqdm import tqdm -# local file with default options: import keyoscacquire.config as config import keyoscacquire.auxiliary as auxiliary +_log = logging.getLogger(__name__) def list_visa_devices(ask_idn=True): """Prints a list of the VISA instruments connected to the computer, @@ -32,65 +32,38 @@ def list_visa_devices(ask_idn=True): resources = rm.list_resources() if len(resources) == 0: print("\nNo VISA devices found!") + return + print(f"\nFound {len(resources)} resources. Now obtaining information about them..") + information = [] + # Loop through resources to learn more about them + for address in resources: + current_resource_info = auxiliary.obtain_instrument_information(rm, address, ask_idn) + information.append(current_resource_info) + nums = [str(i) for i in range(len(current_resource_info))] + if ask_idn: + # transpose to lists of property + addrs, aliases, makers, models, serials, firmwares, model_series = (list(category) for category in zip(*information)) + # select what properties to list + selection = (nums, addrs, makers, models, serials, aliases) + # name columns + header_fields = (' #', 'address', 'maker', 'model', 'serial', 'alias') + row_format = "{:>{p[0]}s} {:{p[1]}s} {:{p[2]}s} {:{p[3]}s} {:{p[4]}s} {:{p[5]}s}" else: - print(f"\nFound {len(resources)} resources. Now obtaining information about them..") - information, could_not_connect = [], [] - # Loop through resources to learn more about them - for i, address in enumerate(resources): - current_resource_info = [] - info_object = rm.resource_info(address) - alias = info_object.alias if info_object.alias is not None else "N/A" - current_resource_info.extend((str(i), address, alias)) - if ask_idn: - # Open the instrument and get the identity string - try: - error_flag = False - instrument = rm.open_resource(address) - id = instrument.query("*IDN?").strip() - instrument.close() - except pyvisa.Error as e: - error_flag = True - could_not_connect.append(i) - current_resource_info.extend(["no IDN response"]*5) - print(f"Instrument #{i}: Did not respond to *IDN?: {e}") - except Exception as ex: - error_flag = True - print(f"Instrument #{i}: Got exception {ex.__class__.__name__} " - f"when asking for its identity.") - could_not_connect.append(i) - current_resource_info.extend(["Error"]*5) - if not error_flag: - try: - current_resource_info.extend(auxiliary.interpret_visa_id(id)) - except Exception as ex: - print(f"Instrument #{i}: Could not interpret VISA id, got " - f"exception {ex.__class__.__name__}: VISA id returned was '{id}'") - could_not_connect.append(i) - current_resource_info.extend(["failed to interpret"]*5) - information.append(current_resource_info) - if ask_idn: - # transpose to lists of property - nums, addrs, aliases, makers, models, serials, firmwares, model_series = (list(category) for category in zip(*information)) - # select what properties to list - selection = (nums, addrs, makers, models, serials, aliases) - # name columns - header_fields = (' #', 'address', 'maker', 'model', 'serial', 'alias') - row_format = "{:>{p[0]}s} {:{p[1]}s} {:{p[2]}s} {:{p[3]}s} {:{p[4]}s} {:{p[5]}s}" - else: - selection = [list(category) for category in zip(*information)] - header_fields = (' #', 'address', 'alias') - row_format = "{:>{p[0]}s} {:{p[1]}s} {:{p[2]}s}" - # find max number of characters for each property to use as padding - padding = [max([len(instance) for instance in property]) for property in selection] - # make sure the padding is not smaller than the header field length - padding = [max(pad, len(field)) for pad, field in zip(padding, header_fields)] - header = row_format.format(*header_fields, p=padding) - # print the table - print("\nVISA devices connected:") - print(header) - print("="*(len(header)+2)) - for info in zip(*selection): - print(row_format.format(*info, p=padding)) + addrs, aliases = [list(category) for category in zip(*information)] + selection = (nums, addrs, aliases) + header_fields = (' #', 'address', 'alias') + row_format = "{:>{p[0]}s} {:{p[1]}s} {:{p[2]}s}" + # find max number of characters for each property to use as padding + padding = [max([len(instance) for instance in property]) for property in selection] + # make sure the padding is not smaller than the header field length + padding = [max(pad, len(field)) for pad, field in zip(padding, header_fields)] + header = row_format.format(*header_fields, p=padding) + # print the table + print("\nVISA devices connected:\n") + print(header) + print("="*(len(header)+2)) + for info in zip(*selection): + print(row_format.format(*info, p=padding)) def path_of_config(): diff --git a/keyoscacquire/traceio.py b/keyoscacquire/traceio.py index 53b15f0..e210f5c 100644 --- a/keyoscacquire/traceio.py +++ b/keyoscacquire/traceio.py @@ -8,7 +8,7 @@ """ import os -import logging; _log = logging.getLogger(__name__) +import logging import numpy as np import pandas as pd import matplotlib.pyplot as plt @@ -16,6 +16,7 @@ import keyoscacquire.config as config import keyoscacquire.oscacq as oscacq +_log = logging.getLogger(__name__) ## Trace saving and plotting ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## @@ -165,10 +166,9 @@ def load_trace(fname, ext=config._filetype, column_names='auto', skip_lines='aut # Format dependent if ext == '.npy': return np.load(fname+ext), None - else: - return _load_trace_with_header(fname, ext, column_names=column_names, - skip_lines=skip_lines, - return_as_df=return_as_df) + return _load_trace_with_header(fname, ext, column_names=column_names, + skip_lines=skip_lines, + return_as_df=return_as_df) def _load_trace_with_header(fname, ext, skip_lines='auto', column_names='auto', @@ -205,8 +205,7 @@ def _load_trace_with_header(fname, ext, skip_lines='auto', column_names='auto', # Return df or array if return_as_df: return df, header - else: - return np.array([df[col].values for col in df.columns]), header + return np.array([df[col].values for col in df.columns]), header def load_header(fname, ext=config._filetype): """Open a trace file and get the header diff --git a/tests/format_comparison.py b/tests/format_comparison.py index da1bcea..83c81a1 100644 --- a/tests/format_comparison.py +++ b/tests/format_comparison.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """ -Script to test obtaining data with different waveform formats for Keysight DSO2024A +Script to test obtaining data with different waveform wformat for Keysight DSO2024A Andreas Svela // 2019 """ @@ -16,7 +16,7 @@ import keyoscacquire as koa format_dict = {0: "BYTE", 1: "WORD", 4: "ASCii"} -formats = ['BYTE', 'WORD', 'ASCii'] +wformat = ['BYTE', 'WORD', 'ASCii'] print("\n## ~~~~~~~~~~~~~~~~~ KEYOSCACQUIRE ~~~~~~~~~~~~~~~~~~ ##") @@ -27,7 +27,7 @@ # scope.stop() times, values = [[], []], [[], []] -for wav_format in formats: +for wav_format in wformat: print("\nWaveform format: ", wav_format) scope.wav_format = wav_format scope.capture_and_read(set_running=False) @@ -44,13 +44,13 @@ rm = pyvisa.ResourceManager()#visa_path) inst = rm.open_resource(visa_address) inst.write('*CLS') # clears the status data structures, the device-defined error queue, and the Request-for-OPC flag -id = inst.query('*IDN?').strip() # get the id of the connected device -print("Connected to\n\t\'%s\'" % id) +idn = inst.query('*IDN?').strip() # get the id of the connected device +print(f"Connected to\n\t'{idn}'") # obtain trace from channel 1 inst.write(':WAVeform:SOURce CHAN1') -for wav_format in formats: +for wav_format in wformat: inst.write(':WAVeform:FORMat ' + wav_format) # choose format for the transmitted waveform inst.write(':WAVeform:BYTeorder LSBFirst') # MSBF is default, must be overridden for WORD to work inst.write(':WAVeform:UNSigned OFF') # make sure the scope is sending signed ints @@ -94,22 +94,22 @@ # Plotting the signals obtained for visual comparison -fig, axs = plt.subplots(nrows=len(formats), ncols=2, sharex=True, sharey=True) +fig, axs = plt.subplots(nrows=len(wformat), ncols=2, sharex=True, sharey=True) for i, (time, value, ax) in enumerate(zip(times, values, axs.T)): - for j, (t, v, a, format) in enumerate(zip(time, value, ax, formats)): + for j, (t, v, a, wformat) in enumerate(zip(time, value, ax, wformat)): try: a.plot(t, v*1000) except ValueError as err: print("Could not plot, check dimensions:", err) - a.set_title(format) + a.set_title(wformat) if i == 0: a.set_ylabel("signal [v]") if j == len(axs)-1: a.set_xlabel("time [s]") fig.suptitle("keyoscacquire pure pyvisa") -print("\nCalculating the difference between same waveform formats") +print("\nCalculating the difference between same waveform wformat") diffs = [values[0][i].T-values[1][i].T for i in range(3)] -for diff, format in zip(diffs, formats): - print("Difference in "+format+" signals: "+str(sum(sum(diff)))) +for diff, wformat in zip(diffs, wformat): + print(f"Difference in {wformat} signals: {sum(sum(diff))}") plt.show() From acba9ee130e4d48e99a048f4c207b7612a38b2d0 Mon Sep 17 00:00:00 2001 From: Andreas Svela Date: Tue, 22 Dec 2020 23:54:39 +0100 Subject: [PATCH 20/52] avoiding cyclic import --- docs/changelog.rst | 4 +++- docs/contents/osc-class.rst | 11 +++-------- keyoscacquire/__init__.py | 3 ++- keyoscacquire/auxiliary.py | 4 ++++ keyoscacquire/oscacq.py | 8 +++----- keyoscacquire/traceio.py | 4 ++-- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 6f7b3c3..7b71f3f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -99,10 +99,12 @@ v4.0.0 (2020-12) * ``Oscilloscope.model`` -> ``Oscilloscope._model`` * ``Oscilloscope.model_series`` -> ``Oscilloscope._model_series`` - - *No compatibility*: Moved functions + - *No compatibility*: Moved functions and attributes * ``interpret_visa_id()`` from ``oscacq`` to ``auxiliary`` * ``check_file()`` from ``oscacq`` to ``auxiliary`` + * ``_screen_colors`` from ``oscacq`` to ``auxiliary`` + * ``_supported_series`` from ``oscacq`` to ``auxiliary`` - *No compatibility*: Some functions no longer take ``sources`` and ``sourcesstring`` as arguments, rather ``Oscilloscope._sources`` must be set by diff --git a/docs/contents/osc-class.rst b/docs/contents/osc-class.rst index c0d77d6..46e13e6 100644 --- a/docs/contents/osc-class.rst +++ b/docs/contents/osc-class.rst @@ -30,6 +30,7 @@ Connection and VISA commands .. automethod:: Oscilloscope.query .. automethod:: Oscilloscope.get_error + Oscilloscope state control -------------------------- @@ -38,6 +39,7 @@ Oscilloscope state control .. automethod:: Oscilloscope.is_running .. autoproperty:: Oscilloscope.active_channels + Acquisition and transfer properties ----------------------------------- @@ -63,6 +65,7 @@ Multiple acquisition and transfer options setting functions .. automethod:: Oscilloscope.set_acquiring_options .. automethod:: Oscilloscope.set_waveform_export_options + Other ----- @@ -71,14 +74,6 @@ Other .. automethod:: Oscilloscope.print_acq_settings -Auxiliary to the class -====================== - -.. autodata:: _supported_series -.. autodata:: _screen_colors -.. autodata:: _datatypes - - .. _preamble: The preamble diff --git a/keyoscacquire/__init__.py b/keyoscacquire/__init__.py index 8717cb3..5b882dc 100644 --- a/keyoscacquire/__init__.py +++ b/keyoscacquire/__init__.py @@ -21,5 +21,6 @@ import keyoscacquire.programmes as programmes import keyoscacquire.auxiliary as auxiliary -from keyoscacquire.oscacq import Oscilloscope, _screen_colors +from keyoscacquire.oscacq import Oscilloscope from keyoscacquire.traceio import save_trace, load_trace +from keyoscacquire.auxiliary import _screen_colors diff --git a/keyoscacquire/auxiliary.py b/keyoscacquire/auxiliary.py index f6edbd5..0cbb0be 100644 --- a/keyoscacquire/auxiliary.py +++ b/keyoscacquire/auxiliary.py @@ -11,6 +11,10 @@ import keyoscacquire.config as config +#: Supported Keysight DSO/MSO InfiniiVision series +_supported_series = ['1000', '2000', '3000', '4000', '6000'] +#: Keysight colour map for the channels +_screen_colors = {1:'C1', 2:'C2', 3:'C0', 4:'C3'} def interpret_visa_id(idn): """Interprets a VISA ID, including finding a oscilloscope model series diff --git a/keyoscacquire/oscacq.py b/keyoscacquire/oscacq.py index 94d2865..6465591 100644 --- a/keyoscacquire/oscacq.py +++ b/keyoscacquire/oscacq.py @@ -23,15 +23,13 @@ import keyoscacquire.auxiliary as auxiliary import keyoscacquire.traceio as traceio +from keyoscacquire.auxiliary import _screen_colors # for backwards compatibility (but rather use the Oscilloscope methods) from keyoscacquire.traceio import save_trace, save_trace_npy, plot_trace _log = logging.getLogger(__name__) -#: Supported Keysight DSO/MSO InfiniiVision series -_supported_series = ['1000', '2000', '3000', '4000', '6000'] -#: Keysight colour map for the channels -_screen_colors = {1:'C1', 2:'C2', 3:'C0', 4:'C3'} + #: Datatype is ``'h'`` for 16 bit signed int (``WORD``), ``'b'`` for 8 bit signed bit (``BYTE``). #: Same naming as for structs `docs.python.org/3/library/struct.html#format-characters` _datatypes = {'BYT':'b', 'WOR':'h', 'BYTE':'b', 'WORD':'h'} @@ -145,7 +143,7 @@ def __init__(self, address=config._visa_address, timeout=config._timeout, verbos if self.verbose: print(f"Connected to '{self._id}'") print("(!) Failed to intepret the VISA IDN string") - if not self._model_series in _supported_series: + if not self._model_series in auxiliary._supported_series: print(f"(!) WARNING: This model ({self._model}) is not yet fully supported by keyoscacquire,") print( " but might work to some extent. keyoscacquire supports Keysight's") print( " InfiniiVision X-series oscilloscopes.") diff --git a/keyoscacquire/traceio.py b/keyoscacquire/traceio.py index e210f5c..93cd0a5 100644 --- a/keyoscacquire/traceio.py +++ b/keyoscacquire/traceio.py @@ -14,7 +14,7 @@ import matplotlib.pyplot as plt import keyoscacquire.config as config -import keyoscacquire.oscacq as oscacq +import keyoscacquire.auxiliary as auxiliary _log = logging.getLogger(__name__) @@ -107,7 +107,7 @@ def plot_trace(time, y, channels, fname="", showplot=config._show_plot, """ fig, ax = plt.subplots() for i, vals in enumerate(np.transpose(y)): # for each channel - ax.plot(time, vals, color=oscacq._screen_colors[channels[i]]) + ax.plot(time, vals, color=auxiliary._screen_colors[channels[i]]) if savepng: fig.savefig(fname+".png", bbox_inches='tight') if showplot: From eb441d919d69979c676ce85a79688ec788eea8aa Mon Sep 17 00:00:00 2001 From: Andreas Svela Date: Wed, 23 Dec 2020 00:04:55 +0100 Subject: [PATCH 21/52] making badges links, updating copyright year --- LICENSE.md | 2 +- README.rst | 5 ++++- docs/conf.py | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index e964b23..2621cd5 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,4 @@ -Copyright 2019 Andreas Svela +Copyright 2020 Andreas Svela Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/README.rst b/README.rst index 2d9a94f..e5d2b4e 100644 --- a/README.rst +++ b/README.rst @@ -2,12 +2,15 @@ keyoscacquire: Keysight oscilloscope acquire ============================================ .. image:: https://img.shields.io/codefactor/grade/github/asvela/keyoscacquire?style=flat-square - :alt: CodeFactor Grade + :target: https://www.codefactor.io/repository/github/asvela/keyoscacquire + :alt: CodeFactor .. image:: https://img.shields.io/readthedocs/keyoscacquire?style=flat-square + :target: https://keyoscacquire.rtfd.io :alt: Read the Docs Building .. image:: https://img.shields.io/pypi/l/keyoscacquire?style=flat-square + :target: https://keyoscacquire.readthedocs.io/en/dev-v4.0.0/contents/license.html :alt: License keyoscacquire is a Python package for acquiring traces from Keysight diff --git a/docs/conf.py b/docs/conf.py index 5e751a3..87035b0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -21,8 +21,8 @@ # -- Project information ----------------------------------------------------- -project = 'keyoscacquire'#'Keysight oscilloscope acquire' -copyright = '2019-2020, Andreas Svela' +project = 'keyoscacquire' +copyright = '2020, Andreas Svela' author = 'Andreas Svela' # The full version, including alpha/beta/rc tags From d7e917315227ae7061393d3d207a564a7cc40d70 Mon Sep 17 00:00:00 2001 From: asvela at 0011 Date: Wed, 23 Dec 2020 00:11:07 +0100 Subject: [PATCH 22/52] (list_visa_devices) fixes --- keyoscacquire/auxiliary.py | 18 ++++++++++-------- keyoscacquire/programmes.py | 9 ++++----- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/keyoscacquire/auxiliary.py b/keyoscacquire/auxiliary.py index 0cbb0be..e3cd6c4 100644 --- a/keyoscacquire/auxiliary.py +++ b/keyoscacquire/auxiliary.py @@ -51,7 +51,7 @@ def interpret_visa_id(idn): return maker, model, serial, firmware, model_series -def obtain_instrument_information(resource_manager, address, ask_idn=True): +def obtain_instrument_information(resource_manager, address, num, ask_idn=True): """Obtain more information about a VISA resource Parameters @@ -59,6 +59,8 @@ def obtain_instrument_information(resource_manager, address, ask_idn=True): resource_manager : :class:`pyvisa.resource_manager` address : str VISA address of the instrument to be investigated + num : int + Sequential numbering of investigated instruments ask_idn : bool If ``True``: will query the instrument's IDN and interpret it if possible @@ -68,38 +70,38 @@ def obtain_instrument_information(resource_manager, address, ask_idn=True): resource_info : list List of information:: - [address, alias, maker, model, serial, firmware, model_series] + [num, address, alias, maker, model, serial, firmware, model_series] when ``ask_idn`` is ``True``, otherwise:: - [address, alias] + [num, address, alias] """ resource_info = [] info_object = resource_manager.resource_info(address) alias = info_object.alias if info_object.alias is not None else "N/A" - resource_info.extend((address, alias)) + resource_info.extend((str(num), address, alias)) if ask_idn: # Open the instrument and get the identity string try: error_flag = False - instrument = rm.open_resource(address) + instrument = resource_manager.open_resource(address) idn = instrument.query("*IDN?").strip() instrument.close() except pyvisa.Error as e: error_flag = True resource_info.extend(["no IDN response"]*5) - print(f"Instrument #{i}: Did not respond to *IDN?: {e}") + print(f"Instrument #{num}: Did not respond to *IDN?: {e}") except Exception as ex: error_flag = True - print(f"Instrument #{i}: Got exception {ex.__class__.__name__} " + print(f"Instrument #{num}: Got exception {ex.__class__.__name__} " f"when asking for its identity.") resource_info.extend(["Error"]*5) if not error_flag: try: resource_info.extend(interpret_visa_id(idn)) except Exception as ex: - print(f"Instrument #{i}: Could not interpret VISA id, got " + print(f"Instrument #{num}: Could not interpret VISA id, got " f"exception {ex.__class__.__name__}: VISA id returned was '{idn}'") resource_info.extend(["failed to interpret"]*5) return resource_info diff --git a/keyoscacquire/programmes.py b/keyoscacquire/programmes.py index 7dec607..c6277c7 100644 --- a/keyoscacquire/programmes.py +++ b/keyoscacquire/programmes.py @@ -36,20 +36,19 @@ def list_visa_devices(ask_idn=True): print(f"\nFound {len(resources)} resources. Now obtaining information about them..") information = [] # Loop through resources to learn more about them - for address in resources: - current_resource_info = auxiliary.obtain_instrument_information(rm, address, ask_idn) + for i, address in enumerate(resources): + current_resource_info = auxiliary.obtain_instrument_information(rm, address, i, ask_idn) information.append(current_resource_info) - nums = [str(i) for i in range(len(current_resource_info))] if ask_idn: # transpose to lists of property - addrs, aliases, makers, models, serials, firmwares, model_series = (list(category) for category in zip(*information)) + nums, addrs, aliases, makers, models, serials, firmwares, model_series = (list(category) for category in zip(*information)) # select what properties to list selection = (nums, addrs, makers, models, serials, aliases) # name columns header_fields = (' #', 'address', 'maker', 'model', 'serial', 'alias') row_format = "{:>{p[0]}s} {:{p[1]}s} {:{p[2]}s} {:{p[3]}s} {:{p[4]}s} {:{p[5]}s}" else: - addrs, aliases = [list(category) for category in zip(*information)] + nums, addrs, aliases = [list(category) for category in zip(*information)] selection = (nums, addrs, aliases) header_fields = (' #', 'address', 'alias') row_format = "{:>{p[0]}s} {:{p[1]}s} {:{p[2]}s}" From c0488ff23c99ed49c73855454d5205d67d5b9c65 Mon Sep 17 00:00:00 2001 From: Andreas Svela Date: Wed, 23 Dec 2020 00:28:42 +0100 Subject: [PATCH 23/52] docfix and adding pypi badge --- README.rst | 4 ++++ docs/known-issues.rst | 4 ++++ keyoscacquire/traceio.py | 7 ++++++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index e5d2b4e..1afbc46 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,10 @@ keyoscacquire: Keysight oscilloscope acquire ============================================ +.. image:: https://img.shields.io/pypi/v/keyoscacquire?style=flat-square + :target: https://pypi.org/project/keyoscacquire/ + :alt: PyPI + .. image:: https://img.shields.io/codefactor/grade/github/asvela/keyoscacquire?style=flat-square :target: https://www.codefactor.io/repository/github/asvela/keyoscacquire :alt: CodeFactor diff --git a/docs/known-issues.rst b/docs/known-issues.rst index 72b196d..0db4985 100644 --- a/docs/known-issues.rst +++ b/docs/known-issues.rst @@ -14,9 +14,13 @@ Known issues and suggested improvements - (feature) include capture of MATH waveform - (feature) expand API to include + * waveform measurements + * trigger settings + * time and voltage axes settings + - (feature) pickling trace to disk for later post-processing to give speed-up in consecutive measurements - (instrument support) expand support for Infiniium oscilloscopes diff --git a/keyoscacquire/traceio.py b/keyoscacquire/traceio.py index 93cd0a5..e6590d2 100644 --- a/keyoscacquire/traceio.py +++ b/keyoscacquire/traceio.py @@ -137,14 +137,19 @@ def load_trace(fname, ext=config._filetype, column_names='auto', skip_lines='aut skip these lines column_names : ``{'auto', 'header', 'first line of data', or list-like}``, default ``'auto'`` Only useful if using with ``return_df=True``: + * ``'header'``: Infer df column names from the last line of the header (expecting '# ' as the last line of the header) - * 'first line of data': Will use the first line that is parsed as names, + + * ``'first line of data'``: Will use the first line that is parsed as names, i.e. the first line after ``skip_lines`` lines in the file + * ``'auto'``: Equivalent to ``'header'`` if there is more than zero lines of header, otherwise ``'first line of data'`` + * list-like: Specify the column names manually + return_as_df : bool, default True If the loaded trace is not a .npy file, decide to return the data as a Pandas dataframe if ``True``, or as an ndarray otherwise From c744d3284f0d78b052c50d88f19151af3131ded3 Mon Sep 17 00:00:00 2001 From: Andreas Svela Date: Fri, 8 Jan 2021 10:55:51 +0100 Subject: [PATCH 24/52] (oscacq) Error queue extraction (not yet tested) --- keyoscacquire/oscacq.py | 51 +++++++++++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/keyoscacquire/oscacq.py b/keyoscacquire/oscacq.py index 6465591..a4ac50c 100644 --- a/keyoscacquire/oscacq.py +++ b/keyoscacquire/oscacq.py @@ -54,6 +54,11 @@ class Oscilloscope: Example address ``'USB0::1234::1234::MY1234567::INSTR'`` timeout : int, default :data:`~keyoscacquire.config._timeout` Milliseconds before timeout on the channel to the instrument + get_errors_on_init : bool + The error queue of the scope is by default cleared when __init__ is called; + however, when this parameter is ``True``, the error queue is extracted + from the scope before the log is cleared, it is printed to the terminal and + the attribute ``errors`` is populated with the errors verbose : bool, default ``True`` If ``True``: prints when the connection to the device is opened etc, and sets attr:`verbose_acquistion` to ``True`` @@ -111,7 +116,8 @@ class Oscilloscope: showplot = config._show_plot verbose_acquistion = False - def __init__(self, address=config._visa_address, timeout=config._timeout, verbose=True): + def __init__(self, address=config._visa_address, timeout=config._timeout, + get_errors_on_init=False, verbose=True): """See class docstring""" self._address = address self.verbose = verbose @@ -126,6 +132,8 @@ def __init__(self, address=config._visa_address, timeout=config._timeout, verbos # For TCP/IP socket connections enable the read Termination Character, or reads will timeout if self._inst.resource_name.endswith('SOCKET'): self._inst.read_termination = '\n' + if get_errors_on_init: + self.get_full_error_queue(verbose=True) # Clear the status data structures, the device-defined error queue, and the Request-for-OPC flag self.write('*CLS') # Make sure WORD and BYTE data is transeferred as signed ints and lease significant bit first @@ -193,11 +201,13 @@ def query(self, command, action=""): else: msg = f"query '{command}'" print(f"\nVisaError: {err}\n When trying {msg}.") - print(f" Have you checked that the timeout (currently {self.timeout:,d} ms) is sufficently long?") + print(f" Have you checked that the timeout (currently " + f"{self.timeout:,d} ms) is sufficently long?") try: - print(f"Latest error from the oscilloscope: '{self.get_error()}'\n") + self.get_full_error_queue(verbose=True) + print("") except Exception: - print("Could not retrieve error from the oscilloscope\n") + print("Could not retrieve errors from the oscilloscope\n") raise def close(self, set_running=True): @@ -216,7 +226,9 @@ def close(self, set_running=True): _log.debug(f"Closed connection to '{self._id}'") def get_error(self): - """Get the latest error + """Get the first error in the error queue, a FIFO queue of max length 30. + The queue is reset when ``*CLS`` is written to the scope, which happens + in __init__(). Returns ------- @@ -226,13 +238,34 @@ def get_error(self): # Do not use self.query here as that can lead to infinite nesting! return self._inst.query(":SYSTem:ERRor?").strip() + def get_full_error_queue(self, verbose=True): + """All the latest errors from the oscilloscope, upto 30 errors + (and store to the attribute ``errors``)""" + self.errors = [] + for i in range(30): + err = self.get_error() + if err[:2] == "+0": # no error + # stop querying + break + else: + # store the error + self.errors.append(e) + if verbose: + if not self.errors: + print("Error queue empty") + else: + print("Latest errors from the oscilloscope (FIFO queue, upto 30 errors)") + for i, err in enumerate(self.errors): + print(f"{i:>2}: {err}") + return self.errors + def run(self): - """Set the ocilloscope to running mode.""" - self.write(':RUN') + """Set the oscilloscope to running mode.""" + self.write(":RUN") def stop(self): """Stop the oscilloscope.""" - self.write(':STOP') + self.write(":STOP") def is_running(self): """Determine if the oscilloscope is running. @@ -243,7 +276,7 @@ def is_running(self): ``True`` if running, ``False`` otherwise """ # The third bit of the operation register is 1 if the instrument is running - reg = int(self.query(':OPERegister:CONDition?')) + reg = int(self.query(":OPERegister:CONDition?")) return (reg & 8) == 8 @property From f18113d776fc0e9d2734a85912ce89369aa77660 Mon Sep 17 00:00:00 2001 From: asvela at 0011 Date: Mon, 11 Jan 2021 19:49:43 +0100 Subject: [PATCH 25/52] (structural) splitting up the auxiliary module, oscacq -> oscilloscope, traceio -> fileio --- README.rst | 4 +- docs/changelog.rst | 17 +- docs/contents/auxiliary.rst | 6 - docs/contents/commandline-programmes.rst | 2 +- docs/contents/dataprocessing.rst | 32 ++-- .../{osc-class.rst => oscilloscope.rst} | 2 +- docs/contents/overview.rst | 6 +- docs/contents/usage.rst | 6 +- docs/contents/visa_utils.rst | 6 + docs/index.rst | 4 +- keyoscacquire/__init__.py | 13 +- keyoscacquire/__main__.py | 8 +- keyoscacquire/dataprocessing.py | 141 ++++++++++++++ keyoscacquire/{traceio.py => fileio.py} | 104 +++++++---- keyoscacquire/installed_cli_programmes.py | 85 +++++---- keyoscacquire/{oscacq.py => oscilloscope.py} | 176 +++--------------- keyoscacquire/programmes.py | 21 ++- keyoscacquire/{auxiliary.py => visa_utils.py} | 34 +--- tests/format_comparison.py | 2 +- 19 files changed, 341 insertions(+), 328 deletions(-) delete mode 100644 docs/contents/auxiliary.rst rename docs/contents/{osc-class.rst => oscilloscope.rst} (98%) create mode 100644 docs/contents/visa_utils.rst create mode 100644 keyoscacquire/dataprocessing.py rename keyoscacquire/{traceio.py => fileio.py} (88%) rename keyoscacquire/{oscacq.py => oscilloscope.py} (85%) rename keyoscacquire/{auxiliary.py => visa_utils.py} (75%) diff --git a/README.rst b/README.rst index 1afbc46..0ab0cd1 100644 --- a/README.rst +++ b/README.rst @@ -54,7 +54,7 @@ information about the API. As an example of API usage/use in the Python console:: - >>> import keyoscacquire.oscacq as koa + >>> import keyoscacquire koa >>> scope = koa.Oscilloscope(address='USB0::1234::1234::MY1234567::INSTR') Connected to: AGILENT TECHNOLOGIES @@ -88,7 +88,7 @@ array which columns contain the data from the active channels listed in The trace can be easily loaded from disk to a Pandas dataframe with:: - >>> df, metadata = koa.traceio.load_trace("data") + >>> df, metadata = koa.fileio.load_trace("data") >>> df.head() time 1 2 4 0 -0.005 -0.004853 -0.005248 -0.005248 diff --git a/docs/changelog.rst b/docs/changelog.rst index 7b71f3f..acfb188 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -39,12 +39,12 @@ v4.0.0 (2020-12) set the to the maximum number of points available, and the number of points can be queried. - - New ``keyoscacquire.traceio.load_trace()`` function for loading saved a trace - from disk to pandas dataframe or numpy array - - - Moved save and plot functions to ``keyoscacquire.traceio``, but are imported + - Moved save and plot functions to ``keyoscacquire.fileio``, but are imported in ``oscacq`` to keep compatibility + - New ``keyoscacquire.fileio.load_trace()`` function for loading saved a trace + from disk to pandas dataframe or numpy array + - ``Oscilloscope.query()`` will now try to read the error from the instrument if pyvisa fails @@ -101,10 +101,11 @@ v4.0.0 (2020-12) - *No compatibility*: Moved functions and attributes - * ``interpret_visa_id()`` from ``oscacq`` to ``auxiliary`` - * ``check_file()`` from ``oscacq`` to ``auxiliary`` - * ``_screen_colors`` from ``oscacq`` to ``auxiliary`` - * ``_supported_series`` from ``oscacq`` to ``auxiliary`` + * ``check_file()`` from ``oscacq`` to ``fileio`` + * ``interpret_visa_id()`` from ``oscacq`` to ``visa_utils`` + * ``process_data()`` (as well as ``_process_data_ascii`` and + ``_process_data_binary``) from ``oscacq`` to ``dataprocessing`` + * ``_screen_colors`` from ``oscacq`` to ``fileio`` - *No compatibility*: Some functions no longer take ``sources`` and ``sourcesstring`` as arguments, rather ``Oscilloscope._sources`` must be set by diff --git a/docs/contents/auxiliary.rst b/docs/contents/auxiliary.rst deleted file mode 100644 index 9b639bd..0000000 --- a/docs/contents/auxiliary.rst +++ /dev/null @@ -1,6 +0,0 @@ -Auxiliary module :mod:`~keyoscacquire.auxiliary` -************************************************* - -.. automodule:: keyoscacquire.auxiliary - :members: - :private-members: diff --git a/docs/contents/commandline-programmes.rst b/docs/contents/commandline-programmes.rst index 555da28..ada3e42 100644 --- a/docs/contents/commandline-programmes.rst +++ b/docs/contents/commandline-programmes.rst @@ -21,7 +21,7 @@ The file header in the ascii files saved is:: time, -Where ```` is the :attr:`~keyoscacquire.oscacq.Oscilloscope.id` of the +Where ```` is the :attr:`~keyoscacquire.oscilloscope.Oscilloscope.id` of the oscilloscope, and ```` are the comma separated channels used. For example:: # AGILENT TECHNOLOGIES,DSO-X 2024A,MY1234567,12.34.1234567890 diff --git a/docs/contents/dataprocessing.rst b/docs/contents/dataprocessing.rst index 91d5096..9a4b082 100644 --- a/docs/contents/dataprocessing.rst +++ b/docs/contents/dataprocessing.rst @@ -3,18 +3,18 @@ Data processing, file saving & loading ************************************** -.. py:currentmodule:: keyoscacquire.oscacq - -The :mod:`keyoscacquire.oscacq` module contains a function for processing -the raw data captured with :class:`Oscilloscope`, and :mod:`keyoscacquire.traceio` +The :mod:`keyoscacquire.dataprocessing` module contains a function for processing +the raw data captured with :class:`Oscilloscope`, and :mod:`keyoscacquire.fileio` for saving the processed data to files and plots. -Data processing ---------------- +Data processing (:mod:`keyoscacquire.dataprocessing`) +----------------------------------------------------- + +.. py:currentmodule:: keyoscacquire.dataprocessing The output from the :func:`Oscilloscope.capture_and_read` function is processed by :func:`process_data`, a wrapper function that sends the data to the -respective binary or ascii processing function. +respective binary or ascii processing functions. This function is kept outside the Oscilloscope class as one might want to post-process data after capturing it. @@ -22,16 +22,16 @@ post-process data after capturing it. .. autofunction:: process_data -File saving and loading (:mod:`keyoscacquire.traceio`) +File saving and loading (:mod:`keyoscacquire.fileio`) ------------------------------------------------------ -The Oscilloscope class has the method :meth:`Oscilloscope.save_trace()` for -saving the most recently captured trace to disk. This method relies on the -``traceio`` module. +The Oscilloscope class has the method :meth:`keyoscacquire.Oscilloscope.save_trace()` +for saving the most recently captured trace to disk. This method relies on the +``fileio`` module. -.. automodule:: keyoscacquire.traceio +.. automodule:: keyoscacquire.fileio -.. autofunction:: keyoscacquire.traceio.save_trace -.. autofunction:: keyoscacquire.traceio.plot_trace -.. autofunction:: keyoscacquire.traceio.load_trace -.. autofunction:: keyoscacquire.traceio.load_header +.. autofunction:: keyoscacquire.fileio.save_trace +.. autofunction:: keyoscacquire.fileio.plot_trace +.. autofunction:: keyoscacquire.fileio.load_trace +.. autofunction:: keyoscacquire.fileio.load_header diff --git a/docs/contents/osc-class.rst b/docs/contents/oscilloscope.rst similarity index 98% rename from docs/contents/osc-class.rst rename to docs/contents/oscilloscope.rst index 46e13e6..f35d2a9 100644 --- a/docs/contents/osc-class.rst +++ b/docs/contents/oscilloscope.rst @@ -7,7 +7,7 @@ Instrument communication: The Oscilloscope class Oscilloscope API ================ -.. py:currentmodule:: keyoscacquire.oscacq +.. py:currentmodule:: keyoscacquire.oscilloscope .. autoclass:: Oscilloscope diff --git a/docs/contents/overview.rst b/docs/contents/overview.rst index fb9c788..5ac8c56 100644 --- a/docs/contents/overview.rst +++ b/docs/contents/overview.rst @@ -2,14 +2,14 @@ Overview and getting started **************************** -The code is structured as a module :mod:`keyoscacquire.oscacq` containing the +The code is structured as a module :mod:`keyoscacquire.oscilloscope` containing the engine doing the `PyVISA `_ -interfacing in a class :class:`~keyoscacquire.oscacq.Oscilloscope`, and +interfacing in a class :class:`~keyoscacquire.oscilloscope.Oscilloscope`, and support functions for data processing. Programmes are located in :mod:`keyoscacquire.programmes`, and the same programmes can be run directly from the command line as they are installed in the Python path, see :ref:`cli-programmes`. Default options are found in :mod:`keyoscacquire.config`, -and the :mod:`keyoscacquire.traceio` provides functions for plotting, saving, +and the :mod:`keyoscacquire.fileio` provides functions for plotting, saving, and loading traces from disk. diff --git a/docs/contents/usage.rst b/docs/contents/usage.rst index 18550e0..8f6695b 100644 --- a/docs/contents/usage.rst +++ b/docs/contents/usage.rst @@ -89,8 +89,8 @@ for from the ocilloscope. The keyoscacquire package supports all three formats and does the conversion for the integer transfer types, i.e. the output files will be ASCII format anyway, it is simply a question of how the data is transferred to and processed on the computer -(see :func:`~keyoscacquire.oscacq.Oscilloscope.capture_and_read` and -:func:`~keyoscacquire.oscacq.process_data`). +(see :func:`~keyoscacquire.oscilloscope.Oscilloscope.capture_and_read` and +:func:`~keyoscacquire.dataprocessing.process_data`). The 16-bit values format is approximately 10x faster than ascii and gives the same vertical resolution. 8-bit has significantly lower vertical resolution @@ -98,7 +98,7 @@ than the two others, but gives an even higher speed-up. The default waveform type can be set in with :const:`~keyoscacquire.config._waveform_format`, see :ref:`default-options`, -or using the API :attr:`~keyoscacquire.oscacq.Oscilloscope.wav_format`. +or using the API :attr:`~keyoscacquire.oscilloscope.Oscilloscope.wav_format`. Using the API diff --git a/docs/contents/visa_utils.rst b/docs/contents/visa_utils.rst new file mode 100644 index 0000000..2530b67 --- /dev/null +++ b/docs/contents/visa_utils.rst @@ -0,0 +1,6 @@ +Visa-related support module :mod:`~keyoscacquire.visa_utils` +************************************************************ + +.. automodule:: keyoscacquire.visa_utils + :members: + :private-members: diff --git a/docs/index.rst b/docs/index.rst index fdf91d6..101967c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -20,11 +20,11 @@ Table of contents :maxdepth: 3 :caption: Reference - contents/osc-class + contents/oscillocope contents/dataprocessing contents/programmes contents/config - contents/auxiliary + contents/visa_utils .. toctree:: diff --git a/keyoscacquire/__init__.py b/keyoscacquire/__init__.py index 5b882dc..ed4dc8e 100644 --- a/keyoscacquire/__init__.py +++ b/keyoscacquire/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """ -Andreas Svela // 2019 +Andreas Svela // 2019-2021 """ @@ -15,12 +15,11 @@ import logging _log = logging.getLogger(__name__) -import keyoscacquire.oscacq as oscacq -import keyoscacquire.traceio as traceio +import keyoscacquire.oscilloscope as oscilloscope +import keyoscacquire.fileio as fileio import keyoscacquire.config as config import keyoscacquire.programmes as programmes -import keyoscacquire.auxiliary as auxiliary +import keyoscacquire.visa_utils as visa_utils -from keyoscacquire.oscacq import Oscilloscope -from keyoscacquire.traceio import save_trace, load_trace -from keyoscacquire.auxiliary import _screen_colors +from .oscilloscope import Oscilloscope, _supported_series +from .fileio import save_trace, load_trace, _screen_colors diff --git a/keyoscacquire/__main__.py b/keyoscacquire/__main__.py index ab92102..a3c827a 100644 --- a/keyoscacquire/__main__.py +++ b/keyoscacquire/__main__.py @@ -1,12 +1,10 @@ # -*- coding: utf-8 -*- """ -When module the executed with 'python -m keyoscacquire' +When the module is executed with 'python -m keyoscacquire': obtain and save a single trace. - -Andreas Svela // 2019 """ -import keyoscacquire.programmes as acqprog +import keyoscacquire.programmes as programmes if __name__ == "__main__": - acqprog.get_single_trace() + programmes.get_single_trace() diff --git a/keyoscacquire/dataprocessing.py b/keyoscacquire/dataprocessing.py new file mode 100644 index 0000000..68cdc98 --- /dev/null +++ b/keyoscacquire/dataprocessing.py @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- +""" +This module provides functions for processing the data captured from the +oscillscope to time and voltage values +""" + +import logging +import numpy as np + +_log = logging.getLogger(__name__) + + +def process_data(raw, metadata, wav_format, verbose_acquistion=True): + """Wrapper function for choosing the correct _process_data function + according to :attr:`wav_format` for the data obtained from + :func:`Oscilloscope.capture_and_read` + + Parameters + ---------- + raw : ~numpy.ndarray or str + From :func:`~Oscilloscope.capture_and_read`: Raw data, type depending + on :attr:`wav_format` + metadata : list or tuple + From :func:`~Oscilloscope.capture_and_read`: List of preambles or + tuple of preamble and model series depending on :attr:`wav_format`. + See :ref:`preamble`. + wav_format : {``'WORD'``, ``'BYTE'``, ``'ASCii'``} + Specify what waveform type was used for acquiring to choose the correct + processing function. + verbose_acquistion : bool + True prints the number of points captured per channel + + Returns + ------- + time : :class:`~numpy.ndarray` + Time axis for the measurement + y : :class:`~numpy.ndarray` + Voltage values, each row represents one channel + + Raises + ------ + ValueError + If ``wav_format`` is not {'BYTE', 'WORD', 'ASCii'} + + See also + -------- + :func:`Oscilloscope.capture_and_read` + """ + if wav_format[:3] in ['WOR', 'BYT']: + processing_fn = _process_data_binary + elif wav_format[:3] == 'ASC': + processing_fn = _process_data_ascii + else: + raise ValueError("Could not process data, waveform format \'{}\' is unknown.".format(wav_format)) + return processing_fn(raw, metadata, verbose_acquistion) + + +def _process_data_binary(raw, preambles, verbose_acquistion=True): + """Process raw 8/16-bit data to time values and y voltage values as received + from :func:`Oscilloscope.capture_and_read_binary`. + + Parameters + ---------- + raw : ~numpy.ndarray + From :func:`~Oscilloscope.capture_and_read_binary`: An ndarray of ints + that is converted to voltage values using the preamble. + preambles : list of str + From :func:`~Oscilloscope.capture_and_read_binary`: List of preamble + metadata for each channel (list of comma separated ascii values, + see :ref:`preamble`) + verbose_acquistion : bool + True prints the number of points captured per channel + + Returns + ------- + time : :class:`~numpy.ndarray` + Time axis for the measurement + y : :class:`~numpy.ndarray` + Voltage values, each row represents one channel + """ + # Pick one preamble and use for calculating the time values (same for all channels) + preamble = preambles[0].split(',') # values separated by commas + num_samples = int(float(preamble[2])) + xIncr, xOrig, xRef = float(preamble[4]), float(preamble[5]), float(preamble[6]) + time = np.array([(np.arange(num_samples)-xRef)*xIncr + xOrig]) # compute x-values + time = time.T # make x values vertical + _log.debug(f"Points captured per channel: {num_samples:,d}") + if verbose_acquistion: + print(f"Points captured per channel: {num_samples:,d}") + y = np.empty((len(raw), num_samples)) + for i, data in enumerate(raw): # process each channel individually + preamble = preambles[i].split(',') + yIncr, yOrig, yRef = float(preamble[7]), float(preamble[8]), float(preamble[9]) + y[i,:] = (data-yRef)*yIncr + yOrig + y = y.T # convert y to np array and transpose for vertical channel columns in csv file + return time, y + +def _process_data_ascii(raw, metadata, verbose_acquistion=True): + """Process raw comma separated ascii data to time values and y voltage + values as received from :func:`Oscilloscope.capture_and_read_ascii` + + Parameters + ---------- + raw : str + From :func:`~Oscilloscope.capture_and_read_ascii`: A string containing + a block header and comma separated ascii values + metadata : tuple + From :func:`~Oscilloscope.capture_and_read_ascii`: Tuple of the + preamble for one of the channels to calculate time axis (same for + all channels) and the model series. See :ref:`preamble`. + verbose_acquistion : bool + True prints the number of points captured per channel + + Returns + ------- + time : :class:`~numpy.ndarray` + Time axis for the measurement + y : :class:`~numpy.ndarray` + Voltage values, each row represents one channel + """ + preamble, model_series = metadata + preamble = preamble.split(',') # Values separated by commas + num_samples = int(float(preamble[2])) + xIncr, xOrig, xRef = float(preamble[4]), float(preamble[5]), float(preamble[6]) + # Compute time axis and wrap in extra [] to make it 2D + time = np.array([(np.arange(num_samples)-xRef)*xIncr + xOrig]) + time = time.T # Make list vertical + _log.debug(f"Points captured per channel: {num_samples:,d}") + if verbose_acquistion: + print(f"Points captured per channel: {num_samples:,d}") + y = [] + for data in raw: + if model_series in ['2000']: + data = data.split(data[:10])[1] # remove first 10 characters (IEEE block header) + elif model_series in ['9000']: + data = data.strip().strip(",") # remove newline character at the end of the string + data = data.split(',') # samples separated by commas + data = np.array([float(sample) for sample in data]) + y.append(data) # add ascii data for this channel to y array + y = np.transpose(np.array(y)) + return time, y diff --git a/keyoscacquire/traceio.py b/keyoscacquire/fileio.py similarity index 88% rename from keyoscacquire/traceio.py rename to keyoscacquire/fileio.py index e6590d2..0dc88e5 100644 --- a/keyoscacquire/traceio.py +++ b/keyoscacquire/fileio.py @@ -14,11 +14,76 @@ import matplotlib.pyplot as plt import keyoscacquire.config as config -import keyoscacquire.auxiliary as auxiliary + _log = logging.getLogger(__name__) -## Trace saving and plotting ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## +#: Keysight colour map for the channels +_screen_colors = {1:'C1', 2:'C2', 3:'C0', 4:'C3'} + + +def check_file(fname, ext=config._filetype, num=""): + """Checking if file ``fname+num+ext`` exists. If it does, the user is + prompted for a string to append to fname until a unique fname is found. + + Parameters + ---------- + fname : str + Base filename to test + ext : str, default :data:`~keyoscacquire.config._filetype` + File extension + num : str, default "" + Filename suffix that is tested for, but the appended part to the fname + will be placed before it,and the suffix will not be part of the + returned fname + + Returns + ------- + fname : str + New fname base + """ + while os.path.exists(fname+num+ext): + append = input(f"File '{fname+num+ext}' exists! Append to filename '{fname}' before saving: ") + fname += append + return fname + + +## Trace plotting ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## + +def plot_trace(time, y, channels, fname="", showplot=config._show_plot, + savepng=config._export_png): + """Plots the trace with oscilloscope channel screen colours according to + the Keysight colourmap and saves as a png. + + .. Caution:: No filename check for the saved plot, can overwrite + existing png files. + + Parameters + ---------- + time : ~numpy.ndarray + Time axis for the measurement + y : ~numpy.ndarray + Voltage values, same sequence as channel_nums + channels : list of ints + list of the channels obtained, example [1, 3] + fname : str, default ``""`` + Filename of possible exported png + show : bool, default :data:`~keyoscacquire.config._show_plot` + True shows the plot (must be closed before the programme proceeds) + savepng : bool, default :data:`~keyoscacquire.config._export_png` + ``True`` exports the plot to ``fname``.png + """ + fig, ax = plt.subplots() + for i, vals in enumerate(np.transpose(y)): # for each channel + ax.plot(time, vals, color=_screen_colors[channels[i]]) + if savepng: + fig.savefig(fname+".png", bbox_inches='tight') + if showplot: + plt.show(fig) + plt.close(fig) + + +## Trace saving ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## def save_trace(fname, time, y, fileheader="", ext=config._filetype, print_filename=True, nowarn=False): @@ -82,44 +147,11 @@ def save_trace_npy(fname, time, y, print_filename=True, **kwargs): save_trace(fname, time, y, ext=".npy", nowarn=True, print_filename=print_filename) -def plot_trace(time, y, channels, fname="", showplot=config._show_plot, - savepng=config._export_png): - """Plots the trace with oscilloscope channel screen colours according to - the Keysight colourmap and saves as a png. - - .. Caution:: No filename check for the saved plot, can overwrite - existing png files. - - Parameters - ---------- - time : ~numpy.ndarray - Time axis for the measurement - y : ~numpy.ndarray - Voltage values, same sequence as channel_nums - channels : list of ints - list of the channels obtained, example [1, 3] - fname : str, default ``""`` - Filename of possible exported png - show : bool, default :data:`~keyoscacquire.config._show_plot` - True shows the plot (must be closed before the programme proceeds) - savepng : bool, default :data:`~keyoscacquire.config._export_png` - ``True`` exports the plot to ``fname``.png - """ - fig, ax = plt.subplots() - for i, vals in enumerate(np.transpose(y)): # for each channel - ax.plot(time, vals, color=auxiliary._screen_colors[channels[i]]) - if savepng: - fig.savefig(fname+".png", bbox_inches='tight') - if showplot: - plt.show(fig) - plt.close(fig) - - ## Trace loading ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## def load_trace(fname, ext=config._filetype, column_names='auto', skip_lines='auto', return_as_df=True): - """Load a trace saved with keyoscacquire.oscacq.save_file() + """Load a trace saved with keyoscacquire.oscilloscope.save_file() What is returned depends on the format of the file (.npy files contain no headers), and if a dataframe format is chosen for the return. diff --git a/keyoscacquire/installed_cli_programmes.py b/keyoscacquire/installed_cli_programmes.py index 9d59367..d9c796d 100644 --- a/keyoscacquire/installed_cli_programmes.py +++ b/keyoscacquire/installed_cli_programmes.py @@ -14,13 +14,12 @@ Optional argument from the command line: string setting the base filename of the output files. Change _visa_address in keyoscacquire.config to the desired instrument's address. -Andreas Svela // 2019 """ import sys import argparse -import keyoscacquire.programmes as acqprog +import keyoscacquire.programmes as programmes import keyoscacquire.config as config ##============================================================================## @@ -37,6 +36,7 @@ points_help = f"Use 0 to get the maximum number of points, or set a specific number (the scope might change it slightly). Defaults to '{config._num_points}." delim_help = f"Delimiter used between filename and filenumber (before filetype). Defaults to '{config._file_delimiter}'." + def standard_arguments(parser): connection_gr = parser.add_argument_group('Connection settings') connection_gr.add_argument('-v', '--visa_address', @@ -61,62 +61,63 @@ def standard_arguments(parser): def connect_each_time_cli(): """Function installed on the command line: Obtains and stores multiple traces, connecting to the oscilloscope each time.""" - parser = argparse.ArgumentParser(description=acqprog.get_traces_connect_each_time_loop.__doc__) + parser = argparse.ArgumentParser(description=programmes.get_traces_connect_each_time_loop.__doc__) trans_gr = standard_arguments(parser) trans_gr.add_argument('--file_delimiter', nargs='?', help=delim_help, default=config._file_delimiter) args = parser.parse_args() # Convert channels arg to ints if args.channels is not None: args.channels = [int(c) for c in args.channels] - acqprog.get_traces_connect_each_time_loop(fname=args.filename, - address=args.visa_address, - timeout=args.timeout, - wav_format=args.wav_format, - channels=args.channels, - acq_type=args.acq_type, - num_points=args.num_points, - file_delim=args.file_delimiter) + programmes.get_traces_connect_each_time_loop(fname=args.filename, + address=args.visa_address, + timeout=args.timeout, + wav_format=args.wav_format, + channels=args.channels, + acq_type=args.acq_type, + num_points=args.num_points, + file_delim=args.file_delimiter) def single_connection_cli(): """Function installed on the command line: Obtains and stores multiple traces, keeping a the same connection to the oscilloscope open all the time.""" - parser = argparse.ArgumentParser(description=acqprog.get_traces_single_connection_loop.__doc__) + parser = argparse.ArgumentParser(description=programmes.get_traces_single_connection_loop.__doc__) trans_gr = standard_arguments(parser) trans_gr.add_argument('--file_delimiter', nargs='?', help=delim_help, default=config._file_delimiter) args = parser.parse_args() # Convert channels arg to ints if args.channels is not None: args.channels = [int(c) for c in args.channels] - acqprog.get_traces_single_connection_loop(fname=args.filename, - address=args.visa_address, - timeout=args.timeout, - wav_format=args.wav_format, - channels=args.channels, - acq_type=args.acq_type, - num_points=args.num_points, - file_delim=args.file_delimiter) + programmes.get_traces_single_connection_loop(fname=args.filename, + address=args.visa_address, + timeout=args.timeout, + wav_format=args.wav_format, + channels=args.channels, + acq_type=args.acq_type, + num_points=args.num_points, + file_delim=args.file_delimiter) def single_trace_cli(): """Function installed on the command line: Obtains and stores a single trace.""" - parser = argparse.ArgumentParser(description=acqprog.get_single_trace.__doc__) + parser = argparse.ArgumentParser(description=programmes.get_single_trace.__doc__) standard_arguments(parser) args = parser.parse_args() # Convert channels arg to ints if args.channels is not None: args.channels = [int(c) for c in args.channels] - acqprog.get_single_trace(fname=args.filename, - address=args.visa_address, - timeout=args.timeout, - wav_format=args.wav_format, - channels=args.channels, - acq_type=args.acq_type, - num_points=args.num_points) + programmes.get_single_trace(fname=args.filename, + address=args.visa_address, + timeout=args.timeout, + wav_format=args.wav_format, + channels=args.channels, + acq_type=args.acq_type, + num_points=args.num_points) + def num_traces_cli(): """Function installed on the command line: Obtains and stores a single trace.""" - parser = argparse.ArgumentParser(description=acqprog.get_num_traces.__doc__) + parser = argparse.ArgumentParser(description=programmes.get_num_traces.__doc__) # postitional arg parser.add_argument('num', help='The number of successive traces to obtain.', type=int) # optional args @@ -126,26 +127,28 @@ def num_traces_cli(): # Convert channels arg to ints if args.channels is not None: args.channels = [int(c) for c in args.channels] - acqprog.get_num_traces(num=args.num, - fname=args.filename, - address=args.visa_address, - timeout=args.timeout, - wav_format=args.wav_format, - channels=args.channels, - acq_type=args.acq_type, - num_points=args.num_points) + programmes.get_num_traces(num=args.num, + fname=args.filename, + address=args.visa_address, + timeout=args.timeout, + wav_format=args.wav_format, + channels=args.channels, + acq_type=args.acq_type, + num_points=args.num_points) + def list_visa_devices_cli(): """Function installed on the command line: Lists VISA devices""" - parser = argparse.ArgumentParser(description=acqprog.list_visa_devices.__doc__) + parser = argparse.ArgumentParser(description=programmes.list_visa_devices.__doc__) parser.add_argument('-n', action="store_false", help=("If this flag is set, the programme will not query " "the instruments for their IDNs.")) args = parser.parse_args() - acqprog.list_visa_devices(ask_idn=args.n) + programmes.list_visa_devices(ask_idn=args.n) + def path_of_config_cli(): """Function installed on the command line: Prints the full path of the config module""" - parser = argparse.ArgumentParser(description=acqprog.path_of_config.__doc__) + parser = argparse.ArgumentParser(description=programmes.path_of_config.__doc__) args = parser.parse_args() - acqprog.path_of_config() + programmes.path_of_config() diff --git a/keyoscacquire/oscacq.py b/keyoscacquire/oscilloscope.py similarity index 85% rename from keyoscacquire/oscacq.py rename to keyoscacquire/oscilloscope.py index a4ac50c..c6d29e1 100644 --- a/keyoscacquire/oscacq.py +++ b/keyoscacquire/oscilloscope.py @@ -20,16 +20,17 @@ # local file with default options: import keyoscacquire.config as config -import keyoscacquire.auxiliary as auxiliary -import keyoscacquire.traceio as traceio +import keyoscacquire.visa_utils as visa_utils +import keyoscacquire.fileio as fileio +import keyoscacquire.dataprocessing as dataprocessing -from keyoscacquire.auxiliary import _screen_colors # for backwards compatibility (but rather use the Oscilloscope methods) -from keyoscacquire.traceio import save_trace, save_trace_npy, plot_trace +from .fileio import save_trace, save_trace_npy _log = logging.getLogger(__name__) - +#: Supported Keysight DSO/MSO InfiniiVision series +_supported_series = ['1000', '2000', '3000', '4000', '6000'] #: Datatype is ``'h'`` for 16 bit signed int (``WORD``), ``'b'`` for 8 bit signed bit (``BYTE``). #: Same naming as for structs `docs.python.org/3/library/struct.html#format-characters` _datatypes = {'BYT':'b', 'WOR':'h', 'BYTE':'b', 'WORD':'h'} @@ -142,7 +143,7 @@ def __init__(self, address=config._visa_address, timeout=config._timeout, # Get information about the connected device self._id = self.query('*IDN?') try: - maker, self._model, self._serial, _, self._model_series = auxiliary.interpret_visa_id(self._id) + maker, self._model, self._serial, _, self._model_series = visa_utils.interpret_visa_id(self._id) if self.verbose: print(f"Connected to:") print(f" {maker}") @@ -151,7 +152,7 @@ def __init__(self, address=config._visa_address, timeout=config._timeout, if self.verbose: print(f"Connected to '{self._id}'") print("(!) Failed to intepret the VISA IDN string") - if not self._model_series in auxiliary._supported_series: + if not self._model_series in _supported_series: print(f"(!) WARNING: This model ({self._model}) is not yet fully supported by keyoscacquire,") print( " but might work to some extent. keyoscacquire supports Keysight's") print( " InfiniiVision X-series oscilloscopes.") @@ -200,7 +201,7 @@ def query(self, command, action=""): msg = f"{action} (command '{command}')" else: msg = f"query '{command}'" - print(f"\nVisaError: {err}\n When trying {msg}.") + print(f"\n\nVisaError: {err}\n When trying {msg}.") print(f" Have you checked that the timeout (currently " f"{self.timeout:,d} ms) is sufficently long?") try: @@ -601,7 +602,7 @@ def capture_and_read(self, set_running=True): The parameters are provided by :func:`set_channels_for_capture`. The populated attributes raw and metadata should be processed - by :func:`process_data`. + by :func:`dataprocessing.process_data`. raw : :class:`~numpy.ndarray` An ndarray of ints that can be converted to voltage values using the preamble. @@ -620,7 +621,7 @@ def capture_and_read(self, set_running=True): See also -------- - :func:`process_data` + :func:`dataprocessing.process_data` """ wav_format = self.wav_format if self.verbose_acquistion: @@ -656,11 +657,11 @@ def _read_binary(self, datatype='standard'): when waveform format is ``'WORD'`` or ``'BYTE'``. The parameters are provided by :func:`set_channels_for_capture`. - The output should be processed by :func:`process_data_binary`. + The output should be processed by :func:`dataprocessing._process_data_binary`. Populates the following attributes raw : :class:`~numpy.ndarray` - Raw data to be processed by :func:`process_data_binary`. + Raw data to be processed by :func:`dataprocessing._process_data_binary`. An ndarray of ints that can be converted to voltage values using the preamble. metadata : list of str List of preamble metadata (comma separated ascii values) for each channel @@ -672,7 +673,7 @@ def _read_binary(self, datatype='standard'): on :attr:`wav_format`. Datatype is ``'h'`` for 16 bit signed int (``'WORD'``), for 8 bit signed bit (``'BYTE'``) (same naming as for structs, `https://docs.python.org/3/library/struct.html#format-characters`). - ``'standard'`` will evaluate :data:`oscacq._datatypes[self.wav_format]` + ``'standard'`` will evaluate :data:`oscilloscope._datatypes[self.wav_format]` to automatically choose according to the waveform format set_running : bool, default ``True`` ``True`` leaves oscilloscope running after data capture @@ -704,11 +705,11 @@ def _read_ascii(self): when waveform format is ASCii. The parameters are provided by :func:`set_channels_for_capture`. - The output should be processed by :func:`process_data_ascii`. + The output should be processed by :func:`dataprocessing._process_data_ascii`. Populates the following attributes raw : str - Raw data to be processed by :func:`process_data_ascii`. + Raw data to be processed by :func:`dataprocessing._process_data_ascii`. The raw data is a list of one IEEE block per channel with a head and then comma separated ascii values. metadata : tuple of str @@ -768,8 +769,8 @@ def get_trace(self, channels=None, verbose_acquistion=None): self.set_channels_for_capture(channels=channels) # Capture, read and process data self.capture_and_read() - self._time, self._values = process_data(self._raw, self._metadata, self.wav_format, - verbose_acquistion=self.verbose_acquistion) + self._time, self._values = dataprocessing.process_data(self._raw, self._metadata, self.wav_format, + verbose_acquistion=self.verbose_acquistion) return self._time, self._values, self._capture_channels def set_options_get_trace(self, channels=None, wav_format=None, acq_type=None, @@ -958,11 +959,11 @@ def save_trace(self, fname=None, ext=None, additional_header_info=None, if self.fname[-4:] in ['.npy', '.csv']: self.ext = self.fname[-4:] self.fname = self.fname[:-4] - self.fname = auxiliary.check_file(self.fname, self.ext) - traceio.plot_trace(self._time, self._values, self._capture_channels, fname=self.fname, + self.fname = fileio.check_file(self.fname, self.ext) + fileio.plot_trace(self._time, self._values, self._capture_channels, fname=self.fname, showplot=self.showplot, savepng=self.savepng) head = self.generate_file_header(additional_line=additional_header_info) - traceio.save_trace(self.fname, self._time, self._values, fileheader=head, ext=self.ext, + fileio.save_trace(self.fname, self._time, self._values, fileheader=head, ext=self.ext, print_filename=self.verbose_acquistion, nowarn=nowarn) else: print("(!) No trace has been acquired yet, use get_trace()") @@ -971,145 +972,12 @@ def save_trace(self, fname=None, ext=None, additional_header_info=None, def plot_trace(self): """Plot and show the most recent trace""" if not self._time is None: - traceio.plot_trace(self._time, self._values, self._capture_channels, + fileio.plot_trace(self._time, self._values, self._capture_channels, savepng=False, showplot=True) else: print("(!) No trace has been acquired yet, use get_trace()") _log.info("(!) No trace has been acquired yet, use get_trace()") -##============================================================================## -## DATA PROCESSING ## -##============================================================================## - -def process_data(raw, metadata, wav_format, verbose_acquistion=True): - """Wrapper function for choosing the correct process_data function - according to :attr:`wav_format` for the data obtained from - :func:`Oscilloscope.capture_and_read` - - Parameters - ---------- - raw : ~numpy.ndarray or str - From :func:`~Oscilloscope.capture_and_read`: Raw data, type depending - on :attr:`wav_format` - metadata : list or tuple - From :func:`~Oscilloscope.capture_and_read`: List of preambles or - tuple of preamble and model series depending on :attr:`wav_format`. - See :ref:`preamble`. - wav_format : {``'WORD'``, ``'BYTE'``, ``'ASCii'``} - Specify what waveform type was used for acquiring to choose the correct - processing function. - verbose_acquistion : bool - True prints the number of points captured per channel - - Returns - ------- - time : :class:`~numpy.ndarray` - Time axis for the measurement - y : :class:`~numpy.ndarray` - Voltage values, each row represents one channel - - Raises - ------ - ValueError - If ``wav_format`` is not {'BYTE', 'WORD', 'ASCii'} - - See also - -------- - :func:`Oscilloscope.capture_and_read` - """ - if wav_format[:3] in ['WOR', 'BYT']: - process_fn = _process_data_binary - elif wav_format[:3] == 'ASC': - processing_fn = _process_data_ascii - else: - raise ValueError("Could not process data, waveform format \'{}\' is unknown.".format(wav_format)) - return processing_fn(raw, metadata, verbose_acquistion) - - -def _process_data_binary(raw, preambles, verbose_acquistion=True): - """Process raw 8/16-bit data to time values and y voltage values as received - from :func:`Oscilloscope.capture_and_read_binary`. - - Parameters - ---------- - raw : ~numpy.ndarray - From :func:`~Oscilloscope.capture_and_read_binary`: An ndarray of ints - that is converted to voltage values using the preamble. - preambles : list of str - From :func:`~Oscilloscope.capture_and_read_binary`: List of preamble - metadata for each channel (list of comma separated ascii values, - see :ref:`preamble`) - verbose_acquistion : bool - True prints the number of points captured per channel - - Returns - ------- - time : :class:`~numpy.ndarray` - Time axis for the measurement - y : :class:`~numpy.ndarray` - Voltage values, each row represents one channel - """ - # Pick one preamble and use for calculating the time values (same for all channels) - preamble = preambles[0].split(',') # values separated by commas - num_samples = int(float(preamble[2])) - xIncr, xOrig, xRef = float(preamble[4]), float(preamble[5]), float(preamble[6]) - time = np.array([(np.arange(num_samples)-xRef)*xIncr + xOrig]) # compute x-values - time = time.T # make x values vertical - _log.debug(f"Points captured per channel: {num_samples:,d}") - if verbose_acquistion: - print(f"Points captured per channel: {num_samples:,d}") - y = np.empty((len(raw), num_samples)) - for i, data in enumerate(raw): # process each channel individually - preamble = preambles[i].split(',') - yIncr, yOrig, yRef = float(preamble[7]), float(preamble[8]), float(preamble[9]) - y[i,:] = (data-yRef)*yIncr + yOrig - y = y.T # convert y to np array and transpose for vertical channel columns in csv file - return time, y - -def _process_data_ascii(raw, metadata, verbose_acquistion=True): - """Process raw comma separated ascii data to time values and y voltage - values as received from :func:`Oscilloscope.capture_and_read_ascii` - - Parameters - ---------- - raw : str - From :func:`~Oscilloscope.capture_and_read_ascii`: A string containing - a block header and comma separated ascii values - metadata : tuple - From :func:`~Oscilloscope.capture_and_read_ascii`: Tuple of the - preamble for one of the channels to calculate time axis (same for - all channels) and the model series. See :ref:`preamble`. - verbose_acquistion : bool - True prints the number of points captured per channel - - Returns - ------- - time : :class:`~numpy.ndarray` - Time axis for the measurement - y : :class:`~numpy.ndarray` - Voltage values, each row represents one channel - """ - preamble, model_series = metadata - preamble = preamble.split(',') # Values separated by commas - num_samples = int(float(preamble[2])) - xIncr, xOrig, xRef = float(preamble[4]), float(preamble[5]), float(preamble[6]) - # Compute time axis and wrap in extra [] to make it 2D - time = np.array([(np.arange(num_samples)-xRef)*xIncr + xOrig]) - time = time.T # Make list vertical - _log.debug(f"Points captured per channel: {num_samples:,d}") - if verbose_acquistion: - print(f"Points captured per channel: {num_samples:,d}") - y = [] - for data in raw: - if model_series in ['2000']: - data = data.split(data[:10])[1] # remove first 10 characters (IEEE block header) - elif model_series in ['9000']: - data = data.strip().strip(",") # remove newline character at the end of the string - data = data.split(',') # samples separated by commas - data = np.array([float(sample) for sample in data]) - y.append(data) # add ascii data for this channel to y array - y = np.transpose(np.array(y)) - return time, y ## Module main function ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## diff --git a/keyoscacquire/programmes.py b/keyoscacquire/programmes.py index c6277c7..1c01509 100644 --- a/keyoscacquire/programmes.py +++ b/keyoscacquire/programmes.py @@ -16,12 +16,13 @@ import sys import pyvisa import logging -import keyoscacquire.oscacq as acq import numpy as np from tqdm import tqdm +import keyoscacquire.oscilloscope as oscillocope import keyoscacquire.config as config -import keyoscacquire.auxiliary as auxiliary +import keyoscacquire.fileio as fileio +import keyoscacquire.visa_utils as visa_utils _log = logging.getLogger(__name__) @@ -37,7 +38,7 @@ def list_visa_devices(ask_idn=True): information = [] # Loop through resources to learn more about them for i, address in enumerate(resources): - current_resource_info = auxiliary.obtain_instrument_information(rm, address, i, ask_idn) + current_resource_info = visa_utils.obtain_instrument_information(rm, address, i, ask_idn) information.append(current_resource_info) if ask_idn: # transpose to lists of property @@ -76,7 +77,7 @@ def get_single_trace(fname=config._filename, ext=config._filetype, address=confi channels=None, acq_type=config._acq_type, num_averages=None, p_mode=config._p_mode, num_points=config._num_points): """This programme captures and stores a single trace.""" - with acq.Oscilloscope(address=address, timeout=timeout) as scope: + with oscilloscope.Oscilloscope(address=address, timeout=timeout) as scope: scope.set_options_get_trace_save(fname=fname, ext=ext, wav_format=wav_format, channels=channels, acq_type=acq_type, num_averages=num_averages, p_mode=p_mode, @@ -100,12 +101,12 @@ def get_traces_connect_each_time_loop(fname=config._filename, ext=config._filety """ # Check that file does not exist from before, append to name if it does n = start_num - fname = auxiliary.check_file(fname, ext, num=f"{file_delim}{n}") + fname = fileio.check_file(fname, ext, num=f"{file_delim}{n}") print(f"Running a loop where at every 'enter' oscilloscope traces will be saved as {fname}{ext},") print("where increases by one for each captured trace. Press 'q'+'enter' to quit the programme.") while sys.stdin.read(1) != 'q': # breaks the loop if q+enter is given as input. For any other character (incl. enter) fnum = f"{file_delim}{n}" - with acq.Oscilloscope(address=address, timeout=timeout) as scope: + with oscilloscope.Oscilloscope(address=address, timeout=timeout) as scope: scope.ext = ext scope.set_options_get_trace(wav_format=wav_format, channels=channels, acq_type=acq_type, @@ -131,7 +132,7 @@ def get_traces_single_connection_loop(fname=config._filename, ext=config._filety a trace is captured. The downside is that which channels are being captured cannot be changing thoughout the measurements. """ - with acq.Oscilloscope(address=address, timeout=timeout) as scope: + with oscilloscope.Oscilloscope(address=address, timeout=timeout) as scope: scope.set_acquiring_options(wav_format=wav_format, acq_type=acq_type, num_averages=num_averages, p_mode=p_mode, num_points=num_points) @@ -140,7 +141,7 @@ def get_traces_single_connection_loop(fname=config._filename, ext=config._filety scope.print_acq_settings() # Check that file does not exist from before, append to name if it does n = start_num - fname = auxiliary.check_file(fname, ext, num=f"{file_delim}{n}") + fname = fileio.check_file(fname, ext, num=f"{file_delim}{n}") print(f"Running a loop where at every 'enter' oscilloscope traces will be saved as {fname}{ext},") print("where increases by one for each captured trace. Press 'q'+'enter' to quit the programme.") while sys.stdin.read(1) != 'q': # breaks the loop if q+enter is given as input. For any other character (incl. enter) @@ -160,7 +161,7 @@ def get_num_traces(fname=config._filename, ext=config._filetype, num=1, """This program connects to the oscilloscope, sets options for the acquisition, and captures and stores 'num' traces. """ - with acq.Oscilloscope(address=address, timeout=timeout) as scope: + with oscilloscope.Oscilloscope(address=address, timeout=timeout) as scope: scope.set_acquiring_options(wav_format=wav_format, acq_type=acq_type, num_averages=num_averages, p_mode=p_mode, num_points=num_points) @@ -171,7 +172,7 @@ def get_num_traces(fname=config._filename, ext=config._filetype, num=1, n = start_num fnum = file_delim+str(n) # Check that file does not exist from before, append to name if it does - fname = auxiliary.check_file(fname, ext, num=fnum) + fname = fileio.check_file(fname, ext, num=fnum) for i in tqdm(range(n, n+num)): try: fnum = file_delim+str(i) diff --git a/keyoscacquire/auxiliary.py b/keyoscacquire/visa_utils.py similarity index 75% rename from keyoscacquire/auxiliary.py rename to keyoscacquire/visa_utils.py index e3cd6c4..19b41e3 100644 --- a/keyoscacquire/auxiliary.py +++ b/keyoscacquire/visa_utils.py @@ -1,20 +1,16 @@ # -*- coding: utf-8 -*- """ -Auxiliary functions for the keyoscacquire package +Visa-related auxiliary functions for the keyoscacquire package """ import os import pyvisa import logging -_log = logging.getLogger(__name__) import keyoscacquire.config as config -#: Supported Keysight DSO/MSO InfiniiVision series -_supported_series = ['1000', '2000', '3000', '4000', '6000'] -#: Keysight colour map for the channels -_screen_colors = {1:'C1', 2:'C2', 3:'C0', 4:'C3'} +_log = logging.getLogger(__name__) def interpret_visa_id(idn): """Interprets a VISA ID, including finding a oscilloscope model series @@ -105,29 +101,3 @@ def obtain_instrument_information(resource_manager, address, num, ask_idn=True): f"exception {ex.__class__.__name__}: VISA id returned was '{idn}'") resource_info.extend(["failed to interpret"]*5) return resource_info - - -def check_file(fname, ext=config._filetype, num=""): - """Checking if file ``fname+num+ext`` exists. If it does, the user is - prompted for a string to append to fname until a unique fname is found. - - Parameters - ---------- - fname : str - Base filename to test - ext : str, default :data:`~keyoscacquire.config._filetype` - File extension - num : str, default "" - Filename suffix that is tested for, but the appended part to the fname - will be placed before it,and the suffix will not be part of the - returned fname - - Returns - ------- - fname : str - New fname base - """ - while os.path.exists(fname+num+ext): - append = input(f"File '{fname+num+ext}' exists! Append to filename '{fname}' before saving: ") - fname += append - return fname diff --git a/tests/format_comparison.py b/tests/format_comparison.py index 83c81a1..2cf6e7a 100644 --- a/tests/format_comparison.py +++ b/tests/format_comparison.py @@ -31,7 +31,7 @@ print("\nWaveform format: ", wav_format) scope.wav_format = wav_format scope.capture_and_read(set_running=False) - time, vals = koa.oscacq.process_data(scope._raw, scope._metadata, wav_format, verbose_acquistion=True) + time, vals = koa.dataprocessing.process_data(scope._raw, scope._metadata, wav_format, verbose_acquistion=True) times[0].append(time) values[0].append(vals) From 149aeaaca701747c06143976541db7f7dae63ec8 Mon Sep 17 00:00:00 2001 From: asvela at 0011 Date: Mon, 11 Jan 2021 20:07:16 +0100 Subject: [PATCH 26/52] (oscilloscope) debugging error queue query --- keyoscacquire/oscilloscope.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/keyoscacquire/oscilloscope.py b/keyoscacquire/oscilloscope.py index c6d29e1..f0cecbc 100644 --- a/keyoscacquire/oscilloscope.py +++ b/keyoscacquire/oscilloscope.py @@ -201,14 +201,16 @@ def query(self, command, action=""): msg = f"{action} (command '{command}')" else: msg = f"query '{command}'" - print(f"\n\nVisaError: {err}\n When trying {msg}.") + print(f"\n\nVisaError: {err}\n When trying {msg} (full traceback below).") print(f" Have you checked that the timeout (currently " f"{self.timeout:,d} ms) is sufficently long?") try: self.get_full_error_queue(verbose=True) print("") - except Exception: - print("Could not retrieve errors from the oscilloscope\n") + except Exception as excep: + print("Could not retrieve errors from the oscilloscope:") + print(excep) + print("") raise def close(self, set_running=True): @@ -250,7 +252,7 @@ def get_full_error_queue(self, verbose=True): break else: # store the error - self.errors.append(e) + self.errors.append(err) if verbose: if not self.errors: print("Error queue empty") @@ -692,12 +694,17 @@ def _read_binary(self, datatype='standard'): datatype=datatype, container=np.array)) except pyvisa.Error as err: - print(f"\n\nVisaError: {err}\n When trying to obtain the waveform.") - print(f" Have you checked that the timeout (currently {self.timeout:,d} ms) is sufficently long?") + print(f"\n\nVisaError: {err}\n When trying to obtain the " + f"waveform (full traceback below).") + print(f" Have you checked that the timeout (currently" + f"{self.timeout:,d} ms) is sufficently long?") try: - print(f"Latest error from the oscilloscope: '{self.get_error()}'\n") - except Exception: - print("Could not retrieve error from the oscilloscope") + self.get_full_error_queue(verbose=True) + print("") + except Exception as excep: + print("Could not retrieve errors from the oscilloscope:") + print(excep) + print("") raise def _read_ascii(self): From fda4f491b151e82d64cf901dc86d6a5836a36a03 Mon Sep 17 00:00:00 2001 From: Andreas Svela Date: Mon, 11 Jan 2021 19:18:07 +0000 Subject: [PATCH 27/52] (docs) typo fix and including oscilloscope module name change --- docs/changelog.rst | 7 ++++++- docs/index.rst | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index acfb188..80afbf0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -34,13 +34,17 @@ v4.0.0 (2020-12) *Oscilloscope*, like active channels and acquisition type, but only set default connection and transfer settings + - Changed the name of the module ``oscacq`` to ``oscilloscope`` and moved + functions not within the ``Oscilloscope`` class to other modules, see + details below + - Bugfixes and docfixes for the number of points to be transferred from the instrument (previously ``num_points`` argument, now a property). Zero will set the to the maximum number of points available, and the number of points can be queried. - Moved save and plot functions to ``keyoscacquire.fileio``, but are imported - in ``oscacq`` to keep compatibility + in the ``oscilloscope`` (prev ``oscacq``) module to keep compatibility - New ``keyoscacquire.fileio.load_trace()`` function for loading saved a trace from disk to pandas dataframe or numpy array @@ -85,6 +89,7 @@ v4.0.0 (2020-12) - *No compatibility*: Name changes + * module ``oscacq`` to ``oscilloscope`` * ``Oscilloscope.determine_channels()`` -> ``Oscilloscope.set_channels_for_capture()`` * ``Oscilloscope.acquire_print`` -> ``Oscilloscope.verbose_acquistion`` * ``Oscilloscope.set_acquire_print()`` set ``Oscilloscope.verbose_acquistion`` diff --git a/docs/index.rst b/docs/index.rst index 101967c..5a80beb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -20,7 +20,7 @@ Table of contents :maxdepth: 3 :caption: Reference - contents/oscillocope + contents/oscilloscope contents/dataprocessing contents/programmes contents/config From 34bb56568fb9f263d2abb2dea06b090eabb35b57 Mon Sep 17 00:00:00 2001 From: Andreas Svela Date: Mon, 11 Jan 2021 19:18:37 +0000 Subject: [PATCH 28/52] beta version bump --- keyoscacquire/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keyoscacquire/VERSION b/keyoscacquire/VERSION index c0de572..0c076d6 100644 --- a/keyoscacquire/VERSION +++ b/keyoscacquire/VERSION @@ -1 +1 @@ -4.0.0-beta +4.0.0b2 From 23ce81fe14b020bb5b75723fd54a6e3ec21fa64f Mon Sep 17 00:00:00 2001 From: Andreas Svela Date: Mon, 11 Jan 2021 19:49:24 +0000 Subject: [PATCH 29/52] (docs) fixing linking --- docs/contents/dataprocessing.rst | 14 +++-------- docs/contents/oscilloscope.rst | 9 ++++--- docs/index.rst | 2 +- keyoscacquire/dataprocessing.py | 42 +++++++++++++++++++------------- keyoscacquire/oscilloscope.py | 8 +++--- 5 files changed, 38 insertions(+), 37 deletions(-) diff --git a/docs/contents/dataprocessing.rst b/docs/contents/dataprocessing.rst index 9a4b082..634f249 100644 --- a/docs/contents/dataprocessing.rst +++ b/docs/contents/dataprocessing.rst @@ -4,22 +4,14 @@ Data processing, file saving & loading ************************************** The :mod:`keyoscacquire.dataprocessing` module contains a function for processing -the raw data captured with :class:`Oscilloscope`, and :mod:`keyoscacquire.fileio` +the raw data captured with :class:`oscilloscope.Oscilloscope`, and :mod:`keyoscacquire.fileio` for saving the processed data to files and plots. Data processing (:mod:`keyoscacquire.dataprocessing`) ----------------------------------------------------- -.. py:currentmodule:: keyoscacquire.dataprocessing - -The output from the :func:`Oscilloscope.capture_and_read` function is processed -by :func:`process_data`, a wrapper function that sends the data to the -respective binary or ascii processing functions. - -This function is kept outside the Oscilloscope class as one might want to -post-process data after capturing it. - -.. autofunction:: process_data +.. automodule:: keyoscacquire.dataprocessing + :members: File saving and loading (:mod:`keyoscacquire.fileio`) diff --git a/docs/contents/oscilloscope.rst b/docs/contents/oscilloscope.rst index f35d2a9..9d5e7c9 100644 --- a/docs/contents/oscilloscope.rst +++ b/docs/contents/oscilloscope.rst @@ -1,11 +1,12 @@ .. _osc-class: -Instrument communication: The Oscilloscope class -************************************************ +Instrument communication +************************ +The :mod:`keyoscacquire.oscilloscope` is responsible for the instrument communication. -Oscilloscope API -================ +The Oscilloscope class +====================== .. py:currentmodule:: keyoscacquire.oscilloscope diff --git a/docs/index.rst b/docs/index.rst index 5a80beb..49fd2f7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -18,7 +18,7 @@ Table of contents .. toctree:: :maxdepth: 3 - :caption: Reference + :caption: Reference/API contents/oscilloscope contents/dataprocessing diff --git a/keyoscacquire/dataprocessing.py b/keyoscacquire/dataprocessing.py index 68cdc98..0017e4b 100644 --- a/keyoscacquire/dataprocessing.py +++ b/keyoscacquire/dataprocessing.py @@ -1,7 +1,15 @@ # -*- coding: utf-8 -*- """ This module provides functions for processing the data captured from the -oscillscope to time and voltage values +oscilloscope to time and voltage values + +The output from the :func:`Oscilloscope.capture_and_read` function is processed +by :func:`process_data`, a wrapper function that sends the data to the +respective binary or ascii processing functions. + +This function is kept outside the Oscilloscope class as one might want to +post-process data separately from capturing it. + """ import logging @@ -13,17 +21,17 @@ def process_data(raw, metadata, wav_format, verbose_acquistion=True): """Wrapper function for choosing the correct _process_data function according to :attr:`wav_format` for the data obtained from - :func:`Oscilloscope.capture_and_read` + :func:`~keyoscacquire.oscilloscope.Oscilloscope.capture_and_read` Parameters ---------- raw : ~numpy.ndarray or str - From :func:`~Oscilloscope.capture_and_read`: Raw data, type depending - on :attr:`wav_format` + From :func:`~keyoscacquire.oscilloscope.Oscilloscope.capture_and_read`: + Raw data, type depending on :attr:`wav_format` metadata : list or tuple - From :func:`~Oscilloscope.capture_and_read`: List of preambles or - tuple of preamble and model series depending on :attr:`wav_format`. - See :ref:`preamble`. + From :func:`~keyoscacquire.oscilloscope.Oscilloscope.capture_and_read`: + List of preambles or tuple of preamble and model series depending on + :attr:`wav_format`. See :ref:`preamble`. wav_format : {``'WORD'``, ``'BYTE'``, ``'ASCii'``} Specify what waveform type was used for acquiring to choose the correct processing function. @@ -44,7 +52,7 @@ def process_data(raw, metadata, wav_format, verbose_acquistion=True): See also -------- - :func:`Oscilloscope.capture_and_read` + :func:`keyoscacquire.oscilloscope.Oscilloscope.capture_and_read` """ if wav_format[:3] in ['WOR', 'BYT']: processing_fn = _process_data_binary @@ -62,12 +70,12 @@ def _process_data_binary(raw, preambles, verbose_acquistion=True): Parameters ---------- raw : ~numpy.ndarray - From :func:`~Oscilloscope.capture_and_read_binary`: An ndarray of ints - that is converted to voltage values using the preamble. + From :func:`~keyoscacquire.oscilloscope.Oscilloscope.capture_and_read_binary`: + An ndarray of ints that is converted to voltage values using the preamble. preambles : list of str - From :func:`~Oscilloscope.capture_and_read_binary`: List of preamble - metadata for each channel (list of comma separated ascii values, - see :ref:`preamble`) + From :func:`~keyoscacquire.oscilloscope.Oscilloscope.capture_and_read_binary`: + List of preamble metadata for each channel (list of comma separated + ascii values, see :ref:`preamble`) verbose_acquistion : bool True prints the number of points captured per channel @@ -102,11 +110,11 @@ def _process_data_ascii(raw, metadata, verbose_acquistion=True): Parameters ---------- raw : str - From :func:`~Oscilloscope.capture_and_read_ascii`: A string containing - a block header and comma separated ascii values + From :func:`~keyoscacquire.oscilloscope.Oscilloscope.capture_and_read_ascii`: + A string containing a block header and comma separated ascii values metadata : tuple - From :func:`~Oscilloscope.capture_and_read_ascii`: Tuple of the - preamble for one of the channels to calculate time axis (same for + From :func:`~keyoscacquire.oscilloscope.Oscilloscope.capture_and_read_ascii`: + Tuple of the preamble for one of the channels to calculate time axis (same for all channels) and the model series. See :ref:`preamble`. verbose_acquistion : bool True prints the number of points captured per channel diff --git a/keyoscacquire/oscilloscope.py b/keyoscacquire/oscilloscope.py index f0cecbc..6ffb5ad 100644 --- a/keyoscacquire/oscilloscope.py +++ b/keyoscacquire/oscilloscope.py @@ -604,7 +604,7 @@ def capture_and_read(self, set_running=True): The parameters are provided by :func:`set_channels_for_capture`. The populated attributes raw and metadata should be processed - by :func:`dataprocessing.process_data`. + by :func:`keyoscacquire.dataprocessing.process_data`. raw : :class:`~numpy.ndarray` An ndarray of ints that can be converted to voltage values using the preamble. @@ -623,7 +623,7 @@ def capture_and_read(self, set_running=True): See also -------- - :func:`dataprocessing.process_data` + :func:`keyoscacquire.dataprocessing.process_data` """ wav_format = self.wav_format if self.verbose_acquistion: @@ -659,11 +659,11 @@ def _read_binary(self, datatype='standard'): when waveform format is ``'WORD'`` or ``'BYTE'``. The parameters are provided by :func:`set_channels_for_capture`. - The output should be processed by :func:`dataprocessing._process_data_binary`. + The output should be processed by :func:`keyoscacquire.dataprocessing._process_data_binary`. Populates the following attributes raw : :class:`~numpy.ndarray` - Raw data to be processed by :func:`dataprocessing._process_data_binary`. + Raw data to be processed by :func:`keyoscacquire.dataprocessing._process_data_binary`. An ndarray of ints that can be converted to voltage values using the preamble. metadata : list of str List of preamble metadata (comma separated ascii values) for each channel From 3dfcbc923bf26863dcbea337b896a738ffa112a1 Mon Sep 17 00:00:00 2001 From: Andreas Svela Date: Tue, 12 Jan 2021 11:03:15 +0000 Subject: [PATCH 30/52] (format comp) PEP8 compliance --- tests/format_comparison.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/format_comparison.py b/tests/format_comparison.py index 2cf6e7a..993ab55 100644 --- a/tests/format_comparison.py +++ b/tests/format_comparison.py @@ -102,8 +102,10 @@ except ValueError as err: print("Could not plot, check dimensions:", err) a.set_title(wformat) - if i == 0: a.set_ylabel("signal [v]") - if j == len(axs)-1: a.set_xlabel("time [s]") + if i == 0: + a.set_ylabel("signal [v]") + if j == len(axs)-1: + a.set_xlabel("time [s]") fig.suptitle("keyoscacquire pure pyvisa") print("\nCalculating the difference between same waveform wformat") From 6310084bdd6046030f38b95fbf6e2cc1c272ff6b Mon Sep 17 00:00:00 2001 From: Andreas Svela Date: Tue, 12 Jan 2021 11:03:54 +0000 Subject: [PATCH 31/52] (docs) update release month --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 80afbf0..ed82bc3 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -13,7 +13,7 @@ That means that there are quite a few non-compatible changes to previous version all of which are detailed below. I am not planning further extensive revisions like this. -v4.0.0 (2020-12) +v4.0.0 (2021-01) - More attributes are used to make the information accessible not only through returns From 56502e0c9c878c9abf164e66f843da7277ce9634 Mon Sep 17 00:00:00 2001 From: Andreas Svela Date: Tue, 12 Jan 2021 11:08:20 +0000 Subject: [PATCH 32/52] module constants in capitals --- docs/changelog.rst | 3 ++- keyoscacquire/__init__.py | 2 +- keyoscacquire/fileio.py | 4 ++-- keyoscacquire/oscilloscope.py | 11 +++++------ 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index ed82bc3..b4e75c0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -103,6 +103,7 @@ v4.0.0 (2021-01) * ``Oscilloscope.address`` -> ``Oscilloscope._address`` * ``Oscilloscope.model`` -> ``Oscilloscope._model`` * ``Oscilloscope.model_series`` -> ``Oscilloscope._model_series`` + * ``oscacq._screen_colors`` -> ``fileio._SCREEN_COLORS`` - *No compatibility*: Moved functions and attributes @@ -110,7 +111,7 @@ v4.0.0 (2021-01) * ``interpret_visa_id()`` from ``oscacq`` to ``visa_utils`` * ``process_data()`` (as well as ``_process_data_ascii`` and ``_process_data_binary``) from ``oscacq`` to ``dataprocessing`` - * ``_screen_colors`` from ``oscacq`` to ``fileio`` + * ``_SCREEN_COLORS`` (prev. ``_screen_colors``) from ``oscacq`` to ``fileio`` - *No compatibility*: Some functions no longer take ``sources`` and ``sourcesstring`` as arguments, rather ``Oscilloscope._sources`` must be set by diff --git a/keyoscacquire/__init__.py b/keyoscacquire/__init__.py index ed4dc8e..0e21ef7 100644 --- a/keyoscacquire/__init__.py +++ b/keyoscacquire/__init__.py @@ -22,4 +22,4 @@ import keyoscacquire.visa_utils as visa_utils from .oscilloscope import Oscilloscope, _supported_series -from .fileio import save_trace, load_trace, _screen_colors +from .fileio import save_trace, load_trace, _SCREEN_COLORS diff --git a/keyoscacquire/fileio.py b/keyoscacquire/fileio.py index 0dc88e5..5350a2f 100644 --- a/keyoscacquire/fileio.py +++ b/keyoscacquire/fileio.py @@ -19,7 +19,7 @@ _log = logging.getLogger(__name__) #: Keysight colour map for the channels -_screen_colors = {1:'C1', 2:'C2', 3:'C0', 4:'C3'} +_SCREEN_COLORS = {1:'C1', 2:'C2', 3:'C0', 4:'C3'} def check_file(fname, ext=config._filetype, num=""): @@ -75,7 +75,7 @@ def plot_trace(time, y, channels, fname="", showplot=config._show_plot, """ fig, ax = plt.subplots() for i, vals in enumerate(np.transpose(y)): # for each channel - ax.plot(time, vals, color=_screen_colors[channels[i]]) + ax.plot(time, vals, color=_SCREEN_COLORS[channels[i]]) if savepng: fig.savefig(fname+".png", bbox_inches='tight') if showplot: diff --git a/keyoscacquire/oscilloscope.py b/keyoscacquire/oscilloscope.py index 6ffb5ad..36e0e39 100644 --- a/keyoscacquire/oscilloscope.py +++ b/keyoscacquire/oscilloscope.py @@ -18,7 +18,6 @@ import numpy as np import matplotlib.pyplot as plt -# local file with default options: import keyoscacquire.config as config import keyoscacquire.visa_utils as visa_utils import keyoscacquire.fileio as fileio @@ -30,10 +29,10 @@ _log = logging.getLogger(__name__) #: Supported Keysight DSO/MSO InfiniiVision series -_supported_series = ['1000', '2000', '3000', '4000', '6000'] +_SUPPORTED_SERIES = ['1000', '2000', '3000', '4000', '6000'] #: Datatype is ``'h'`` for 16 bit signed int (``WORD``), ``'b'`` for 8 bit signed bit (``BYTE``). #: Same naming as for structs `docs.python.org/3/library/struct.html#format-characters` -_datatypes = {'BYT':'b', 'WOR':'h', 'BYTE':'b', 'WORD':'h'} +_DATATYPES = {'BYT':'b', 'WOR':'h', 'BYTE':'b', 'WORD':'h'} ## ========================================================================= ## @@ -152,7 +151,7 @@ def __init__(self, address=config._visa_address, timeout=config._timeout, if self.verbose: print(f"Connected to '{self._id}'") print("(!) Failed to intepret the VISA IDN string") - if not self._model_series in _supported_series: + if not self._model_series in _SUPPORTED_SERIES: print(f"(!) WARNING: This model ({self._model}) is not yet fully supported by keyoscacquire,") print( " but might work to some extent. keyoscacquire supports Keysight's") print( " InfiniiVision X-series oscilloscopes.") @@ -641,7 +640,7 @@ def capture_and_read(self, set_running=True): ## Read from the scope wav_format = wav_format[:3] if wav_format in ['WOR', 'BYT']: - self._read_binary(datatype=_datatypes[wav_format]) + self._read_binary(datatype=_DATATYPES[wav_format]) elif wav_format[:3] == 'ASC': self._read_ascii() else: @@ -675,7 +674,7 @@ def _read_binary(self, datatype='standard'): on :attr:`wav_format`. Datatype is ``'h'`` for 16 bit signed int (``'WORD'``), for 8 bit signed bit (``'BYTE'``) (same naming as for structs, `https://docs.python.org/3/library/struct.html#format-characters`). - ``'standard'`` will evaluate :data:`oscilloscope._datatypes[self.wav_format]` + ``'standard'`` will evaluate :data:`oscilloscope._DATATYPES[self.wav_format]` to automatically choose according to the waveform format set_running : bool, default ``True`` ``True`` leaves oscilloscope running after data capture From 41b81dfec068ed6e930b1ebd01836938f8e3a3e6 Mon Sep 17 00:00:00 2001 From: Andreas Svela Date: Tue, 12 Jan 2021 13:42:31 +0000 Subject: [PATCH 33/52] (readme) fix typo --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 0ab0cd1..6936ad3 100644 --- a/README.rst +++ b/README.rst @@ -54,7 +54,7 @@ information about the API. As an example of API usage/use in the Python console:: - >>> import keyoscacquire koa + >>> import keyoscacquire as koa >>> scope = koa.Oscilloscope(address='USB0::1234::1234::MY1234567::INSTR') Connected to: AGILENT TECHNOLOGIES From a8290308ca55f2a2b6e761b2fac3ff463ce7ec7b Mon Sep 17 00:00:00 2001 From: Andreas Svela Date: Tue, 12 Jan 2021 13:52:53 +0000 Subject: [PATCH 34/52] (docs) know issues adding tests --- docs/known-issues.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/known-issues.rst b/docs/known-issues.rst index 0db4985..60b331c 100644 --- a/docs/known-issues.rst +++ b/docs/known-issues.rst @@ -4,9 +4,10 @@ Known issues and suggested improvements * Issues: - - There has previously been an issue with the data transfer/interpretation - where the output waveform is not as it is on the oscilloscope screen. If - this happens, open *KeySight BenchVue* and obtain one trace through the + - An issue has been reported where the data transfer/interpretation + where the output waveform is not correct, causing the trace to look nothing + like on the oscilloscope screen. If this happens, a fix has been to open + *KeySight BenchVue* and obtain one trace through the software. Now try to obtain a trace through this package -- it should now work again. Please report this if this happens. @@ -24,3 +25,4 @@ Known issues and suggested improvements - (feature) pickling trace to disk for later post-processing to give speed-up in consecutive measurements - (instrument support) expand support for Infiniium oscilloscopes + - (development) include tests From 641a6525fc5d5bef765e598d53bd21359d8d46f2 Mon Sep 17 00:00:00 2001 From: Andreas Svela Date: Tue, 12 Jan 2021 15:41:30 +0000 Subject: [PATCH 35/52] (init) capitalisation of constant --- keyoscacquire/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keyoscacquire/__init__.py b/keyoscacquire/__init__.py index 0e21ef7..6c2a42f 100644 --- a/keyoscacquire/__init__.py +++ b/keyoscacquire/__init__.py @@ -21,5 +21,5 @@ import keyoscacquire.programmes as programmes import keyoscacquire.visa_utils as visa_utils -from .oscilloscope import Oscilloscope, _supported_series +from .oscilloscope import Oscilloscope, _SUPPORTED_SERIES from .fileio import save_trace, load_trace, _SCREEN_COLORS From 8a305030ed0f84ad42e0542aae11925979884029 Mon Sep 17 00:00:00 2001 From: Andreas Svela Date: Tue, 12 Jan 2021 15:57:33 +0000 Subject: [PATCH 36/52] (license) update year --- LICENSE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE.md b/LICENSE.md index 2621cd5..4c5e259 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,4 @@ -Copyright 2020 Andreas Svela +Copyright 2021 Andreas Svela Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the From b25edaa4b7c51c83020120800ed7dc9f53d49001 Mon Sep 17 00:00:00 2001 From: Andreas Svela Date: Tue, 12 Jan 2021 15:58:52 +0000 Subject: [PATCH 37/52] (docs, setup) pypi compatible readme and minor updates --- README.rst | 32 +++++++++++++++++++++----------- docs/contents/oscilloscope.rst | 2 +- docs/contents/overview.rst | 26 ++++++++++++++------------ docs/index.rst | 4 ++++ setup.py | 11 +++++++++-- 5 files changed, 49 insertions(+), 26 deletions(-) diff --git a/README.rst b/README.rst index 6936ad3..fcc06cb 100644 --- a/README.rst +++ b/README.rst @@ -26,30 +26,35 @@ format files (default csv) or numpy `npy `_. -A few examples below, but formatting and links are broken as the snippet is intended +A few examples below, but formatting and links are broken as this file is intended for the documentation parser. + +Installation +------------ + +Install the package with pip:: + + pip install keyoscacquire + +or download locally and install with ``$ python setup.py install`` or +by running ``install.bat``. + .. API-use-marker Python console/API ------------------ -The Reference section (particularly :ref:`osc-class`) gives all the necessary +The Reference/API section (particularly :ref:`osc-class`) gives all the necessary information about the API. As an example of API usage/use in the Python console:: @@ -140,9 +145,14 @@ See more under :ref:`cli-programmes-short`. .. contribute-marker -Contribute ----------- +Contribute/report issues +------------------------ + +Please report any issues with the package with the +`issue tracker on Github `_. -Contributions are welcome, find the project on +Contributions are welcome via `github `_. + + The package is written and maintained by Andreas Svela. diff --git a/docs/contents/oscilloscope.rst b/docs/contents/oscilloscope.rst index 9d5e7c9..1a9cfc8 100644 --- a/docs/contents/oscilloscope.rst +++ b/docs/contents/oscilloscope.rst @@ -3,7 +3,7 @@ Instrument communication ************************ -The :mod:`keyoscacquire.oscilloscope` is responsible for the instrument communication. +The :mod:`keyoscacquire.oscilloscope` module is responsible for the instrument communication. The Oscilloscope class ====================== diff --git a/docs/contents/overview.rst b/docs/contents/overview.rst index 5ac8c56..86ab533 100644 --- a/docs/contents/overview.rst +++ b/docs/contents/overview.rst @@ -12,13 +12,7 @@ see :ref:`cli-programmes`. Default options are found in :mod:`keyoscacquire.conf and the :mod:`keyoscacquire.fileio` provides functions for plotting, saving, and loading traces from disk. - -Quick reference -=============== - -.. include:: ../../README.rst - :start-after: API-use-marker - :end-before: contribute-marker +keyoscacquire uses the :py:mod:`logging` module, see :ref:`logging`. Installation @@ -35,16 +29,24 @@ or download locally and install with ``$ python setup.py install`` or by running ``install.bat``. -Building the docs ------------------ -To build a local copy of the sphinx docs make sure the necessary packages -are installed +Building a local copy of the docs (optional) +-------------------------------------------- +Should you wish to build a local copy of the sphinx docs, make sure the +necessary packages are installed .. prompt:: bash pip install sphinx sphinx-prompt furo recommonmark -Then build by executing ``make html`` in the ``docs`` folder. +and then build by executing ``make html`` in the ``docs`` folder. + + +Quick reference +=============== + +.. include:: ../../README.rst + :start-after: API-use-marker + :end-before: contribute-marker .. include:: ../known-issues.rst diff --git a/docs/index.rst b/docs/index.rst index 49fd2f7..5f98889 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -41,3 +41,7 @@ License ------- The project is licensed under the MIT license, see :ref:`license`. + + +.. include:: ../README.rst + :start-after: contribute-marker diff --git a/setup.py b/setup.py index d8445c2..5afa455 100644 --- a/setup.py +++ b/setup.py @@ -18,14 +18,21 @@ # Get the contents of readme with open(os.path.join(current_dir, "README.rst")) as readme_file: README = readme_file.read() + # To avoid the parts with sphinx markup: + README, _ = README.split(".. API-use-marker") + print(README) if __name__ == '__main__': setup(name='keyoscacquire', version=__version__, description='keyoscacquire is a Python package for acquiring traces from Keysight oscilloscopes through a VISA interface.', long_description=README, - long_description_content_type="text/x-rst", - url='https://github.com/asvela/keyoscacquire.git', + long_description_content_type='text/x-rst', + # url='https://keyoscacquire.readthedocs.io/', + project_urls={ + "Documentation": "https://keyoscacquire.readthedocs.io/", + "Source": "https://github.com/asvela/keyoscacquire", + }, author='Andreas Svela', author_email='asvela@ic.ac.uk', license='MIT', From 6dff58d6e92514d17a4005f1c640b8368e0c6f21 Mon Sep 17 00:00:00 2001 From: Andreas Svela Date: Thu, 14 Jan 2021 12:13:35 +0000 Subject: [PATCH 38/52] (visa_utils) reducing the visa timeout for faster listing of devices --- keyoscacquire/visa_utils.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/keyoscacquire/visa_utils.py b/keyoscacquire/visa_utils.py index 19b41e3..364e9e7 100644 --- a/keyoscacquire/visa_utils.py +++ b/keyoscacquire/visa_utils.py @@ -47,7 +47,8 @@ def interpret_visa_id(idn): return maker, model, serial, firmware, model_series -def obtain_instrument_information(resource_manager, address, num, ask_idn=True): +def obtain_instrument_information(resource_manager, address, num, + ask_idn=True, timeout=200): """Obtain more information about a VISA resource Parameters @@ -60,6 +61,8 @@ def obtain_instrument_information(resource_manager, address, num, ask_idn=True): ask_idn : bool If ``True``: will query the instrument's IDN and interpret it if possible + timeout : int, default 200 + VISA connection timeout Returns ------- @@ -81,12 +84,12 @@ def obtain_instrument_information(resource_manager, address, num, ask_idn=True): # Open the instrument and get the identity string try: error_flag = False - instrument = resource_manager.open_resource(address) + instrument = resource_manager.open_resource(address, timeout=timeout) idn = instrument.query("*IDN?").strip() instrument.close() except pyvisa.Error as e: error_flag = True - resource_info.extend(["no IDN response"]*5) + resource_info.extend(["no IDN reply"]*5) print(f"Instrument #{num}: Did not respond to *IDN?: {e}") except Exception as ex: error_flag = True From 5e6f53d2de7847d6415688bf6422c14aadc5818a Mon Sep 17 00:00:00 2001 From: Andreas Svela Date: Thu, 14 Jan 2021 12:18:28 +0000 Subject: [PATCH 39/52] (programmes) fix typo --- keyoscacquire/programmes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keyoscacquire/programmes.py b/keyoscacquire/programmes.py index 1c01509..041e625 100644 --- a/keyoscacquire/programmes.py +++ b/keyoscacquire/programmes.py @@ -19,7 +19,7 @@ import numpy as np from tqdm import tqdm -import keyoscacquire.oscilloscope as oscillocope +import keyoscacquire.oscilloscope as oscilloscope import keyoscacquire.config as config import keyoscacquire.fileio as fileio import keyoscacquire.visa_utils as visa_utils From 8d49b218ca667e05c145165cec454eac2e982b9f Mon Sep 17 00:00:00 2001 From: Andreas Svela Date: Thu, 14 Jan 2021 12:20:11 +0000 Subject: [PATCH 40/52] module docstring update --- keyoscacquire/__init__.py | 2 ++ keyoscacquire/fileio.py | 1 - keyoscacquire/installed_cli_programmes.py | 1 - keyoscacquire/oscilloscope.py | 7 ++----- keyoscacquire/programmes.py | 1 - 5 files changed, 4 insertions(+), 8 deletions(-) diff --git a/keyoscacquire/__init__.py b/keyoscacquire/__init__.py index 6c2a42f..49d424f 100644 --- a/keyoscacquire/__init__.py +++ b/keyoscacquire/__init__.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- """ +Docs avaliable at keyoscacquire.rtfd.io + Andreas Svela // 2019-2021 """ diff --git a/keyoscacquire/fileio.py b/keyoscacquire/fileio.py index 5350a2f..bc19b63 100644 --- a/keyoscacquire/fileio.py +++ b/keyoscacquire/fileio.py @@ -4,7 +4,6 @@ (see :mod:`numpy.lib.format`) or ascii files. The latter is slower but permits a header with metadata for the measurement, see :func:`Oscilloscope.generate_file_header` which is used when saving directly from the ``Oscilloscope`` class. - """ import os diff --git a/keyoscacquire/installed_cli_programmes.py b/keyoscacquire/installed_cli_programmes.py index d9c796d..c568178 100644 --- a/keyoscacquire/installed_cli_programmes.py +++ b/keyoscacquire/installed_cli_programmes.py @@ -13,7 +13,6 @@ Optional argument from the command line: string setting the base filename of the output files. Change _visa_address in keyoscacquire.config to the desired instrument's address. - """ import sys diff --git a/keyoscacquire/oscilloscope.py b/keyoscacquire/oscilloscope.py index 36e0e39..84abe76 100644 --- a/keyoscacquire/oscilloscope.py +++ b/keyoscacquire/oscilloscope.py @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- """ -The PyVISA communication with the oscilloscope. +The PyVISA communication with the oscilloscope See Keysight's Programmer's Guide for reference on the VISA commands. - -Andreas Svela // 2019 """ __docformat__ = "restructuredtext en" @@ -771,8 +769,7 @@ def get_trace(self, channels=None, verbose_acquistion=None): # Possibility to override verbose_acquistion if verbose_acquistion is not None: self.verbose_acquistion = verbose_acquistion - if channels is not None: - self.set_channels_for_capture(channels=channels) + self.set_channels_for_capture(channels=channels) # Capture, read and process data self.capture_and_read() self._time, self._values = dataprocessing.process_data(self._raw, self._metadata, self.wav_format, diff --git a/keyoscacquire/programmes.py b/keyoscacquire/programmes.py index 041e625..47e66d9 100644 --- a/keyoscacquire/programmes.py +++ b/keyoscacquire/programmes.py @@ -9,7 +9,6 @@ * :func:`get_traces_single_connection_loop` :func:`get_traces_connect_each_time_loop`: two programmes for taking multiple traces when a key is pressed, see descriptions for difference * :func:`get_num_traces`: get a specific number of traces - """ import os From 7cae8b75d3244a67d333eb47ee1617320319c0f1 Mon Sep 17 00:00:00 2001 From: Andreas Svela Date: Thu, 14 Jan 2021 12:22:34 +0000 Subject: [PATCH 41/52] (README) addition of default visa address and transfer format --- README.rst | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index fcc06cb..74b9d9c 100644 --- a/README.rst +++ b/README.rst @@ -26,6 +26,10 @@ format files (default csv) or numpy `npy Date: Thu, 14 Jan 2021 12:22:57 +0000 Subject: [PATCH 42/52] (VERSION) bump to beta3 --- keyoscacquire/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keyoscacquire/VERSION b/keyoscacquire/VERSION index 0c076d6..1dc06cd 100644 --- a/keyoscacquire/VERSION +++ b/keyoscacquire/VERSION @@ -1 +1 @@ -4.0.0b2 +4.0.0b3 From 2f7ba9556918827aeb367ee0ad1172098e727aec Mon Sep 17 00:00:00 2001 From: Andreas Svela Date: Thu, 14 Jan 2021 12:24:41 +0000 Subject: [PATCH 43/52] (README) fix typo --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 74b9d9c..c967540 100644 --- a/README.rst +++ b/README.rst @@ -160,7 +160,7 @@ Please report any issues with the package with the `issue tracker on Github `_. Contributions are welcome via -`github `_. +`Github `_. The package is written and maintained by Andreas Svela. From 416d494e371cb00294ddf32c5dfb418fa4c4f83b Mon Sep 17 00:00:00 2001 From: Andreas Svela Date: Thu, 14 Jan 2021 12:40:10 +0000 Subject: [PATCH 44/52] (oscilloscope) bugfix for channels not passed on --- keyoscacquire/oscilloscope.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/keyoscacquire/oscilloscope.py b/keyoscacquire/oscilloscope.py index 84abe76..971e9bc 100644 --- a/keyoscacquire/oscilloscope.py +++ b/keyoscacquire/oscilloscope.py @@ -811,11 +811,10 @@ def set_options_get_trace(self, channels=None, wav_format=None, acq_type=None, _capture_channels : list of ints list of the channels obtaied from, example ``[1, 3]`` """ - self.set_channels_for_capture(channels=channels) self.set_acquiring_options(wav_format=wav_format, acq_type=acq_type, num_averages=num_averages, p_mode=p_mode, num_points=num_points) - self.get_trace() + self.get_trace(channels=channels) return self._time, self._values, self._capture_channels def set_options_get_trace_save(self, fname=None, ext=None, From 17dbc1ba8eeb6cebd574982f1a7ecfab507ce1aa Mon Sep 17 00:00:00 2001 From: Andreas Svela Date: Thu, 14 Jan 2021 12:40:26 +0000 Subject: [PATCH 45/52] (VERSION) bump to beta4 --- keyoscacquire/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keyoscacquire/VERSION b/keyoscacquire/VERSION index 1dc06cd..69b22a5 100644 --- a/keyoscacquire/VERSION +++ b/keyoscacquire/VERSION @@ -1 +1 @@ -4.0.0b3 +4.0.0b4 From 09e28775c03620a482942ed71d349608dd44f04c Mon Sep 17 00:00:00 2001 From: Andreas Svela Date: Thu, 14 Jan 2021 13:30:44 +0000 Subject: [PATCH 46/52] (oscilloscope) restructuring for better maintainability --- keyoscacquire/oscilloscope.py | 74 ++++++++++++++++++++++------------- 1 file changed, 46 insertions(+), 28 deletions(-) diff --git a/keyoscacquire/oscilloscope.py b/keyoscacquire/oscilloscope.py index 971e9bc..6bfa153 100644 --- a/keyoscacquire/oscilloscope.py +++ b/keyoscacquire/oscilloscope.py @@ -138,6 +138,15 @@ def __init__(self, address=config._visa_address, timeout=config._timeout, self.write(':WAVeform:UNSigned OFF') self.write(':WAVeform:BYTeorder LSBFirst') # MSBF is default, must be overridden for WORD to work # Get information about the connected device + self._information_about_device() + # Set standard settings + self.set_acquiring_options(wav_format=config._waveform_format, p_mode=config._p_mode, + num_points=config._num_points) + # Will set channels to the active channels + self.set_channels_for_capture() + self.verbose_acquistion = verbose + + def _information_about_device(self): self._id = self.query('*IDN?') try: maker, self._model, self._serial, _, self._model_series = visa_utils.interpret_visa_id(self._id) @@ -150,15 +159,9 @@ def __init__(self, address=config._visa_address, timeout=config._timeout, print(f"Connected to '{self._id}'") print("(!) Failed to intepret the VISA IDN string") if not self._model_series in _SUPPORTED_SERIES: - print(f"(!) WARNING: This model ({self._model}) is not yet fully supported by keyoscacquire,") - print( " but might work to some extent. keyoscacquire supports Keysight's") - print( " InfiniiVision X-series oscilloscopes.") - # Set standard settings - self.set_acquiring_options(wav_format=config._waveform_format, p_mode=config._p_mode, - num_points=config._num_points) - # Will set channels to the active channels - self.set_channels_for_capture() - self.verbose_acquistion = verbose + print(f"(!) WARNING: This model ({self._model}) is not yet fully supported by keyoscacquire,") + print( " but might work to some extent. keyoscacquire supports Keysight's") + print( " InfiniiVision X-series oscilloscopes.") def __enter__(self): return self @@ -244,21 +247,25 @@ def get_full_error_queue(self, verbose=True): self.errors = [] for i in range(30): err = self.get_error() - if err[:2] == "+0": # no error - # stop querying + if err[:2] == "+0": # No error + # Stop querying break else: - # store the error + # Store the error self.errors.append(err) if verbose: - if not self.errors: - print("Error queue empty") - else: - print("Latest errors from the oscilloscope (FIFO queue, upto 30 errors)") - for i, err in enumerate(self.errors): - print(f"{i:>2}: {err}") + self._print_errors(self.errors) return self.errors + def _print_errors(self, errors): + """Print the errors obtained by :func:`Oscilloscope.get_full_error_queue`""" + if not errors: + print("Error queue empty") + else: + print("Latest errors from the oscilloscope (FIFO queue, upto 30 errors)") + for i, err in enumerate(errors): + print(f"{i:>2}: {err}") + def run(self): """Set the oscilloscope to running mode.""" self.write(":RUN") @@ -357,13 +364,24 @@ def acq_type(self, a_type: str): self.write(f":ACQuire:TYPE {acq_type}") # Handle AVER expressions if acq_type == 'AVER': - if len(a_type) > 4 and not a_type[4:].lower() == 'age': - try: - self.num_averages = int(a_type[4:]) - except ValueError: - ValueError(f"\nValueError: Failed to convert '{a_type[4:]}' to an integer, " - "check that acquisition type is on the form AVER or AVER " - f"where is an integer (currently acq. type is '{a_type}').\n") + self._handle_aver(a_type) + + def _handle_aver(self, a_type: str): + """Handle ``AVER*`` acquisition types, using a possible int after + ``*`` as the number of averages + + Raises + ------ + ValueError + If * cannot be converted to int + """ + if len(a_type) > 4 and not a_type[4:].lower() == 'age': + try: + self.num_averages = int(a_type[4:]) + except ValueError: + ValueError(f"\nValueError: Failed to convert '{a_type[4:]}' to an integer, " + "check that acquisition type is on the form AVER or AVER " + f"where is an integer (currently acq. type is '{a_type}').\n") @property def num_averages(self): @@ -385,7 +403,7 @@ def num_averages(self): def num_averages(self, num: int): """See getter""" if not (2 <= num <= 65536): - raise ValueError(f"\nThe number of averages {num} is out of range.") + raise ValueError(f"\nThe number of averages {num} is out of range.") self.write(f":ACQuire:COUNt {num}") def print_acq_settings(self): @@ -966,7 +984,7 @@ def save_trace(self, fname=None, ext=None, additional_header_info=None, showplot=self.showplot, savepng=self.savepng) head = self.generate_file_header(additional_line=additional_header_info) fileio.save_trace(self.fname, self._time, self._values, fileheader=head, ext=self.ext, - print_filename=self.verbose_acquistion, nowarn=nowarn) + print_filename=self.verbose_acquistion, nowarn=nowarn) else: print("(!) No trace has been acquired yet, use get_trace()") _log.info("(!) No trace has been acquired yet, use get_trace()") @@ -975,7 +993,7 @@ def plot_trace(self): """Plot and show the most recent trace""" if not self._time is None: fileio.plot_trace(self._time, self._values, self._capture_channels, - savepng=False, showplot=True) + savepng=False, showplot=True) else: print("(!) No trace has been acquired yet, use get_trace()") _log.info("(!) No trace has been acquired yet, use get_trace()") From 00a4505c1f00f9cff2170501406bb291533de186 Mon Sep 17 00:00:00 2001 From: Andreas Svela Date: Fri, 15 Jan 2021 11:45:57 +0000 Subject: [PATCH 47/52] (docs) info about compiling docs locally and installing a specific version --- README.rst | 19 +++++++++++++++++++ docs/changelog.rst | 5 +++++ docs/contents/overview.rst | 12 ------------ docs/index.rst | 8 ++++++++ 4 files changed, 32 insertions(+), 12 deletions(-) diff --git a/README.rst b/README.rst index c967540..5be4910 100644 --- a/README.rst +++ b/README.rst @@ -9,6 +9,10 @@ keyoscacquire: Keysight oscilloscope acquire :target: https://www.codefactor.io/repository/github/asvela/keyoscacquire :alt: CodeFactor +.. image:: https://img.shields.io/codeclimate/maintainability/asvela/keyoscacquire?style=flat-square + :target: https://codeclimate.com/github/asvela/keyoscacquire + :alt: Code Climate maintainability + .. image:: https://img.shields.io/readthedocs/keyoscacquire?style=flat-square :target: https://keyoscacquire.rtfd.io :alt: Read the Docs Building @@ -42,6 +46,21 @@ Available at `keyoscacquire.rtfd.io +Building a local copy of the docs +--------------------------------- + +.. include:: ../README.rst + :start-after: start-local-copy-documentation-marker + :end-before: end-local-copy-documentation-marker + + License ------- From 3a5155b21793b74a1db24c96163511061dc1f0f3 Mon Sep 17 00:00:00 2001 From: Andreas Svela Date: Fri, 15 Jan 2021 11:52:09 +0000 Subject: [PATCH 48/52] (MANIFEST) adding docs to pypi package --- MANIFEST.in | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MANIFEST.in b/MANIFEST.in index acec0c4..b455379 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,3 +4,5 @@ include INSTALL.md include LICENSE.md include install.bat include scripts/* +include docs/* +include docs/contents/* From cda1983cebd46fed03a18d06bc97e0bc62c346ed Mon Sep 17 00:00:00 2001 From: Andreas Svela Date: Fri, 15 Jan 2021 11:52:38 +0000 Subject: [PATCH 49/52] (README) removing sphinx rst commands --- README.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 5be4910..076719c 100644 --- a/README.rst +++ b/README.rst @@ -52,9 +52,7 @@ Building a local copy of the docs .. start-local-copy-documentation-marker Should you wish to build a local copy of the sphinx docs, make sure the -necessary packages are installed - -.. prompt:: bash +necessary packages are installed:: pip install sphinx sphinx-prompt furo recommonmark From addbd3b2dd1b532bc8a59f840568776c4fcac800 Mon Sep 17 00:00:00 2001 From: Andreas Svela Date: Thu, 21 Jan 2021 02:47:01 +0000 Subject: [PATCH 50/52] (oscilloscope) typos and main docstring --- keyoscacquire/oscilloscope.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/keyoscacquire/oscilloscope.py b/keyoscacquire/oscilloscope.py index 6bfa153..afa50fd 100644 --- a/keyoscacquire/oscilloscope.py +++ b/keyoscacquire/oscilloscope.py @@ -147,6 +147,7 @@ def __init__(self, address=config._visa_address, timeout=config._timeout, self.verbose_acquistion = verbose def _information_about_device(self): + """Get the IDN of the instrument and parse it""" self._id = self.query('*IDN?') try: maker, self._model, self._serial, _, self._model_series = visa_utils.interpret_visa_id(self._id) @@ -649,7 +650,7 @@ def capture_and_read(self, set_running=True): # on the screen and hence don't want to use DIGitize as digitize # will obtain a new trace. if self.is_running(): - # DIGitize is a specialized RUN command. + # DIGitize is a specialised RUN command. # Waveforms are acquired according to the settings of the :ACQuire commands. # When acquisition is complete, the instrument is stopped. self.write(':DIGitize ' + ", ".join(self._sources)) @@ -999,10 +1000,8 @@ def plot_trace(self): _log.info("(!) No trace has been acquired yet, use get_trace()") - -## Module main function ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## - def main(): + """Take a trace using the default settings""" fname = sys.argv[1] if len(sys.argv) >= 2 else config._filename ext = config._filetype with Oscilloscope() as scope: From 232bd263074f978721680a178f5691220a002b6f Mon Sep 17 00:00:00 2001 From: Andreas Svela Date: Thu, 21 Jan 2021 02:48:34 +0000 Subject: [PATCH 51/52] (cli) added a missing docstring and _ prefix --- keyoscacquire/installed_cli_programmes.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/keyoscacquire/installed_cli_programmes.py b/keyoscacquire/installed_cli_programmes.py index c568178..814bbeb 100644 --- a/keyoscacquire/installed_cli_programmes.py +++ b/keyoscacquire/installed_cli_programmes.py @@ -36,7 +36,8 @@ delim_help = f"Delimiter used between filename and filenumber (before filetype). Defaults to '{config._file_delimiter}'." -def standard_arguments(parser): +def _standard_arguements(parser): + """Short hand for adding arguments to the parser""" connection_gr = parser.add_argument_group('Connection settings') connection_gr.add_argument('-v', '--visa_address', nargs='?', default=config._visa_address, help=visa_help) @@ -61,7 +62,7 @@ def connect_each_time_cli(): """Function installed on the command line: Obtains and stores multiple traces, connecting to the oscilloscope each time.""" parser = argparse.ArgumentParser(description=programmes.get_traces_connect_each_time_loop.__doc__) - trans_gr = standard_arguments(parser) + trans_gr = _standard_arguements(parser) trans_gr.add_argument('--file_delimiter', nargs='?', help=delim_help, default=config._file_delimiter) args = parser.parse_args() # Convert channels arg to ints @@ -81,7 +82,7 @@ def single_connection_cli(): """Function installed on the command line: Obtains and stores multiple traces, keeping a the same connection to the oscilloscope open all the time.""" parser = argparse.ArgumentParser(description=programmes.get_traces_single_connection_loop.__doc__) - trans_gr = standard_arguments(parser) + trans_gr = _standard_arguements(parser) trans_gr.add_argument('--file_delimiter', nargs='?', help=delim_help, default=config._file_delimiter) args = parser.parse_args() # Convert channels arg to ints @@ -100,7 +101,7 @@ def single_connection_cli(): def single_trace_cli(): """Function installed on the command line: Obtains and stores a single trace.""" parser = argparse.ArgumentParser(description=programmes.get_single_trace.__doc__) - standard_arguments(parser) + _standard_arguements(parser) args = parser.parse_args() # Convert channels arg to ints if args.channels is not None: @@ -120,7 +121,7 @@ def num_traces_cli(): # postitional arg parser.add_argument('num', help='The number of successive traces to obtain.', type=int) # optional args - trans_gr = standard_arguments(parser) + trans_gr = _standard_arguements(parser) trans_gr.add_argument('--file_delimiter', nargs='?', help=delim_help, default=config._file_delimiter) args = parser.parse_args() # Convert channels arg to ints From fcef04c8f74a0c355a6c5b2da1987680936b6f4c Mon Sep 17 00:00:00 2001 From: Andreas Svela Date: Thu, 21 Jan 2021 02:52:41 +0000 Subject: [PATCH 52/52] (VERSION) bump to 4.0.0 --- keyoscacquire/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keyoscacquire/VERSION b/keyoscacquire/VERSION index 69b22a5..fcdb2e1 100644 --- a/keyoscacquire/VERSION +++ b/keyoscacquire/VERSION @@ -1 +1 @@ -4.0.0b4 +4.0.0