diff --git a/.gitignore b/.gitignore index c18dd8d..64777da 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ __pycache__/ +.DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1c69b3d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,14 @@ +v0.2 +- More PEP8 compliant: + - `func_gen()` -> `FuncGen()` + - `func_gen_channel()` -> `FuncGenChannel()` + - All lines < 100 characters (mostly < 80) +- No more `enable`/`disable_frequency_lock()`, now `set_frequency_lock()` +- Settings dictionary now contains tuples of value and unit, e.g. `settings = {"amplitude": (3, "Vpp"), ..}` +- Implemented `set_settings()` in both `FuncGen` and `FuncGenChannel` that takes a settings dictionary as input +- `get`/`set_output()` links to `get`/`set_output_state()` +- More examples +- Expanded README + +v0.1 +- First release diff --git a/README.md b/README.md index 128a46f..e705be1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Tektronix arbitrary function generator control through PyVISA -v0.1.0 // Dec 2019 +v0.2.0 // Dec 2019 API documentation can be found at [GitHub pages](https://asvela.github.io/tektronix-func-gen/) or in the repository [docs/index.html](docs/index.html). (To build the documentation yourself use [pdoc3](https://pdoc3.github.io/pdoc/) and run `$ pdoc --html tektronix_func_gen`.) @@ -10,18 +10,21 @@ API documentation can be found at [GitHub pages](https://asvela.github.io/tektro Put the module file in the folder wherein the file you will import it from resides. -## Usage +## Usage (through examples) -An example: +An example of basic control ```python import tektronix_func_gen as tfg -with tfg.func_gen('VISA ADDRESS OF YOUR INSTRUMENT') as fgen: +with tfg.FuncGen('VISA ADDRESS OF YOUR INSTRUMENT') as fgen: fgen.ch1.set_function("SIN") fgen.ch1.set_frequency(25, unit="Hz") + fgen.ch1.set_offset(50, unit="mV") + fgen.ch1.set_amplitude(0.002) fgen.ch1.set_output("ON") fgen.ch2.set_output("OFF") + # alternatively fgen.ch1.print_settings() to show from one channel only fgen.print_settings() ``` @@ -37,20 +40,42 @@ Current settings for TEKTRONIX AFG1022 XXXXX output ON OFF function SIN RAMP amplitude 0.002 1 Vpp - offset 0.0 -0.45 V + offset 0.05 -0.45 V frequency 25.0 10.0 Hz ``` -More examples are included at the end of the module. +Settings can also be stored and restored: -### Impedance - -Unfortunately the impedance (50Ω or high Z) cannot be controlled or read remotely. Which setting is in use affects the limits of the output voltage. Use the optional impedance keyword in the initialisation of the func_gen object to make the object aware what limits applies: `func_gen('VISA ADDRESS OF YOUR INSTRUMENT', impedance=("highZ", "50ohm"))`. +```python +"""Example showing how to connect, get the current settings of +the instrument, store them, change a setting and then restore the +initial settings""" +import tektronix_func_gen as tfg +with tfg.FuncGen('VISA ADDRESS OF YOUR INSTRUMENT') as fgen: + fgen.print_settings() + print("Saving these settings..") + settings = fgen.get_settings() + print("Change to 1Vpp amplitude for channel 1..") + fgen.ch1.set_amplitude(1) + fgen.print_settings() + print("Reset back to initial settings..") + fgen.set_settings(settings) + fgen.print_settings() +``` ### Syncronisation and frequency lock -The phase of the two channels can be syncronised with `syncronise_waveforms()`. Frequency lock can also be enabled/disabled with `enable_frequency_lock()`/`disable_frequency_lock()`. +The phase of the two channels can be syncronised with `syncronise_waveforms()`. Frequency lock can also be enabled/disabled with `set_frequency_lock()`: + +```python +"""Example showing the frequency being set to 10Hz and then the frequency +lock enabled, using the frequency at ch1 as the common frequency""" +import tektronix_func_gen as tfg +with tfg.FuncGen('VISA ADDRESS OF YOUR INSTRUMENT', verbose=False) as fgen: + fgen.ch1.set_frequency(10) + fgen.set_frequency_lock("ON", use_channel=1) +``` ### Arbitrary waveforms @@ -61,7 +86,7 @@ The length of the waveform must be between 2 and 8192 points. ```python import numpy as np import tektronix_func_gen as tfg -with tfg.func_gen('VISA ADDRESS OF YOUR INSTRUMENT') as fgen: +with tfg.FuncGen('VISA ADDRESS OF YOUR INSTRUMENT') as fgen: # create waveform x = np.linspace(0, 4*np.pi, 8000) waveform = np.sin(x)+x/5 @@ -82,7 +107,7 @@ with tfg.func_gen('VISA ADDRESS OF YOUR INSTRUMENT') as fgen: ### Set voltage and frequency limits -Limits for amplitude, voltage and frequency can be set by accessing the `func_gen_channel.channel_limits` dictionary or using the `set_stricter_limits()`. The dictionary has the following structure (these are the standard limits for AFG1022) +Limits for amplitude, voltage and frequency for each channel are kept in a dictionary `FuncGenChannel.channel_limits` (these are the standard limits for AFG1022) ```python channel_limits = { @@ -92,3 +117,27 @@ channel_limits = { "amplitude lims": ({"50ohm": {"min": 0.001, "max": 10}, "highZ": {"min": 0.002, "max": 20}}, "Vpp")} ``` + +They chan be changed by `FuncGenChannel.set_limit()`, or by using the `FuncGenChannel.set_stricter_limits()` for a series of prompts. + +```python +import tektronix_func_gen as tfg +"""Example showing how limits can be read and changed""" +with tfg.FuncGen('VISA ADDRESS OF YOUR INSTRUMENT') as fgen: + lims = fgen.ch1.get_frequency_lims() + print("Channel 1 frequency limits: {}".format(lims)) + print("Change the lower limit to 2Hz..") + fgen.ch1.set_limit("frequency lims", "min", 2) + lims = fgen.ch1.get_frequency_lims() + print("Channel 1 frequency limits: {}".format(lims)) + print("Try to set ch1 frequency to 1Hz..") + try: + fgen.ch1.set_frequency(1) + except NotSetError as err: + print(err) +``` + + +### Impedance + +Unfortunately the impedance (50Ω or high Z) cannot be controlled or read remotely. Which setting is in use affects the limits of the output voltage. Use the optional impedance keyword in the initialisation of the func_gen object to make the object aware what limits applies: `FuncGen('VISA ADDRESS OF YOUR INSTRUMENT', impedance=("highZ", "50ohm"))`. diff --git a/docs/index.html b/docs/tektronix_func_gen.html similarity index 52% rename from docs/index.html rename to docs/tektronix_func_gen.html index 984e773..3d1fe11 100644 --- a/docs/index.html +++ b/docs/tektronix_func_gen.html @@ -21,7 +21,8 @@

Module tektronix_func_gen

Control of Tektronix AFG1022 function generator through PyVISA

-

To build the documentation use pdoc3 and run $ pdoc --html tektronix_func_gen

+

To build the documentation use pdoc3 and +run $ pdoc --html tektronix_func_gen

Andreas Svela 2019

Todo

@@ -947,12 +1237,14 @@

Functions

Parameters

unit : str
-
The unit whose first character is checked against the list of prefactors
+
The unit whose first character is checked against the list of +prefactors {"M": 1e6, "k": 1e3, "m": 1e-3}

Returns

factor : float or None
-
The appropriate factor or 1 if not found in the list, or None if the unit string is empty
+
The appropriate factor or 1 if not found in the list, or None +if the unit string is empty
@@ -964,60 +1256,124 @@

Returns

Parameters ---------- unit : str - The unit whose first character is checked against the list of prefactors - {"M": 1e6, "k": 1e3, "m": 1e-3} + The unit whose first character is checked against the list of + prefactors {"M": 1e6, "k": 1e3, "m": 1e-3} Returns ------- factor : float or `None` - The appropriate factor or 1 if not found in the list, or `None` if the unit string is empty + The appropriate factor or 1 if not found in the list, or `None` + if the unit string is empty """ # SI prefix to numerical value - SI_conversion = {"M": 1e6, "k": 1e3, "m": 1e-3} - try: # using the unit's first character as key in the dictionary + SI_conversion = {"M":1e6, "k":1e3, "m":1e-3} + try: # using the unit's first character as key in the dictionary factor = SI_conversion[unit[0]] - except KeyError: # if the entry does not exist + except KeyError: # if the entry does not exist factor = 1 - except IndexError: # if the unit string is empty + except IndexError: # if the unit string is empty factor = None return factor
-
-def example_lock_frequencies(address) +
+def example_basic_control(address)
-

Example showing the frequency being set to 10Hz and then the frequency -lock enabled, using the frequency at ch1 as the common frequency

+

Example showing how to connect, and the most basic control of the +instrument parameteres

Expand source code -
def example_lock_frequencies(address):
-    """Example showing the frequency being set to 10Hz and then the frequency
-    lock enabled, using the frequency at ch1 as the common frequency"""
-    with func_gen(address, verbose=False) as fgen:
-        fgen.ch1.set_frequency(10)
-        fgen.enable_frequency_lock(use_channel=1)
+
def example_basic_control(address):
+    """Example showing how to connect, and the most basic control of the
+    instrument parameteres"""
+    print("\n\n", example_basic_control.__doc__)
+    with FuncGen(address) as fgen:
+      fgen.ch1.set_function("SIN")
+      fgen.ch1.set_frequency(25, unit="Hz")
+      fgen.ch1.set_offset(50, unit="mV")
+      fgen.ch1.set_amplitude(0.002)
+      fgen.ch1.set_output("ON")
+      fgen.ch2.set_output("OFF")
+      # Alternatively fgen.ch1.print_settings() to show from one channel only
+      fgen.print_settings()
-
-def example_print_current_settings(address) +
+def example_change_settings(address)
-

Example showing how to connect and get the current settings of the instrument

+

Example showing how to get the current settings of the instrument, +store them, change a setting and then restore the initial settings

Expand source code -
def example_print_current_settings(address):
-    """Example showing how to connect and get the current settings of the instrument"""
-    with func_gen(address) as fgen:
+
def example_change_settings(address):
+    """Example showing how to get the current settings of the instrument,
+    store them, change a setting and then restore the initial settings"""
+    print("\n\n", example_change_settings.__doc__)
+    with FuncGen(address) as fgen:
+        fgen.print_settings()
+        print("Saving these settings..")
+        settings = fgen.get_settings()
+        print("Change to 1Vpp amplitude for channel 1..")
+        fgen.ch1.set_amplitude(1)
+        fgen.print_settings()
+        print("Reset back to initial settings..")
+        fgen.set_settings(settings)
         fgen.print_settings()
+
+def example_changing_limits(address) +
+
+

Example showing how limits can be read and changed

+
+ +Expand source code + +
def example_changing_limits(address):
+    """Example showing how limits can be read and changed"""
+    print("\n\n", example_changing_limits.__doc__)
+    with FuncGen(address) as fgen:
+        lims = fgen.ch1.get_frequency_lims()
+        print("Channel 1 frequency limits: {}".format(lims))
+        print("Change the lower limit to 2Hz..")
+        fgen.ch1.set_limit("frequency lims", "min", 2)
+        lims = fgen.ch1.get_frequency_lims()
+        print("Channel 1 frequency limits: {}".format(lims))
+        print("Try to set ch1 frequency to 1Hz..")
+        try:
+            fgen.ch1.set_frequency(1)
+        except NotSetError as err:
+            print(err)
+
+
+
+def example_lock_frequencies(address) +
+
+

Example showing the frequency being set to 10Hz and then the frequency +lock enabled, using the frequency at ch1 as the common frequency

+
+ +Expand source code + +
def example_lock_frequencies(address):
+    """Example showing the frequency being set to 10Hz and then the frequency
+    lock enabled, using the frequency at ch1 as the common frequency"""
+    print("\n\n", example_lock_frequencies.__doc__)
+    with FuncGen(address, verbose=False) as fgen:
+        fgen.ch1.set_frequency(10)
+        fgen.set_frequency_lock("ON", use_channel=1)
+
+
-def example_set_and_use_custom_waveform(fgen=None, channel=1, plot_signal=True) +def example_set_and_use_custom_waveform(fgen=None, address=None, channel=1, plot_signal=True)

Example showing a waveform being created, transferred to the instrument, @@ -1026,33 +1382,37 @@

Returns

Expand source code -
def example_set_and_use_custom_waveform(fgen=None, channel=1, plot_signal=True):
+
def example_set_and_use_custom_waveform(fgen=None, address=None, channel=1,
+                                        plot_signal=True):
     """Example showing a waveform being created, transferred to the instrument,
     and applied to a channel"""
-    # create a signal
+    print("\n\n", example_set_and_use_custom_waveform.__doc__)
+    # Create a signal
     x = np.linspace(0, 4*np.pi, 8000)
     signal = np.sin(x)+x/5
     if plot_signal: # plot the signal for visual control
         import matplotlib.pyplot as plt
         plt.plot(signal)
         plt.show()
-    # create initialise fgen if it was not supplied
+    # Create initialise fgen if it was not supplied
     if fgen is None:
-        fgen = func_gen()
+        fgen = FuncGen(address)
         close_fgen = True # specify that it should be closed at end of function
     else:
         close_fgen = False # do not close the supplied fgen at end
     print("Current waveform catalogue")
-    for i, wav in enumerate(fgen.get_waveform_catalogue()): print("  {}: {}".format(i, wav))
-    # transfer the waveform
+    for i, wav in enumerate(fgen.get_waveform_catalogue()):
+        print("  {}: {}".format(i, wav))
+    # Transfer the waveform
     fgen.set_custom_waveform(signal, memory_num=5, verify=True)
     print("New waveform catalogue:")
-    for i, wav in enumerate(fgen.get_waveform_catalogue()): print("  {}: {}".format(i, wav))
+    for i, wav in enumerate(fgen.get_waveform_catalogue()):
+        print("  {}: {}".format(i, wav))
     print("Set new wavefrom to channel {}..".format(channel), end=" ")
-    fgen.channels[channel-1].set_output("OFF")
+    fgen.channels[channel-1].set_output_state("OFF")
     fgen.channels[channel-1].set_function("USER5")
     print("ok")
-    # print current settings
+    # Print current settings
     fgen.get_settings()
     if close_fgen: fgen.close()
@@ -1062,48 +1422,8 @@

Returns

Classes

-
-class NotCompatibleError -(*args, **kwargs) -
-
-

Error for when the instrument is not compatible with this module

-
- -Expand source code - -
class NotCompatibleError(Exception):
-    """Error for when the instrument is not compatible with this module"""
-    pass
-
-

Ancestors

-
    -
  • builtins.Exception
  • -
  • builtins.BaseException
  • -
-
-
-class NotSetError -(*args, **kwargs) -
-
-

Error for when a value cannot be written to the instrument

-
- -Expand source code - -
class NotSetError(Exception):
-    """Error for when a value cannot be written to the instrument"""
-    pass
-
-

Ancestors

-
    -
  • builtins.Exception
  • -
  • builtins.BaseException
  • -
-
-
-class func_gen +
+class FuncGen (address, impedance=('highZ', 'highZ'), timeout=5000, verify_param_set=False, override_compatibility=False, verbose=True)
@@ -1134,28 +1454,32 @@

Attributes

address : str
The VISA address of the instrument
id : str
-
Comma separated string with maker, model, serial and firmware of the instrument
+
Comma separated string with maker, model, serial and firmware of +the instrument
inst : pyvisa.resources.Resource
The PyVISA resource
-
channels : tuple of func_gen_channel
+
channels : tuple of FuncGenChannel
Objects to control the channels
-
ch1 : func_gen_channel
+
ch1 : FuncGenChannel
Short hand for channels[0] Object to control channel 1
-
ch2 : func_gen_channel
+
ch2 : FuncGenChannel
Short hand for channels[1] Object to control channel 2
instrument_limits : dict
Contains the following keys with subdictionaries frequency lims -Containing the frequency limits for the instrument where the keys "min" -and "max" have values corresponding to minimum and maximum frequencies in Hz +Containing the frequency limits for the instrument where the keys +"min" and "max" have values corresponding to minimum and maximum +frequencies in Hz voltage lims -Contains the maximum absolute voltage the instrument can output for the keys -"50ohm" and "highZ" according to the impedance setting +Contains the maximum absolute voltage the instrument can output +for the keys "50ohm" and "highZ" according to the impedance setting amplitude lims -Contains the smallest and largest possible amplitudes where the keys -"50ohm" and "highZ" will have subdictionaries with keys "min" and "max"
+Contains the smallest and largest possible amplitudes where the +keys "50ohm" and "highZ" will have subdictionaries with keys +"min" and "max"
arbitrary_waveform_length : list
-
The permitted minimum and maximum length of an arbitrary waveform, e.g. [2, 8192]
+
The permitted minimum and maximum length of an arbitrary waveform, +e.g. [2, 8192]
arbitrary_waveform_resolution : int
The vertical resolution of the arbitrary waveform, for instance 14 bit => 2**14-1 = 16383
@@ -1165,13 +1489,15 @@

Raises

pyvisa.Error
If the supplied VISA address cannot be connected to
NotCompatibleError
-
If the instrument limits for the model connected to are not known (Call the class with override_compatibility=True to override and use AFG1022 limits)
+
If the instrument limits for the model connected to are not known +(Call the class with override_compatibility=True to override and +use AFG1022 limits)
Expand source code -
class func_gen():
+
class FuncGen():
     """Class for interacting with Tektronix function generator
 
     Parameters
@@ -1200,28 +1526,32 @@ 

Raises

address : str The VISA address of the instrument id : str - Comma separated string with maker, model, serial and firmware of the instrument + Comma separated string with maker, model, serial and firmware of + the instrument inst : `pyvisa.resources.Resource` The PyVISA resource - channels : tuple of func_gen_channel + channels : tuple of FuncGenChannel Objects to control the channels - ch1 : func_gen_channel + ch1 : FuncGenChannel Short hand for `channels[0]` Object to control channel 1 - ch2 : func_gen_channel + ch2 : FuncGenChannel Short hand for `channels[1]` Object to control channel 2 instrument_limits : dict Contains the following keys with subdictionaries `frequency lims` - Containing the frequency limits for the instrument where the keys "min" - and "max" have values corresponding to minimum and maximum frequencies in Hz + Containing the frequency limits for the instrument where the keys + "min" and "max" have values corresponding to minimum and maximum + frequencies in Hz `voltage lims` - Contains the maximum absolute voltage the instrument can output for the keys - "50ohm" and "highZ" according to the impedance setting + Contains the maximum absolute voltage the instrument can output + for the keys "50ohm" and "highZ" according to the impedance setting `amplitude lims` - Contains the smallest and largest possible amplitudes where the keys - "50ohm" and "highZ" will have subdictionaries with keys "min" and "max" + Contains the smallest and largest possible amplitudes where the + keys "50ohm" and "highZ" will have subdictionaries with keys + "min" and "max" arbitrary_waveform_length : list - The permitted minimum and maximum length of an arbitrary waveform, e.g. [2, 8192] + The permitted minimum and maximum length of an arbitrary waveform, + e.g. [2, 8192] arbitrary_waveform_resolution : int The vertical resolution of the arbitrary waveform, for instance 14 bit => 2**14-1 = 16383 @@ -1231,11 +1561,14 @@

Raises

pyvisa.Error If the supplied VISA address cannot be connected to NotCompatibleError - If the instrument limits for the model connected to are not known (Call the class with `override_compatibility=True` to override and use AFG1022 limits) + If the instrument limits for the model connected to are not known + (Call the class with `override_compatibility=True` to override and + use AFG1022 limits) """ def __init__(self, address, impedance=("highZ",)*2, timeout=5000, - verify_param_set=False, override_compatibility=False, verbose=True): + verify_param_set=False, override_compatibility=False, + verbose=True): self.override_compatibility = override_compatibility self.address = address self.timeout = timeout @@ -1248,24 +1581,26 @@

Raises

print("\nVisaError: Could not connect to \'{}\'".format(address)) raise self.inst.timeout = self.timeout - # Clear all the event registers and queues used in the instrument status and event reporting system + # Clear all the event registers and queues used in the instrument + # status and event reporting system self.write("*CLS") # Get information about the connected device - self.id = self.query("*IDN?") # get the id of the connected device - if self.id == '': # second query might be needed due to unknown reason (suspecting *CLS does something weird) + self.id = self.query("*IDN?") + # Second query might be needed due to unknown reason + # (suspecting *CLS does something weird) + if self.id == '': self.id = self.query("*IDN?") self.maker, self.model, self.serial = self.id.split(",")[:3] - if self.verbose: print("Connected to {} model {}, serial {}".format(self.maker, self.model, self.serial)) + if self.verbose: + print("Connected to {} model {}, serial" + " {}".format(self.maker, self.model, self.serial)) self.initialise_model_properties() - self.channels = self.spawn_channel(1, impedance[0]), self.spawn_channel(2, impedance[1]) + self.channels = (self.spawn_channel(1, impedance[0]), + self.spawn_channel(2, impedance[1])) self.ch1, self.ch2 = self.channels - # __enter__ and __exit__ functions defined to use the module with a 'with' statement - # with func_gen(verify_param_set=True) as fgen: - # fgen.get_settings() - # This gives automatic handling of closing down the connection when fgen goes out of scope - # The kwargs in __enter__ will be passed on to __init__ def __enter__(self, **kwargs): + # The kwargs will be passed on to __init__ return self def __exit__(self, exc_type, exc_val, exc_tb): @@ -1285,20 +1620,25 @@

Raises

Raises ------ NotCompatibleError - If the connected model is not necessarily compatible with this package, - limits are not known. + If the connected model is not necessarily compatible with this + package, slimits are not known. """ if self.model == 'AFG1022' or self.override_compatibility: self.instrument_limits = { - "frequency lims": ({"min": 1e-6, "max": 25e6}, "Hz"), - "voltage lims": ({"50ohm": {"min": -5, "max": 5}, - "highZ": {"min": -10, "max": 10}}, "V"), - "amplitude lims": ({"50ohm": {"min": 0.001, "max": 10}, - "highZ": {"min": 0.002, "max": 20}}, "Vpp")} - self.arbitrary_waveform_length = [2, 8192] # min length, max length - self.arbitrary_waveform_resolution = 16383 # 14 bit + "frequency lims": ({"min": 1e-6, "max": 25e6}, "Hz"), + "voltage lims": ({"50ohm": {"min": -5, "max": 5}, + "highZ": {"min": -10, "max": 10}}, "V"), + "amplitude lims": ({"50ohm": {"min": 0.001, "max": 10}, + "highZ": {"min": 0.002, "max": 20}}, "Vpp")} + self.arbitrary_waveform_length = [2, 8192] # min length, max length + self.arbitrary_waveform_resolution = 16383 # 14 bit else: - raise NotCompatibleError("Model {} not supported, no limits set!\n\tTo use the limits for AFG1022, call the class with 'override_compatibility=True'\n\tNote that this might lead to unexpected behaviour for custom waveforms and 'MIN'/'MAX' keywords.") + msg = ("Model {} not supported, no limits set!" + "\n\tTo use the limits for AFG1022, call the class with " + "'override_compatibility=True'" + "\n\tNote that this might lead to unexpected behaviour " + "for custom waveforms and 'MIN'/'MAX' keywords.") + raise NotCompatibleError(msg) def write(self, command, custom_err_message=None): """Write a VISA command to the instrument @@ -1308,8 +1648,10 @@

Raises

command : str The VISA command to be written to the instrument custom_err_message : str, default `None` - When `None`, the RuntimeError message is "Writing command {command} failed: pyvisa returned StatusCode ..". - Otherwise, if a message is supplied "Could not {message}: pyvisa returned StatusCode .." + When `None`, the RuntimeError message is "Writing command {command} + failed: pyvisa returned StatusCode ..". + Otherwise, if a message is supplied "Could not {message}: + pyvisa returned StatusCode .." Returns ------- @@ -1321,15 +1663,20 @@

Raises

Raises ------ RuntimeError - If status returned by PyVISA write command is not `pyvisa.constants.StatusCode.success` + If status returned by PyVISA write command is not + `pyvisa.constants.StatusCode.success` """ - bytes, status = self.inst.write(command) + num_bytes, status = self.inst.write(command) if not status == pyvisa.constants.StatusCode.success: if custom_err_message is not None: - raise RuntimeError("Could not {}: pyvisa returned StatusCode {} ({})".format(custom_err_message, status, str(status))) + msg = ("Could not {}: pyvisa returned StatusCode {} " + "({})".format(custom_err_message, status, str(status))) + raise RuntimeError(msg) else: - raise RuntimeError("Writing command {} failed: pyvisa returned StatusCode {} ({})".format(command, status, str(status))) - return bytes, status + msg = ("Writing command {} failed: pyvisa returned StatusCode" + " {} ({})".format(command, status, str(status))) + raise RuntimeError(msg) + return num_bytes, status def query(self, command): """Query the instrument @@ -1357,18 +1704,13 @@

Raises

return self.query("SYSTEM:ERROR:NEXT?") def spawn_channel(self, channel, impedance): - """Wrapper function to create a `func_gen_channel` object for + """Wrapper function to create a `FuncGenChannel` object for a channel -- see the class docstring""" - return func_gen_channel(self, channel, impedance) + return FuncGenChannel(self, channel, impedance) def get_settings(self): """Get dictionaries of the current settings of the two channels - Parameters - ---------- - print_settings : bool, default `True` - `True` prints table of the current setting for both channels - Returns ------- settings : list of dicts @@ -1381,25 +1723,46 @@

Raises

def print_settings(self): """Prints table of the current setting for both channels""" settings = self.get_settings() - # find the necessary padding for the table columns + # Find the necessary padding for the table columns # by evaluating the maximum length of the entries key_padding = max([len(key) for key in settings[0].keys()]) - ch_paddings = [max([len(str(val[0])) for val in ch_settings.values()]) for ch_settings in settings] + ch_paddings = [max([len(str(val[0])) for val in ch_settings.values()]) + for ch_settings in settings] padding = [key_padding]+ch_paddings - print("\nCurrent settings for {} {} {}\n".format(self.maker, self.model, self.serial)) - + print("\nCurrent settings for {} {} {}\n".format(self.maker, + self.model, + self.serial)) row_format = "{:>{padd[0]}s} {:{padd[1]}s} {:{padd[2]}s} {}" - table_header = row_format.format("Setting", "Ch1", "Ch2", "Unit", padd=padding) + table_header = row_format.format("Setting", "Ch1", "Ch2", + "Unit", padd=padding) print(table_header) print("="*len(table_header)) - for (ch1key, (ch1val, unit)), (ch2key, (ch2val, _)) in zip(settings[0].items(), settings[1].items()): - print(row_format.format(ch1key, str(ch1val), str(ch2val), unit, padd=padding)) + for (ch1key, (ch1val, unit)), (_, (ch2val, _)) in zip(settings[0].items(), + settings[1].items()): + print(row_format.format(ch1key, str(ch1val), str(ch2val), + unit, padd=padding)) + + def set_settings(self, settings): + """Set the settings of both channels with settings dictionaries + + (Each channel is turned off before applying the changes to avoid + potenitally harmful combinations) + + Parameteres + ----------- + settings : list of dicts + List of settings dictionaries as returned by `get_settings`, first + entry for channel 1, second for channel 2. The dictionaries should + have keys output, function, amplitude, offset, and frequency + """ + for ch, s in zip(self.channels, settings): + ch.set_settings(s) def syncronise_waveforms(self): """Syncronise waveforms of the two channels when using the same frequency - Note: Does NOT enable the frequency lock that can be enabled on the user - interface of the instrument) + Note: Does NOT enable the frequency lock that can be enabled on the + user interface of the instrument) """ self.write(":PHAS:INIT", custom_err_message="syncronise waveforms") @@ -1411,39 +1774,44 @@

Raises

bool `True` if frequency lock enabled """ - # if one is locked so is the other, so just need to check one + # If one is locked so is the other, so just need to check one return int(self.query("SOURCE1:FREQuency:CONCurrent?")) == 1 - def enable_frequency_lock(self, use_channel=1): - """Enable the frequency lock to make the two channels have the same frequency - and phase of their signals, also after adjustments. + def set_frequency_lock(self, state, use_channel=1): + """Enable the frequency lock to make the two channels have the same + frequency and phase of their signals, also after adjustments. - See also `func_gen.syncronise_waveforms` for one-time sync only. + See also `FuncGen.syncronise_waveforms` for one-time sync only. Parameters ---------- + state : {"ON", "OFF"} + ON to enable, OFF to disable the lock use_channel : int, default 1 - The channel whose frequency shall be used as the common freqency - """ - self.write("SOURCE{}:FREQuency:CONCurrent ON".format(use_channel), - custom_err_message="enable frequency lock") - - def disable_frequency_lock(self): - """Disable the frequency lock that makes the two channels have the same - frequency and phase of their signals, also after adjustments + Only relevant if turning the lock ON: The channel whose frequency + shall be used as the common freqency """ - if self.get_frequency_lock(): - # sufficient to disable for only one of the channels - self.write("SOURCE1:FREQuency:CONCurrent OFF") - else: - if self.verbose: print("(!) {}: Tried to disable frequency lock, but frequency lock was not enabled".format(self.model)) + if self.verbose: + if state.lower() == "off" and not self.get_frequency_lock(): + print("(!) {}: Tried to disable frequency lock, but " + "frequency lock was not enabled".format(self.model)) + return + if state.lower() == "on" and self.get_frequency_lock(): + print("(!) {}: Tried to enable frequency lock, but " + "frequency lock was already enabled".format(self.model)) + return + # (Sufficient to disable for only one of the channels) + cmd = "SOURCE{}:FREQuency:CONCurrent {}".format(use_channel, state) + msg = "turn frequency lock {}".format(state) + self.write(cmd, custom_err_message=msg) def software_trig(self): - """NOT TESTED: sends a trigger signal to the device (for bursts or modulations)""" + """NOT TESTED: sends a trigger signal to the device + (for bursts or modulations)""" self.write("*TRG", custom_err_message="send trigger signal") - ## ~~~~~~~~~~~~~~~~~~~~~ CUSTOM WAVEFORM FUNCTIONS ~~~~~~~~~~~~~~~~~~~~~~ ## + ## ~~~~~~~~~~~~~~~~~~~~~ CUSTOM WAVEFORM FUNCTIONS ~~~~~~~~~~~~~~~~~~~~~ ## def get_waveform_catalogue(self): """Get list of the waveforms that are in use (not empty) @@ -1454,7 +1822,7 @@

Raises

Strings with the names of the user functions that are not empty """ catalogue = self.query("DATA:CATalog?").split(",") - catalogue = [wf[1:-1] for wf in catalogue] # strip off extra quotes + catalogue = [wf[1:-1] for wf in catalogue] # strip off extra quotes return catalogue def get_custom_waveform(self, memory_num): @@ -1471,33 +1839,40 @@

Raises

Waveform as ints spanning the resolution of the function gen or and empty array if waveform not in use """ - # find the wavefroms in use + # Find the wavefroms in use waveforms_in_use = self.get_waveform_catalogue() if "USER{}".format(memory_num) in waveforms_in_use: - # copy the waveform to edit memory + # Copy the waveform to edit memory self.write("DATA:COPY EMEMory,USER{}".format(memory_num)) - # get the length of the waveform + # Get the length of the waveform waveform_length = int(self.query("DATA:POINts? EMEMory")) - # get the waveform (returns binary values) - waveform = self.inst.query_binary_values("DATA:DATA? EMEMory", datatype='H', is_big_endian=True, container=np.ndarray) - assert len(waveform) == waveform_length, "Waveform length from native length command (DATA:POINts?) and the processed binary values do not match, {} and {} respectively".format(waveform_length, len(waveform)) + # Get the waveform (returns binary values) + waveform = self.inst.query_binary_values("DATA:DATA? EMEMory", + datatype='H', + is_big_endian=True, + container=np.ndarray) + msg = ("Waveform length from native length command (DATA:POINts?) " + "and the processed binary values do not match, {} and {} " + "respectively".format(waveform_length, len(waveform))) + assert len(waveform) == waveform_length, msg return waveform else: print("Waveform USER{} is not in use".format(memory_num)) return np.array([]) - def set_custom_waveform(self, waveform, normalise=True, memory_num=0, verify=True, print_progress=True): + def set_custom_waveform(self, waveform, normalise=True, memory_num=0, + verify=True, print_progress=True): """Transfer waveform data to edit memory and then user memory. NOTE: Will overwrite without warnings Parameters ---------- waveform : ndarray - Either unnormalised arbitrary waveform (then use `normalise=True`), or - ints spanning the resolution of the function generator + Either unnormalised arbitrary waveform (then use `normalise=True`), + or ints spanning the resolution of the function generator normalise : bool - Choose whether to normalise the waveform to ints over the resolution - span of the function generator + Choose whether to normalise the waveform to ints over the + resolution span of the function generator memory_num : str or int {0,...,255}, default 0 Select which user memory to copy to verify : bool, default `True` @@ -1517,33 +1892,46 @@

Raises

If the waveform transferred to the instrument is of a different length than the waveform supplied """ - # check if waveform data is suitable - if print_progress: print("Check if waveform data is suitable..", end=" ") + # Check if waveform data is suitable + if print_progress: + print("Check if waveform data is suitable..", end=" ") self.check_arb_waveform_length(waveform) try: self.check_arb_waveform_type_and_range(waveform) except ValueError as e: - if print_progress: print("\n "+str(e)) - if print_progress: print("Trying again normalising the waveform..", end=" ") + if print_progress: + print("\n "+str(e)) + print("Trying again normalising the waveform..", end=" ") waveform = self.normalise_to_waveform(waveform) - if print_progress: print("ok") - # transfer waveform - if print_progress: print("Transfer waveform to function generator..", end=" ") - self.inst.write_binary_values("DATA:DATA EMEMory,", waveform, datatype='H', is_big_endian=True) - # The first query after the write_binary_values returns '', so here is a mock query + if print_progress: + print("ok") + print("Transfer waveform to function generator..", end=" ") + # Transfer waveform + self.inst.write_binary_values("DATA:DATA EMEMory,", waveform, + datatype='H', is_big_endian=True) + # The first query after the write_binary_values returns '', + # so here is a mock query self.query("") transfer_error = self.get_error() emem_wf_length = self.query("DATA:POINts? EMEMory") if emem_wf_length == '' or not int(emem_wf_length) == len(waveform): - raise RuntimeError("Waveform in temporary EMEMory has a length of {}, not of the same length as the waveform ({}).\nError from the instrument: {}".format(emem_wf_length, len(waveform), transfer_error)) - if print_progress: print("ok") - if print_progress: print("Copy waveform to USER{}..".format(memory_num), end=" ") + msg = ("Waveform in temporary EMEMory has a length of {}, not of " + "the same length as the waveform ({}).\nError from the " + "instrument: {}".format(emem_wf_length, len(waveform), + transfer_error)) + raise RuntimeError(msg) + if print_progress: + print("ok") + print("Copy waveform to USER{}..".format(memory_num), end=" ") self.write("DATA:COPY USER{},EMEMory".format(memory_num)) - if print_progress: print("ok") + if print_progress: + print("ok") if verify: - if print_progress: print("Verify waveform..".format(memory_num)) + if print_progress: + print("Verify waveform USER{}..".format(memory_num)) if "USER{}".format(memory_num) in self.get_waveform_catalogue(): - self.verify_waveform(waveform, memory_num, normalise=normalise, print_result=print_progress) + self.verify_waveform(waveform, memory_num, normalise=normalise, + print_result=print_progress) else: print("(!) USER{} is empty".format(memory_num)) return waveform @@ -1563,15 +1951,16 @@

Raises

waveform : ndarray Waveform as ints spanning the resolution of the function gen """ - # check if waveform data is suitable + # Check if waveform data is suitable self.check_arb_waveform_length(shape) - # normalise + # Normalise waveform = shape - np.min(shape) normalisation_factor = np.max(waveform) waveform = waveform/normalisation_factor*self.arbitrary_waveform_resolution return waveform.astype(np.uint16) - def verify_waveform(self, waveform, memory_num, normalise=True, print_result=True): + def verify_waveform(self, waveform, memory_num, normalise=True, + print_result=True): """Compare a waveform in user memory to argument waveform Parameters @@ -1593,25 +1982,32 @@

Raises

List of the indices where the waveforms are not equal or `None` if the waveforms were of different lengths """ - if normalise:# make sure test waveform is normalised + if normalise: # make sure test waveform is normalised waveform = self.normalise_to_waveform(waveform) - # get the waveform on the instrument + # Get the waveform on the instrument instrument_waveform = self.get_custom_waveform(memory_num) - # compare lengths + # Compare lengths len_inst_wav, len_wav = len(instrument_waveform), len(waveform) if not len_inst_wav == len_wav: - if print_result: print("The waveform in USER{} and the compared waveform are not of same length (instrument {} vs {})".format(memory_num, len_inst_wav, len_wav)) + if print_result: + print("The waveform in USER{} and the compared waveform are " + "not of same length (instrument {} vs {})" + "".format(memory_num, len_inst_wav, len_wav)) return False, instrument_waveform, None - # compare each element + # Compare each element not_equal = [] for i in range(len_wav): if not instrument_waveform[i] == waveform[i]: not_equal.append(i) - # return depending of whether list is empty or not + # Return depending of whether list is empty or not if not not_equal: # if list is empty - if print_result: print("The waveform in USER{} and the compared waveform are equal".format(memory_num)) + if print_result: + print("The waveform in USER{} and the compared waveform " + "are equal".format(memory_num)) return True, instrument_waveform, not_equal - if print_result: print("The waveform in USER{} and the compared waveform are NOT equal".format(memory_num)) + if print_result: + print("The waveform in USER{} and the compared waveform are " + "NOT equal".format(memory_num)) return False, instrument_waveform, not_equal def check_arb_waveform_length(self, waveform): @@ -1627,8 +2023,12 @@

Raises

ValueError If the waveform is not within the permitted length """ - if (len(waveform) < self.arbitrary_waveform_length[0]) or (len(waveform) > self.arbitrary_waveform_length[1]): - raise ValueError("The waveform is of length {}, which is not within the acceptable length {} < len < {}".format(len(waveform), *self.arbitrary_waveform_length)) + if ((len(waveform) < self.arbitrary_waveform_length[0]) or + (len(waveform) > self.arbitrary_waveform_length[1])): + msg = ("The waveform is of length {}, which is not within the " + "acceptable length {} < len < {}" + "".format(len(waveform), *self.arbitrary_waveform_length)) + raise ValueError(msg) def check_arb_waveform_type_and_range(self, waveform): """Checks if waveform is of int/np.int32 type and within the resolution @@ -1647,13 +2047,17 @@

Raises

""" for value in waveform: if not isinstance(value, (int, np.uint16, np.int32)): - raise ValueError("The waveform contains values that are not int, np.uint16 or np.int32") + raise ValueError("The waveform contains values that are not" + "int, np.uint16 or np.int32") if (value < 0) or (value > self.arbitrary_waveform_resolution): - raise ValueError("The waveform contains values out of range ({} is not within the resolution [0, {}])".format(value, self.arbitrary_waveform_resolution))
+ raise ValueError("The waveform contains values out of range " + "({} is not within the resolution " + "[0, {}])".format(value, + self.arbitrary_waveform_resolution))

Methods

-
+
def check_arb_waveform_length(self, waveform)
@@ -1685,11 +2089,15 @@

Raises

ValueError If the waveform is not within the permitted length """ - if (len(waveform) < self.arbitrary_waveform_length[0]) or (len(waveform) > self.arbitrary_waveform_length[1]): - raise ValueError("The waveform is of length {}, which is not within the acceptable length {} < len < {}".format(len(waveform), *self.arbitrary_waveform_length))
+ if ((len(waveform) < self.arbitrary_waveform_length[0]) or + (len(waveform) > self.arbitrary_waveform_length[1])): + msg = ("The waveform is of length {}, which is not within the " + "acceptable length {} < len < {}" + "".format(len(waveform), *self.arbitrary_waveform_length)) + raise ValueError(msg)
-
+
def check_arb_waveform_type_and_range(self, waveform)
@@ -1727,12 +2135,16 @@

Raises

""" for value in waveform: if not isinstance(value, (int, np.uint16, np.int32)): - raise ValueError("The waveform contains values that are not int, np.uint16 or np.int32") + raise ValueError("The waveform contains values that are not" + "int, np.uint16 or np.int32") if (value < 0) or (value > self.arbitrary_waveform_resolution): - raise ValueError("The waveform contains values out of range ({} is not within the resolution [0, {}])".format(value, self.arbitrary_waveform_resolution))
+ raise ValueError("The waveform contains values out of range " + "({} is not within the resolution " + "[0, {}])".format(value, + self.arbitrary_waveform_resolution))
-
+
def close(self)
@@ -1746,59 +2158,7 @@

Raises

self.inst.close()
-
-def disable_frequency_lock(self) -
-
-

Disable the frequency lock that makes the two channels have the same -frequency and phase of their signals, also after adjustments

-
- -Expand source code - -
def disable_frequency_lock(self):
-    """Disable the frequency lock that makes the two channels have the same
-    frequency and phase of their signals, also after adjustments
-    """
-    if self.get_frequency_lock():
-        # sufficient to disable for only one of the channels
-        self.write("SOURCE1:FREQuency:CONCurrent OFF")
-    else:
-        if self.verbose: print("(!) {}: Tried to disable frequency lock, but frequency lock was not enabled".format(self.model))
-
-
-
-def enable_frequency_lock(self, use_channel=1) -
-
-

Enable the frequency lock to make the two channels have the same frequency -and phase of their signals, also after adjustments.

-

See also func_gen.syncronise_waveforms() for one-time sync only.

-

Parameters

-
-
use_channel : int, default 1
-
The channel whose frequency shall be used as the common freqency
-
-
- -Expand source code - -
def enable_frequency_lock(self, use_channel=1):
-    """Enable the frequency lock to make the two channels have the same frequency
-    and phase of their signals, also after adjustments.
-
-    See also `func_gen.syncronise_waveforms` for one-time sync only.
-
-    Parameters
-    ----------
-    use_channel : int, default 1
-        The channel whose frequency shall be used as the common freqency
-    """
-    self.write("SOURCE{}:FREQuency:CONCurrent ON".format(use_channel),
-               custom_err_message="enable frequency lock")
-
-
-
+
def get_custom_waveform(self, memory_num)
@@ -1832,23 +2192,29 @@

Returns

Waveform as ints spanning the resolution of the function gen or and empty array if waveform not in use """ - # find the wavefroms in use + # Find the wavefroms in use waveforms_in_use = self.get_waveform_catalogue() if "USER{}".format(memory_num) in waveforms_in_use: - # copy the waveform to edit memory + # Copy the waveform to edit memory self.write("DATA:COPY EMEMory,USER{}".format(memory_num)) - # get the length of the waveform + # Get the length of the waveform waveform_length = int(self.query("DATA:POINts? EMEMory")) - # get the waveform (returns binary values) - waveform = self.inst.query_binary_values("DATA:DATA? EMEMory", datatype='H', is_big_endian=True, container=np.ndarray) - assert len(waveform) == waveform_length, "Waveform length from native length command (DATA:POINts?) and the processed binary values do not match, {} and {} respectively".format(waveform_length, len(waveform)) + # Get the waveform (returns binary values) + waveform = self.inst.query_binary_values("DATA:DATA? EMEMory", + datatype='H', + is_big_endian=True, + container=np.ndarray) + msg = ("Waveform length from native length command (DATA:POINts?) " + "and the processed binary values do not match, {} and {} " + "respectively".format(waveform_length, len(waveform))) + assert len(waveform) == waveform_length, msg return waveform else: print("Waveform USER{} is not in use".format(memory_num)) return np.array([])
-
+
def get_error(self)
@@ -1873,7 +2239,7 @@

Returns

return self.query("SYSTEM:ERROR:NEXT?")
-
+
def get_frequency_lock(self)
@@ -1895,20 +2261,15 @@

Returns

bool `True` if frequency lock enabled """ - # if one is locked so is the other, so just need to check one + # If one is locked so is the other, so just need to check one return int(self.query("SOURCE1:FREQuency:CONCurrent?")) == 1
-
+
def get_settings(self)

Get dictionaries of the current settings of the two channels

-

Parameters

-
-
print_settings : bool, default True
-
True prints table of the current setting for both channels
-

Returns

settings : list of dicts
@@ -1923,11 +2284,6 @@

Returns

def get_settings(self):
     """Get dictionaries of the current settings of the two channels
 
-    Parameters
-    ----------
-    print_settings : bool, default `True`
-        `True` prints table of the current setting for both channels
-
     Returns
     -------
     settings : list of dicts
@@ -1938,7 +2294,7 @@ 

Returns

return [ch.get_settings() for ch in self.channels]
-
+
def get_waveform_catalogue(self)
@@ -1961,11 +2317,11 @@

Returns

Strings with the names of the user functions that are not empty """ catalogue = self.query("DATA:CATalog?").split(",") - catalogue = [wf[1:-1] for wf in catalogue] # strip off extra quotes + catalogue = [wf[1:-1] for wf in catalogue] # strip off extra quotes return catalogue
-
+
def initialise_model_properties(self)
@@ -1974,8 +2330,8 @@

Returns

Raises

NotCompatibleError
-
If the connected model is not necessarily compatible with this package, -limits are not known.
+
If the connected model is not necessarily compatible with this +package, slimits are not known.
@@ -1988,23 +2344,28 @@

Raises

Raises ------ NotCompatibleError - If the connected model is not necessarily compatible with this package, - limits are not known. + If the connected model is not necessarily compatible with this + package, slimits are not known. """ if self.model == 'AFG1022' or self.override_compatibility: self.instrument_limits = { - "frequency lims": ({"min": 1e-6, "max": 25e6}, "Hz"), - "voltage lims": ({"50ohm": {"min": -5, "max": 5}, - "highZ": {"min": -10, "max": 10}}, "V"), - "amplitude lims": ({"50ohm": {"min": 0.001, "max": 10}, - "highZ": {"min": 0.002, "max": 20}}, "Vpp")} - self.arbitrary_waveform_length = [2, 8192] # min length, max length - self.arbitrary_waveform_resolution = 16383 # 14 bit + "frequency lims": ({"min": 1e-6, "max": 25e6}, "Hz"), + "voltage lims": ({"50ohm": {"min": -5, "max": 5}, + "highZ": {"min": -10, "max": 10}}, "V"), + "amplitude lims": ({"50ohm": {"min": 0.001, "max": 10}, + "highZ": {"min": 0.002, "max": 20}}, "Vpp")} + self.arbitrary_waveform_length = [2, 8192] # min length, max length + self.arbitrary_waveform_resolution = 16383 # 14 bit else: - raise NotCompatibleError("Model {} not supported, no limits set!\n\tTo use the limits for AFG1022, call the class with 'override_compatibility=True'\n\tNote that this might lead to unexpected behaviour for custom waveforms and 'MIN'/'MAX' keywords.")
+ msg = ("Model {} not supported, no limits set!" + "\n\tTo use the limits for AFG1022, call the class with " + "'override_compatibility=True'" + "\n\tNote that this might lead to unexpected behaviour " + "for custom waveforms and 'MIN'/'MAX' keywords.") + raise NotCompatibleError(msg)
-
+
def normalise_to_waveform(self, shape)
@@ -2040,16 +2401,16 @@

Returns

waveform : ndarray Waveform as ints spanning the resolution of the function gen """ - # check if waveform data is suitable + # Check if waveform data is suitable self.check_arb_waveform_length(shape) - # normalise + # Normalise waveform = shape - np.min(shape) normalisation_factor = np.max(waveform) waveform = waveform/normalisation_factor*self.arbitrary_waveform_resolution return waveform.astype(np.uint16)
-
+
def print_settings(self)
@@ -2061,22 +2422,27 @@

Returns

def print_settings(self):
     """Prints table of the current setting for both channels"""
     settings = self.get_settings()
-    # find the necessary padding for the table columns
+    # Find the necessary padding for the table columns
     # by evaluating the maximum length of the entries
     key_padding = max([len(key) for key in settings[0].keys()])
-    ch_paddings = [max([len(str(val[0])) for val in ch_settings.values()]) for ch_settings in settings]
+    ch_paddings = [max([len(str(val[0])) for val in ch_settings.values()])
+                   for ch_settings in settings]
     padding = [key_padding]+ch_paddings
-    print("\nCurrent settings for {} {} {}\n".format(self.maker, self.model, self.serial))
-
+    print("\nCurrent settings for {} {} {}\n".format(self.maker,
+                                                     self.model,
+                                                     self.serial))
     row_format = "{:>{padd[0]}s} {:{padd[1]}s} {:{padd[2]}s} {}"
-    table_header = row_format.format("Setting", "Ch1", "Ch2", "Unit", padd=padding)
+    table_header = row_format.format("Setting", "Ch1", "Ch2",
+                                     "Unit", padd=padding)
     print(table_header)
     print("="*len(table_header))
-    for (ch1key, (ch1val, unit)), (ch2key, (ch2val, _)) in zip(settings[0].items(), settings[1].items()):
-        print(row_format.format(ch1key, str(ch1val), str(ch2val), unit, padd=padding))
+ for (ch1key, (ch1val, unit)), (_, (ch2val, _)) in zip(settings[0].items(), + settings[1].items()): + print(row_format.format(ch1key, str(ch1val), str(ch2val), + unit, padd=padding))
-
+
def query(self, command)
@@ -2111,7 +2477,7 @@

Returns

return self.inst.query(command).strip()
-
+
def set_custom_waveform(self, waveform, normalise=True, memory_num=0, verify=True, print_progress=True)
@@ -2120,11 +2486,11 @@

Returns

Parameters

waveform : ndarray
-
Either unnormalised arbitrary waveform (then use normalise=True), or -ints spanning the resolution of the function generator
+
Either unnormalised arbitrary waveform (then use normalise=True), +or ints spanning the resolution of the function generator
normalise : bool
-
Choose whether to normalise the waveform to ints over the resolution -span of the function generator
+
Choose whether to normalise the waveform to ints over the +resolution span of the function generator
memory_num : str or int {0,...,255}, default 0
Select which user memory to copy to
verify : bool, default True
@@ -2149,18 +2515,19 @@

Raises

Expand source code -
def set_custom_waveform(self, waveform, normalise=True, memory_num=0, verify=True, print_progress=True):
+
def set_custom_waveform(self, waveform, normalise=True, memory_num=0,
+                        verify=True, print_progress=True):
     """Transfer waveform data to edit memory and then user memory.
     NOTE: Will overwrite without warnings
 
     Parameters
     ----------
     waveform : ndarray
-        Either unnormalised arbitrary waveform (then use `normalise=True`), or
-        ints spanning the resolution of the function generator
+        Either unnormalised arbitrary waveform (then use `normalise=True`),
+        or ints spanning the resolution of the function generator
     normalise : bool
-        Choose whether to normalise the waveform to ints over the resolution
-        span of the function generator
+        Choose whether to normalise the waveform to ints over the
+        resolution span of the function generator
     memory_num : str or int {0,...,255}, default 0
         Select which user memory to copy to
     verify : bool, default `True`
@@ -2180,75 +2547,173 @@ 

Raises

If the waveform transferred to the instrument is of a different length than the waveform supplied """ - # check if waveform data is suitable - if print_progress: print("Check if waveform data is suitable..", end=" ") + # Check if waveform data is suitable + if print_progress: + print("Check if waveform data is suitable..", end=" ") self.check_arb_waveform_length(waveform) try: self.check_arb_waveform_type_and_range(waveform) except ValueError as e: - if print_progress: print("\n "+str(e)) - if print_progress: print("Trying again normalising the waveform..", end=" ") + if print_progress: + print("\n "+str(e)) + print("Trying again normalising the waveform..", end=" ") waveform = self.normalise_to_waveform(waveform) - if print_progress: print("ok") - # transfer waveform - if print_progress: print("Transfer waveform to function generator..", end=" ") - self.inst.write_binary_values("DATA:DATA EMEMory,", waveform, datatype='H', is_big_endian=True) - # The first query after the write_binary_values returns '', so here is a mock query + if print_progress: + print("ok") + print("Transfer waveform to function generator..", end=" ") + # Transfer waveform + self.inst.write_binary_values("DATA:DATA EMEMory,", waveform, + datatype='H', is_big_endian=True) + # The first query after the write_binary_values returns '', + # so here is a mock query self.query("") transfer_error = self.get_error() emem_wf_length = self.query("DATA:POINts? EMEMory") if emem_wf_length == '' or not int(emem_wf_length) == len(waveform): - raise RuntimeError("Waveform in temporary EMEMory has a length of {}, not of the same length as the waveform ({}).\nError from the instrument: {}".format(emem_wf_length, len(waveform), transfer_error)) - if print_progress: print("ok") - if print_progress: print("Copy waveform to USER{}..".format(memory_num), end=" ") + msg = ("Waveform in temporary EMEMory has a length of {}, not of " + "the same length as the waveform ({}).\nError from the " + "instrument: {}".format(emem_wf_length, len(waveform), + transfer_error)) + raise RuntimeError(msg) + if print_progress: + print("ok") + print("Copy waveform to USER{}..".format(memory_num), end=" ") self.write("DATA:COPY USER{},EMEMory".format(memory_num)) - if print_progress: print("ok") + if print_progress: + print("ok") if verify: - if print_progress: print("Verify waveform..".format(memory_num)) + if print_progress: + print("Verify waveform USER{}..".format(memory_num)) if "USER{}".format(memory_num) in self.get_waveform_catalogue(): - self.verify_waveform(waveform, memory_num, normalise=normalise, print_result=print_progress) + self.verify_waveform(waveform, memory_num, normalise=normalise, + print_result=print_progress) else: print("(!) USER{} is empty".format(memory_num)) return waveform
-
+
+def set_frequency_lock(self, state, use_channel=1) +
+
+

Enable the frequency lock to make the two channels have the same +frequency and phase of their signals, also after adjustments.

+

See also FuncGen.syncronise_waveforms() for one-time sync only.

+

Parameters

+
+
state : {"ON", "OFF"}
+
ON to enable, OFF to disable the lock
+
use_channel : int, default 1
+
Only relevant if turning the lock ON: The channel whose frequency +shall be used as the common freqency
+
+
+ +Expand source code + +
def set_frequency_lock(self, state, use_channel=1):
+    """Enable the frequency lock to make the two channels have the same
+    frequency and phase of their signals, also after adjustments.
+
+    See also `FuncGen.syncronise_waveforms` for one-time sync only.
+
+    Parameters
+    ----------
+    state : {"ON", "OFF"}
+        ON to enable, OFF to disable the lock
+    use_channel : int, default 1
+        Only relevant if turning the lock ON: The channel whose frequency
+        shall be used as the common freqency
+    """
+    if self.verbose:
+        if state.lower() == "off" and not self.get_frequency_lock():
+            print("(!) {}: Tried to disable frequency lock, but "
+                  "frequency lock was not enabled".format(self.model))
+            return
+        if state.lower() == "on" and self.get_frequency_lock():
+            print("(!) {}: Tried to enable frequency lock, but "
+                  "frequency lock was already enabled".format(self.model))
+            return
+    # (Sufficient to disable for only one of the channels)
+    cmd = "SOURCE{}:FREQuency:CONCurrent {}".format(use_channel, state)
+    msg = "turn frequency lock {}".format(state)
+    self.write(cmd, custom_err_message=msg)
+
+
+
+def set_settings(self, settings) +
+
+

Set the settings of both channels with settings dictionaries

+

(Each channel is turned off before applying the changes to avoid +potenitally harmful combinations)

+

Parameteres

+
+
settings : list of dicts
+
List of settings dictionaries as returned by get_settings, first +entry for channel 1, second for channel 2. The dictionaries should +have keys output, function, amplitude, offset, and frequency
+
+
+ +Expand source code + +
def set_settings(self, settings):
+    """Set the settings of both channels with settings dictionaries
+
+    (Each channel is turned off before applying the changes to avoid
+    potenitally harmful combinations)
+
+    Parameteres
+    -----------
+    settings : list of dicts
+        List of settings dictionaries as returned by `get_settings`, first
+        entry for channel 1, second for channel 2. The dictionaries should
+        have keys output, function, amplitude, offset, and frequency
+    """
+    for ch, s in zip(self.channels, settings):
+        ch.set_settings(s)
+
+
+
def software_trig(self)
-

NOT TESTED: sends a trigger signal to the device (for bursts or modulations)

+

NOT TESTED: sends a trigger signal to the device +(for bursts or modulations)

Expand source code
def software_trig(self):
-    """NOT TESTED: sends a trigger signal to the device (for bursts or modulations)"""
+    """NOT TESTED: sends a trigger signal to the device
+    (for bursts or modulations)"""
     self.write("*TRG", custom_err_message="send trigger signal")
-
+
def spawn_channel(self, channel, impedance)
-

Wrapper function to create a func_gen_channel object for +

Wrapper function to create a FuncGenChannel object for a channel – see the class docstring

Expand source code
def spawn_channel(self, channel, impedance):
-    """Wrapper function to create a `func_gen_channel` object for
+    """Wrapper function to create a `FuncGenChannel` object for
     a channel -- see the class docstring"""
-    return func_gen_channel(self, channel, impedance)
+ return FuncGenChannel(self, channel, impedance)
-
+
def syncronise_waveforms(self)

Syncronise waveforms of the two channels when using the same frequency

-

Note: Does NOT enable the frequency lock that can be enabled on the user -interface of the instrument)

+

Note: Does NOT enable the frequency lock that can be enabled on the +user interface of the instrument)

Expand source code @@ -2256,13 +2721,13 @@

Raises

def syncronise_waveforms(self):
     """Syncronise waveforms of the two channels when using the same frequency
 
-    Note: Does NOT enable the frequency lock that can be enabled on the user
-    interface of the instrument)
+    Note: Does NOT enable the frequency lock that can be enabled on the
+    user interface of the instrument)
     """
     self.write(":PHAS:INIT", custom_err_message="syncronise waveforms")
-
+
def verify_waveform(self, waveform, memory_num, normalise=True, print_result=True)
@@ -2290,7 +2755,8 @@

Returns

Expand source code -
def verify_waveform(self, waveform, memory_num, normalise=True, print_result=True):
+
def verify_waveform(self, waveform, memory_num, normalise=True,
+                    print_result=True):
     """Compare a waveform in user memory to argument waveform
 
     Parameters
@@ -2312,29 +2778,36 @@ 

Returns

List of the indices where the waveforms are not equal or `None` if the waveforms were of different lengths """ - if normalise:# make sure test waveform is normalised + if normalise: # make sure test waveform is normalised waveform = self.normalise_to_waveform(waveform) - # get the waveform on the instrument + # Get the waveform on the instrument instrument_waveform = self.get_custom_waveform(memory_num) - # compare lengths + # Compare lengths len_inst_wav, len_wav = len(instrument_waveform), len(waveform) if not len_inst_wav == len_wav: - if print_result: print("The waveform in USER{} and the compared waveform are not of same length (instrument {} vs {})".format(memory_num, len_inst_wav, len_wav)) + if print_result: + print("The waveform in USER{} and the compared waveform are " + "not of same length (instrument {} vs {})" + "".format(memory_num, len_inst_wav, len_wav)) return False, instrument_waveform, None - # compare each element + # Compare each element not_equal = [] for i in range(len_wav): if not instrument_waveform[i] == waveform[i]: not_equal.append(i) - # return depending of whether list is empty or not + # Return depending of whether list is empty or not if not not_equal: # if list is empty - if print_result: print("The waveform in USER{} and the compared waveform are equal".format(memory_num)) + if print_result: + print("The waveform in USER{} and the compared waveform " + "are equal".format(memory_num)) return True, instrument_waveform, not_equal - if print_result: print("The waveform in USER{} and the compared waveform are NOT equal".format(memory_num)) + if print_result: + print("The waveform in USER{} and the compared waveform are " + "NOT equal".format(memory_num)) return False, instrument_waveform, not_equal
-
+
def write(self, command, custom_err_message=None)
@@ -2344,8 +2817,10 @@

Parameters

command : str
The VISA command to be written to the instrument
custom_err_message : str, default None
-
When None, the RuntimeError message is "Writing command {command} failed: pyvisa returned StatusCode ..". -Otherwise, if a message is supplied "Could not {message}: pyvisa returned StatusCode .."
+
When None, the RuntimeError message is "Writing command {command} +failed: pyvisa returned StatusCode ..". +Otherwise, if a message is supplied "Could not {message}: +pyvisa returned StatusCode .."

Returns

@@ -2357,7 +2832,8 @@

Returns

Raises

RuntimeError
-
If status returned by PyVISA write command is not pyvisa.constants.StatusCode.success
+
If status returned by PyVISA write command is not +pyvisa.constants.StatusCode.success
@@ -2371,8 +2847,10 @@

Raises

command : str The VISA command to be written to the instrument custom_err_message : str, default `None` - When `None`, the RuntimeError message is "Writing command {command} failed: pyvisa returned StatusCode ..". - Otherwise, if a message is supplied "Could not {message}: pyvisa returned StatusCode .." + When `None`, the RuntimeError message is "Writing command {command} + failed: pyvisa returned StatusCode ..". + Otherwise, if a message is supplied "Could not {message}: + pyvisa returned StatusCode .." Returns ------- @@ -2384,28 +2862,33 @@

Raises

Raises ------ RuntimeError - If status returned by PyVISA write command is not `pyvisa.constants.StatusCode.success` + If status returned by PyVISA write command is not + `pyvisa.constants.StatusCode.success` """ - bytes, status = self.inst.write(command) + num_bytes, status = self.inst.write(command) if not status == pyvisa.constants.StatusCode.success: if custom_err_message is not None: - raise RuntimeError("Could not {}: pyvisa returned StatusCode {} ({})".format(custom_err_message, status, str(status))) + msg = ("Could not {}: pyvisa returned StatusCode {} " + "({})".format(custom_err_message, status, str(status))) + raise RuntimeError(msg) else: - raise RuntimeError("Writing command {} failed: pyvisa returned StatusCode {} ({})".format(command, status, str(status))) - return bytes, status
+ msg = ("Writing command {} failed: pyvisa returned StatusCode" + " {} ({})".format(command, status, str(status))) + raise RuntimeError(msg) + return num_bytes, status
-
-class func_gen_channel -(func_gen, channel, impedance) +
+class FuncGenChannel +(fgen, channel, impedance)

Class for controlling a channel on a function generator object

Parameters

-
func_gen : <a title="tektronix_func_gen.func_gen" href="#tektronix_func_gen.func_gen">func_gen</a>
+
fgen : <a title="tektronix_func_gen.FuncGen" href="#tektronix_func_gen.FuncGen">FuncGen</a>
The function generator object
channel : {1, 2}
The channel to be controlled
@@ -2415,7 +2898,7 @@

Parameters

Attributes

-
func_gen : <a title="tektronix_func_gen.func_gen" href="#tektronix_func_gen.func_gen">func_gen</a>
+
fgen : <a title="tektronix_func_gen.FuncGen" href="#tektronix_func_gen.FuncGen">FuncGen</a>
The function generator object for which the channel exists
channel : {1, 2}
The channel number
@@ -2425,7 +2908,7 @@

Attributes

source : str
"SOURce{}:" where {} is the channel number
settings_units : list
-
The units for the settings produced by func_gen_channel.get_settings
+
The units for the settings produced by FuncGenChannel.get_settings
state_str : dict
For conversion from states 1 and 2 to "ON" and "OFF"
@@ -2433,12 +2916,12 @@

Attributes

Expand source code -
class func_gen_channel:
+
class FuncGenChannel:
     """Class for controlling a channel on a function generator object
 
     Parameters
     ----------
-    func_gen : `func_gen`
+    fgen : `FuncGen`
         The function generator object
     channel : {1, 2}
         The channel to be controlled
@@ -2448,7 +2931,7 @@ 

Attributes

Attributes ---------- - func_gen : `func_gen` + fgen : `FuncGen` The function generator object for which the channel exists channel : {1, 2} The channel number @@ -2458,7 +2941,7 @@

Attributes

source : str "SOURce{}:" where {} is the channel number settings_units : list - The units for the settings produced by `func_gen_channel.get_settings` + The units for the settings produced by `FuncGenChannel.get_settings` state_str : dict For conversion from states 1 and 2 to "ON" and "OFF" """ @@ -2468,92 +2951,162 @@

Attributes

1 : "ON", 0 : "OFF"} """Dictionary for converting output states to "ON" and "OFF" """ - def __init__(self, func_gen, channel, impedance): - self.func_gen = func_gen + def __init__(self, fgen, channel, impedance): + self.fgen = fgen self.channel = channel self.source = "SOURce{}:".format(channel) self.impedance = impedance - # adopt limits dictionary from instrument - self.channel_limits = self.func_gen.instrument_limits + # Adopt limits dictionary from instrument + self.channel_limits = copy.deepcopy(self.fgen.instrument_limits) + + def impedance_dependent_limit(self, limit_type): + """Check if the limit type is impedance dependent (voltages) or + not (frequency) + + Returns + ------- + bool + `True` if the limit is impedance dependent + """ + try: # to access the key "min" to check if impedance must be selected + _ = self.channel_limits[limit_type][0]["min"] + return False + except KeyError: # if the key does not exist + # The impedance must be selected + return True def set_stricter_limits(self): """Set limits for the voltage and frequency limits of the channel output through a series of prompts""" - print("Set stricter voltage and frequency limits for channel {}".format(self.channel)) + print("Set stricter voltage and frequency limits " + "for channel {}".format(self.channel)) print("Use enter only to leave a limit unchanged.") # Go through the different limits in the instrument_limits dict - for limit_type, (inst_limit_dict, unit) in self.func_gen.instrument_limits.items(): - try: # to access the key "min" to check if impedance must be selected - _ = inst_limit_dict["min"] - use_impedance = False - except KeyError: # if the key does not exist - # the impedance must be selected - use_impedance = True - inst_limit_dict = inst_limit_dict[self.impedance] + for limit_type, (inst_limit_dict, unit) in self.fgen.instrument_limits.items(): + use_impedance = self.impedance_dependent_limit(limit_type) print("Set {} in {}".format(limit_type, unit), end=" ") if use_impedance: + inst_limit_dict = inst_limit_dict[self.impedance] print("[{} impedance limit]".format(self.impedance)) else: print("") # get new line # Go through the min and max for the limit type for key, inst_value in inst_limit_dict.items(): # prompt for new value - new_value = input(" {} (instrument limit {}{}): ".format(key, inst_value, unit)) + new_value = input(" {} (instrument limit {}{}): " + "".format(key, inst_value, unit)) if new_value == "": - # do not change if empty + # Do not change if empty print("\tLimit not changed") else: try: # to convert to float new_value = float(new_value) except ValueError: - print("\tLimit unchanged: Could not convert {} to float".format(new_value)) + print("\tLimit unchanged: Could not convert \'{}\' " + "to float".format(new_value)) continue # to next item in dict - # check that the new value is within the intrument limits - acceptable_min = key == "min" and new_value > inst_value - current_min = self.channel_limits[limit_type][0][self.impedance]["min"] if use_impedance else self.channel_limits[limit_type][0]["min"] - larger_than_min = new_value > current_min - acceptable_max = key == "max" and new_value < inst_value and larger_than_min - if acceptable_min or acceptable_max: # within the limits - # set the new channel_limit, using the impedance depending on the limit type - if use_impedance: - self.channel_limits[limit_type][0][self.impedance][key] = new_value - else: - self.channel_limits[limit_type][0][key] = new_value - print("\tNew limit set {}{}".format(new_value, unit)) - else: # print description of why the limit was not set - if larger_than_min: - reason = "larger" if key == "max" else "smaller" - print("\tNew limit NOT set: {}{unit} is {} than the instrument limit ({}{unit})".format(new_value, reason, inst_value, unit=unit)) - else: - print("\tNew limit NOT set: {}{unit} is smaller than the current set minimum ({}{unit})".format(new_value, current_min, unit=unit)) - - # get currently used parameters from function generator + # Set the new limit + self.set_limit(limit_type, key, new_value, verbose=True) + + def set_limit(self, limit_type, bound, new_value, verbose=False): + """Set a limit if the new value is within the instrument limits and are + self consistent (max larger than min) + + Parameterers + ------------ + limit_type : str + The name of the limit in the channel_limits dictionary + bound : {"min", "max"} + Specifies if it is the max or the min limit that is to be set + new_value : float + The new value to be used for the limit + verbose : bool + Print confirmation that the limit was set or reason for why the + limit was not set + + Returns + ------- + bool + `True` if new limit set, `False` otherwise + """ + # Short hand references + inst_limit_dict = self.fgen.instrument_limits[limit_type] + channel_limit_dict = self.channel_limits[limit_type] + # Find the instrument limit and unit + use_impedance = self.impedance_dependent_limit(limit_type) + if use_impedance: + inst_value = inst_limit_dict[0][self.impedance][bound] + else: + inst_value = inst_limit_dict[0][bound] + unit = inst_limit_dict[1] + # Check that the new value is within the intrument limits + acceptable_min = bound == "min" and new_value > inst_value + if use_impedance: + current_min = channel_limit_dict[0][self.impedance]["min"] + else: + current_min = channel_limit_dict[0]["min"] + larger_than_min = new_value > current_min + acceptable_max = bound == "max" and new_value < inst_value and larger_than_min + if acceptable_min or acceptable_max: # within the limits + # Set the new channel_limit, using the impedance depending on the + # limit type. Beware that the shorthand cannot be used, as this + # only changes the shorthand not the dictionary itself + if use_impedance: + self.channel_limits[limit_type][0][self.impedance][bound] = new_value + else: + self.channel_limits[limit_type][0][bound] = new_value + if verbose: + print("\tNew limit set {}{}".format(new_value, unit)) + return True + elif verbose: # print description of why the limit was not set + if larger_than_min: + reason = "larger" if bound == "max" else "smaller" + print("\tNew limit NOT set: {}{unit} is {} than the instrument " + "limit ({}{unit})".format(new_value, reason, inst_value, + unit=unit)) + else: + print("\tNew limit NOT set: {}{unit} is smaller than the " + "current set minimum ({}{unit})".format(new_value, + current_min, + unit=unit)) + return False + + # Get currently used parameters from function generator def get_output_state(self): """Returns "0" for "OFF", "1" for "ON" """ - return self.func_gen.query("OUTPut{}:STATe?".format(self.channel)) + return self.fgen.query("OUTPut{}:STATe?".format(self.channel)) + def get_function(self): """Returns string of function name""" - return self.func_gen.query("{}FUNCtion:SHAPe?".format(self.source)) + return self.fgen.query("{}FUNCtion:SHAPe?".format(self.source)) + def get_amplitude(self): """Returns peak-to-peak voltage in volts""" - return float(self.func_gen.query("{}VOLTage:AMPLitude?".format(self.source))) + return float(self.fgen.query("{}VOLTage:AMPLitude?".format(self.source))) + def get_offset(self): """Returns offset voltage in volts""" - return float(self.func_gen.query("{}VOLTage:OFFSet?".format(self.source))) + return float(self.fgen.query("{}VOLTage:OFFSet?".format(self.source))) + def get_frequency(self): """Returns frequency in Hertz""" - return float(self.func_gen.query("{}FREQuency?".format(self.source))) + return float(self.fgen.query("{}FREQuency?".format(self.source))) - # get limits set in the channel class + # Get limits set in the channel class def get_frequency_lims(self): """Returns list of min and max frequency limits""" - return [self.channel_limits["frequency lims"][0][key] for key in ["min", "max"]] + return [self.channel_limits["frequency lims"][0][key] + for key in ["min", "max"]] + def get_voltage_lims(self): """Returns list of min and max voltage limits for the current impedance""" - return [self.channel_limits["voltage lims"][0][self.impedance][key] for key in ["min", "max"]] + return [self.channel_limits["voltage lims"][0][self.impedance][key] + for key in ["min", "max"]] + def get_amplitude_lims(self): """Returns list of min and max amplitude limits for the current impedance""" - return [self.channel_limits["amplitude lims"][0][self.impedance][key] for key in ["min", "max"]] + return [self.channel_limits["amplitude lims"][0][self.impedance][key] + for key in ["min", "max"]] def get_settings(self): """Get the settings for the channel @@ -2561,9 +3114,9 @@

Attributes

Returns ------- current_settings : dict - Settings currently in use as a dictionary with keys output, function, - amplitude, offset, and frequency and values tuples of the corresponding - return and unit + Settings currently in use as a dictionary with keys output, + function, amplitude, offset, and frequency and values tuples of + the corresponding return and unit """ return {"output": (self.state_str[self.get_output_state()], ""), "function": (self.get_function(), ""), @@ -2572,17 +3125,39 @@

Attributes

"frequency": (self.get_frequency(), "Hz")} def print_settings(self): - """Print the settings currently in use for the channel - (Recommended to use the `func_gen.print_settings` for printing both channels) + """Print the settings currently in use for the channel (Recommended + to use the `FuncGen.print_settings` for printing both channels) """ settings = self.get_settings() longest_key = max([len(key) for key in settings.keys()]) print("\nCurrent settings for channel {}".format(self.channel)) print("==============================") - for key, (val, unit) in zip(settings.items()): - print("{:>{num_char}s} {} {}".format(key, val, unit, num_char=longest_key)) - - def set_output(self, state): + for key, (val, unit) in settings.items(): + print("{:>{num_char}s} {} {}".format(key, val, unit, + num_char=longest_key)) + + def set_settings(self, settings): + """Set the settings of the channel with a settings dictionary. Will + set the outout to OFF before applyign the settings (and turn the + channel ON or leave it OFF depending on the settings dict) + + Parameteres + ----------- + settings : dict + Settings dictionary as returned by `get_settings`: should have + keys output, function, amplitude, offset, and frequency + """ + # First turn off to ensure no potentially harmful + # combination of settings + self.set_output_state("OFF") + # Set settings according to dictionary + self.set_function(settings['function'][0]) + self.set_amplitude(settings['amplitude'][0]) + self.set_offset(settings['offset'][0]) + self.set_frequency(settings['frequency'][0]) + self.set_output_state(settings['output'][0]) + + def set_output_state(self, state): """Enables or diables the output of the channel Parameters @@ -2594,38 +3169,65 @@

Attributes

Raises ------ NotSetError - If `self.func_gen.verify_param_set` is `True` and the value after + If `self.fgen.verify_param_set` is `True` and the value after applying the set function does not match the value returned by the get function """ - self.func_gen.write("OUTPut{}:STATe {}".format(self.channel, state), - custom_err_message="turn channel {} to state {}".format(self.channel, state)) - if self.func_gen.verify_param_set: - actual_state = self.get_output() + err_msg = "turn channel {} to state {}".format(self.channel, state) + self.fgen.write("OUTPut{}:STATe {}".format(self.channel, state), + custom_err_message=err_msg) + if self.fgen.verify_param_set: + actual_state = self.get_output_state() if not actual_state == state: - raise NotSetError("Channel {} was not turned {}, it is {}.\nError from the instrument: {}".format(self.channel, state, self.state_str[actual_state], self.func_gen.get_error())) + msg = ("Channel {} was not turned {}, it is {}.\n" + "Error from the instrument: {}" + "".format(self.channel, state, + self.state_str[actual_state], + self.fgen.get_error())) + raise NotSetError(msg) + + def get_output(self): + """Wrapper for get_output_state""" + return self.get_output_state() + + def set_output(self, state): + """Wrapper for set_output_state""" + self.set_output_state(state) def set_function(self, shape): """Set the function shape of the output Parameters ---------- - shape : {SINusoid, SQUare, PULSe, RAMP, PRNoise, <Built_in>, USER[0], USER1, ..., USER255, EMEMory, EFILe} - <Built_in>::={StairDown|StairUp|Stair Up&Dwn|Trapezoid|RoundHalf|AbsSine|AbsHalfSine|ClippedSine|ChoppedSine|NegRamp|OscRise|OscDecay|CodedPulse|PosPulse|NegPulse|ExpRise|ExpDecay|Sinc|Tan|Cotan|SquareRoot|X^2|HaverSine|Lorentz|Ln(x)|X^3|CauchyDistr|BesselJ|BesselY|ErrorFunc|Airy|Rectangle|Gauss|Hamming|Hanning|Bartlett|Blackman|Laylight|Triangle|DC|Heart|Round|Chirp|Rhombus|Cardiac} + shape : {SINusoid, SQUare, PULSe, RAMP, PRNoise, <Built_in>, USER[0], + USER1, ..., USER255, EMEMory, EFILe} + <Built_in>::={StairDown|StairUp|Stair Up&Dwn|Trapezoid|RoundHalf| + AbsSine|AbsHalfSine|ClippedSine|ChoppedSine|NegRamp|OscRise| + OscDecay|CodedPulse|PosPulse|NegPulse|ExpRise|ExpDecay|Sinc| + Tan|Cotan|SquareRoot|X^2|HaverSine|Lorentz|Ln(x)|X^3|CauchyDistr| + BesselJ|BesselY|ErrorFunc|Airy|Rectangle|Gauss|Hamming|Hanning| + Bartlett|Blackman|Laylight|Triangle|DC|Heart|Round|Chirp|Rhombus| + Cardiac} Raises ------ NotSetError - If `self.func_gen.verify_param_set` is `True` and the value after + If `self.fgen.verify_param_set` is `True` and the value after applying the set function does not match the value returned by the get function """ - self.func_gen.write("{}FUNCtion:SHAPe {}".format(self.source, shape), - custom_err_message="set function {}".format(shape)) - if self.func_gen.verify_param_set: + cmd = "{}FUNCtion:SHAPe {}".format(self.source, shape) + self.fgen.write(cmd, custom_err_message="set function {}".format(shape)) + if self.fgen.verify_param_set: actual_shape = self.get_function() if not actual_shape == shape: - raise NotSetError("Function {} was not set on channel {}, it is {}. Check that the function name is correctly spelt. Run set_function.__doc__ to see available shapes.\nError from the instrument: {}".format(shape, self.channel, actual_shape, self.func_gen.get_error())) + msg = ("Function {} was not set on channel {}, it is {}. " + "Check that the function name is correctly spelt. " + "Run set_function.__doc__ to see available shapes.\n" + "Error from the instrument: {}" + "".format(shape, self.channel, actual_shape, + self.fgen.get_error())) + raise NotSetError(msg) def set_amplitude(self, amplitude): """Set the peak-to-peak amplitude in volts @@ -2633,43 +3235,60 @@

Attributes

Parameters ---------- amplitude : float or {"max", "min"} - 0.1mV or four digits resolution, "max" or "min" will set the amplitude - to the maximum or minimum limit given in `channel_limits` + 0.1mV or four digits resolution, "max" or "min" will set the + amplitude to the maximum or minimum limit given in `channel_limits` Raises ------ NotSetError - If `self.func_gen.verify_param_set` is `True` and the value after + If `self.fgen.verify_param_set` is `True` and the value after applying the set function does not match the value returned by the get function """ - # check if keyword min or max is given + # Check if keyword min or max is given if str(amplitude).lower() in ["min", "max"]: unit = "" # no unit for MIN/MAX - # look up what the limit is for this keyword + # Look up what the limit is for this keyword amplitude = self.channel_limits["amplitude lims"][0][self.impedance][str(amplitude).lower()] else: unit = "Vpp" - # check if the given amplitude is within the current limits + # Check if the given amplitude is within the current limits min_ampl, max_ampl = self.get_amplitude_lims() if amplitude < min_ampl or amplitude > max_ampl: - raise NotSetError("Could not set the amplitude {}{unit} as it is not within the amplitude limits set for the instrument [{}, {}]{unit}".format(amplitude, min_ampl, max_ampl, unit=unit)) - # check that the new amplitude will not violate voltage limits + msg = ("Could not set the amplitude {}{unit} as it is not " + "within the amplitude limits set for the instrument " + "[{}, {}]{unit}".format(amplitude, min_ampl, max_ampl, + unit=unit)) + raise NotSetError(msg) + # Check that the new amplitude will not violate voltage limits min_volt, max_volt = self.get_voltage_lims() current_offset = self.get_offset() - if amplitude/2-current_offset < min_volt or amplitude/2+current_offset > max_volt: - raise NotSetError("Could not set the amplitude {}{unit} as the amplitude combined with the offset ({}V) will be outside the absolute voltage limits [{}, {}]{unit}".format(amplitude, current_offset, min_volt, max_volt, unit=unit)) - # set the amplitude - self.func_gen.write("{}VOLTage:LEVel {}{}".format(self.source, amplitude, unit), - custom_err_message="set amplitude {}{}".format(amplitude, unit)) - # verify that the amplitude has been set - if self.func_gen.verify_param_set: + if (amplitude/2-current_offset < min_volt or + amplitude/2+current_offset > max_volt): + msg = ("Could not set the amplitude {}{unit} as the amplitude " + "combined with the offset ({}V) will be outside the " + "absolute voltage limits [{}, {}]{unit}" + "".format(amplitude, current_offset, min_volt, max_volt, + unit=unit)) + raise NotSetError(msg) + # Set the amplitude + cmd = "{}VOLTage:LEVel {}{}".format(self.source, amplitude, unit) + err_msg = "set amplitude {}{}".format(amplitude, unit) + self.fgen.write(cmd, custom_err_message=err_msg) + # Verify that the amplitude has been set + if self.fgen.verify_param_set: actual_amplitude = self.get_amplitude() - # multiply with the appropriate factor according to SI prefix, or + # Multiply with the appropriate factor according to SI prefix, or # if string is empty, use the value looked up from channel_limits earlier check_amplitude = amplitude*SI_prefix_to_factor(unit) if not unit == "" else amplitude if not actual_amplitude == check_amplitude: - raise NotSetError("Amplitude {}{} was not set on channel {}, it is {}Vpp. Check that the number is within the possible range and in the correct format.\nError from the instrument: {}".format(amplitude, unit, self.channel, actual_amplitude, self.func_gen.get_error())) + msg = ("Amplitude {}{} was not set on channel {}, it is " + "{}Vpp. Check that the number is within the possible " + "range and in the correct format.\nError from the " + "instrument: {}" + "".format(amplitude, unit, self.channel, + actual_amplitude, self.fgen.get_error())) + raise NotSetError(msg) def set_offset(self, offset, unit="V"): """Set offset in volts (or mV, see options) @@ -2683,25 +3302,38 @@

Attributes

Raises ------ NotSetError - If `self.func_gen.verify_param_set` is `True` and the value after + If `self.fgen.verify_param_set` is `True` and the value after applying the set function does not match the value returned by the get function """ - # check that the new offset will not violate voltage limits + # Check that the new offset will not violate voltage limits min_volt, max_volt = self.get_voltage_lims() current_amplitude = self.get_amplitude() - if current_amplitude/2-offset < min_volt or current_amplitude/2+offset > max_volt: - raise NotSetError("Could not set the offset {}{unit} as the offset combined with the amplitude ({}V) will be outside the absolute voltage limits [{}, {}]{unit}".format(offset, current_amplitude, min_volt, max_volt, unit=unit)) - # set the offset - self.func_gen.write("{}VOLTage:LEVel:OFFSet {}{}".format(self.source, offset, unit), - custom_err_message="set offset {}{}".format(offset, unit)) - # verify that the offset has been set - if self.func_gen.verify_param_set: + if (current_amplitude/2-offset < min_volt or + current_amplitude/2+offset > max_volt): + msg = ("Could not set the offset {}{unit} as the offset combined" + "with the amplitude ({}V) will be outside the absolute " + "voltage limits [{}, {}]{unit}".format(offset, + current_amplitude, + min_volt, max_volt, + unit=unit)) + raise NotSetError(msg) + # Set the offset + cmd = "{}VOLTage:LEVel:OFFSet {}{}".format(self.source, offset, unit) + err_msg = "set offset {}{}".format(offset, unit) + self.fgen.write(cmd, custom_err_message=err_msg) + # Verify that the offset has been set + if self.fgen.verify_param_set: actual_offset = self.get_offset() - # multiply with the appropriate factor according to SI prefix + # Multiply with the appropriate factor according to SI prefix check_offset = offset*SI_prefix_to_factor(unit) if not actual_offset == check_offset: - raise NotSetError("Offset {}{} was not set on channel {}, it is {}V. Check that the number is within the possible range and in the correct format.\nError from the instrument: {}".format(offset, unit, self.channel, actual_offset, self.func_gen.get_error())) + msg = ("Offset {}{} was not set on channel {}, it is {}V. " + "Check that the number is within the possible range and " + "in the correct format.\nError from the instrument: {}" + "".format(offset, unit, self.channel, actual_offset, + self.fgen.get_error())) + raise NotSetError(msg) def set_frequency(self, freq, unit="Hz"): """Set the frequency in Hertz (or kHz, MHz, see options) @@ -2715,43 +3347,51 @@

Attributes

Raises ------ NotSetError - If `self.func_gen.verify_param_set` is `True` and the value after + If `self.fgen.verify_param_set` is `True` and the value after applying the set function does not match the value returned by the get function """ if str(freq).lower() in ["min", "max"]: # handle min and max keywords unit = "" # no unit for MIN/MAX - # look up what the limit is for this keyword + # Look up what the limit is for this keyword freq = self.channel_limits["frequency lims"][0][str(freq).lower()] else: - # check if the given frequency is within the current limits + # Check if the given frequency is within the current limits min_freq, max_freq = self.get_frequency_lims() if freq < min_freq or freq > max_freq: - raise NotSetError("Could not set the frequency {}{} as it is not within the frequency limits set for the instrument [{}, {}]Hz".format(freq, unit, min_freq, max_freq)) - # check that the new amplitude will not violate voltage limits + msg = ("Could not set the frequency {}{} as it is not within " + "the frequency limits set for the instrument [{}, {}]" + "Hz".format(freq, unit, min_freq, max_freq)) + raise NotSetError(msg) + # Check that the new amplitude will not violate voltage limits min_volt, max_volt = self.get_voltage_lims() - # set the frequency - self.func_gen.write("{}FREQuency:FIXed {}{}".format(self.source, freq, unit), + # Set the frequency + self.fgen.write("{}FREQuency:FIXed {}{}".format(self.source, freq, unit), custom_err_message="set frequency {}{}".format(freq, unit)) - # verify that the amplitude has been set - if self.func_gen.verify_param_set: + # Verify that the amplitude has been set + if self.fgen.verify_param_set: actual_freq = self.get_frequency() - # multiply with the appropriate factor according to SI prefix, or + # Multiply with the appropriate factor according to SI prefix, or # if string is empty, use the value looked up from channel_limits earlier check_freq = freq*SI_prefix_to_factor(unit) if not unit == "" else freq if not actual_freq == check_freq: - raise NotSetError("Frequency {}{} was not set on channel {}, it is {}Hz. Check that the number is within the possible range and in the correct format.\nError from the instrument: {}".format(freq, unit, self.channel, actual_freq, self.func_gen.get_error()))
+ msg = ("Frequency {}{} was not set on channel {}, it is {}Hz. " + "Check that the number is within the possible range and in " + "the correct format.\nError from the instrument: {}" + "".format(freq, unit, self.channel, actual_freq, + self.fgen.get_error())) + raise NotSetError(msg)

Class variables

-
var state_str
+
var state_str

Dictionary for converting output states to "ON" and "OFF"

Methods

-
+
def get_amplitude(self)
@@ -2762,10 +3402,10 @@

Methods

def get_amplitude(self):
     """Returns peak-to-peak voltage in volts"""
-    return float(self.func_gen.query("{}VOLTage:AMPLitude?".format(self.source)))
+ return float(self.fgen.query("{}VOLTage:AMPLitude?".format(self.source)))
-
+
def get_amplitude_lims(self)
@@ -2776,10 +3416,11 @@

Methods

def get_amplitude_lims(self):
     """Returns list of min and max amplitude limits for the current impedance"""
-    return [self.channel_limits["amplitude lims"][0][self.impedance][key] for key in ["min", "max"]]
+ return [self.channel_limits["amplitude lims"][0][self.impedance][key] + for key in ["min", "max"]]
-
+
def get_frequency(self)
@@ -2790,10 +3431,10 @@

Methods

def get_frequency(self):
     """Returns frequency in Hertz"""
-    return float(self.func_gen.query("{}FREQuency?".format(self.source)))
+ return float(self.fgen.query("{}FREQuency?".format(self.source)))
-
+
def get_frequency_lims(self)
@@ -2804,10 +3445,11 @@

Methods

def get_frequency_lims(self):
     """Returns list of min and max frequency limits"""
-    return [self.channel_limits["frequency lims"][0][key] for key in ["min", "max"]]
+ return [self.channel_limits["frequency lims"][0][key] + for key in ["min", "max"]]
-
+
def get_function(self)
@@ -2818,10 +3460,10 @@

Methods

def get_function(self):
     """Returns string of function name"""
-    return self.func_gen.query("{}FUNCtion:SHAPe?".format(self.source))
+ return self.fgen.query("{}FUNCtion:SHAPe?".format(self.source))
-
+
def get_offset(self)
@@ -2832,10 +3474,24 @@

Methods

def get_offset(self):
     """Returns offset voltage in volts"""
-    return float(self.func_gen.query("{}VOLTage:OFFSet?".format(self.source)))
+ return float(self.fgen.query("{}VOLTage:OFFSet?".format(self.source)))
+ +
+
+def get_output(self) +
+
+

Wrapper for get_output_state

+
+ +Expand source code + +
def get_output(self):
+    """Wrapper for get_output_state"""
+    return self.get_output_state()
-
+
def get_output_state(self)
@@ -2846,10 +3502,10 @@

Methods

def get_output_state(self):
     """Returns "0" for "OFF", "1" for "ON" """
-    return self.func_gen.query("OUTPut{}:STATe?".format(self.channel))
+ return self.fgen.query("OUTPut{}:STATe?".format(self.channel))
-
+
def get_settings(self)
@@ -2857,9 +3513,9 @@

Methods

Returns

current_settings : dict
-
Settings currently in use as a dictionary with keys output, function, -amplitude, offset, and frequency and values tuples of the corresponding -return and unit
+
Settings currently in use as a dictionary with keys output, +function, amplitude, offset, and frequency and values tuples of +the corresponding return and unit
@@ -2871,9 +3527,9 @@

Returns

Returns ------- current_settings : dict - Settings currently in use as a dictionary with keys output, function, - amplitude, offset, and frequency and values tuples of the corresponding - return and unit + Settings currently in use as a dictionary with keys output, + function, amplitude, offset, and frequency and values tuples of + the corresponding return and unit """ return {"output": (self.state_str[self.get_output_state()], ""), "function": (self.get_function(), ""), @@ -2882,7 +3538,7 @@

Returns

"frequency": (self.get_frequency(), "Hz")}
-
+
def get_voltage_lims(self)
@@ -2893,32 +3549,66 @@

Returns

def get_voltage_lims(self):
     """Returns list of min and max voltage limits for the current impedance"""
-    return [self.channel_limits["voltage lims"][0][self.impedance][key] for key in ["min", "max"]]
+ return [self.channel_limits["voltage lims"][0][self.impedance][key] + for key in ["min", "max"]]
+ +
+
+def impedance_dependent_limit(self, limit_type) +
+
+

Check if the limit type is impedance dependent (voltages) or +not (frequency)

+

Returns

+
+
bool
+
True if the limit is impedance dependent
+
+
+ +Expand source code + +
def impedance_dependent_limit(self, limit_type):
+    """Check if the limit type is impedance dependent (voltages) or
+    not (frequency)
+
+    Returns
+    -------
+    bool
+        `True` if the limit is impedance dependent
+    """
+    try: # to access the key "min" to check if impedance must be selected
+        _ = self.channel_limits[limit_type][0]["min"]
+        return False
+    except KeyError: # if the key does not exist
+        # The impedance must be selected
+        return True
-
+
def print_settings(self)
-

Print the settings currently in use for the channel -(Recommended to use the func_gen.print_settings() for printing both channels)

+

Print the settings currently in use for the channel (Recommended +to use the FuncGen.print_settings() for printing both channels)

Expand source code
def print_settings(self):
-    """Print the settings currently in use for the channel
-    (Recommended to use the `func_gen.print_settings` for printing both channels)
+    """Print the settings currently in use for the channel (Recommended
+    to use the `FuncGen.print_settings` for printing both channels)
     """
     settings = self.get_settings()
     longest_key = max([len(key) for key in settings.keys()])
     print("\nCurrent settings for channel {}".format(self.channel))
     print("==============================")
-    for key, (val, unit) in zip(settings.items()):
-        print("{:>{num_char}s} {} {}".format(key, val, unit, num_char=longest_key))
+ for key, (val, unit) in settings.items(): + print("{:>{num_char}s} {} {}".format(key, val, unit, + num_char=longest_key))
-
+
def set_amplitude(self, amplitude)
@@ -2926,13 +3616,13 @@

Returns

Parameters

amplitude : float or {"max", "min"}
-
0.1mV or four digits resolution, "max" or "min" will set the amplitude -to the maximum or minimum limit given in channel_limits
+
0.1mV or four digits resolution, "max" or "min" will set the +amplitude to the maximum or minimum limit given in channel_limits

Raises

NotSetError
-
If self.func_gen.verify_param_set is True and the value after +
If self.fgen.verify_param_set is True and the value after applying the set function does not match the value returned by the get function
@@ -2946,46 +3636,63 @@

Raises

Parameters ---------- amplitude : float or {"max", "min"} - 0.1mV or four digits resolution, "max" or "min" will set the amplitude - to the maximum or minimum limit given in `channel_limits` + 0.1mV or four digits resolution, "max" or "min" will set the + amplitude to the maximum or minimum limit given in `channel_limits` Raises ------ NotSetError - If `self.func_gen.verify_param_set` is `True` and the value after + If `self.fgen.verify_param_set` is `True` and the value after applying the set function does not match the value returned by the get function """ - # check if keyword min or max is given + # Check if keyword min or max is given if str(amplitude).lower() in ["min", "max"]: unit = "" # no unit for MIN/MAX - # look up what the limit is for this keyword + # Look up what the limit is for this keyword amplitude = self.channel_limits["amplitude lims"][0][self.impedance][str(amplitude).lower()] else: unit = "Vpp" - # check if the given amplitude is within the current limits + # Check if the given amplitude is within the current limits min_ampl, max_ampl = self.get_amplitude_lims() if amplitude < min_ampl or amplitude > max_ampl: - raise NotSetError("Could not set the amplitude {}{unit} as it is not within the amplitude limits set for the instrument [{}, {}]{unit}".format(amplitude, min_ampl, max_ampl, unit=unit)) - # check that the new amplitude will not violate voltage limits + msg = ("Could not set the amplitude {}{unit} as it is not " + "within the amplitude limits set for the instrument " + "[{}, {}]{unit}".format(amplitude, min_ampl, max_ampl, + unit=unit)) + raise NotSetError(msg) + # Check that the new amplitude will not violate voltage limits min_volt, max_volt = self.get_voltage_lims() current_offset = self.get_offset() - if amplitude/2-current_offset < min_volt or amplitude/2+current_offset > max_volt: - raise NotSetError("Could not set the amplitude {}{unit} as the amplitude combined with the offset ({}V) will be outside the absolute voltage limits [{}, {}]{unit}".format(amplitude, current_offset, min_volt, max_volt, unit=unit)) - # set the amplitude - self.func_gen.write("{}VOLTage:LEVel {}{}".format(self.source, amplitude, unit), - custom_err_message="set amplitude {}{}".format(amplitude, unit)) - # verify that the amplitude has been set - if self.func_gen.verify_param_set: + if (amplitude/2-current_offset < min_volt or + amplitude/2+current_offset > max_volt): + msg = ("Could not set the amplitude {}{unit} as the amplitude " + "combined with the offset ({}V) will be outside the " + "absolute voltage limits [{}, {}]{unit}" + "".format(amplitude, current_offset, min_volt, max_volt, + unit=unit)) + raise NotSetError(msg) + # Set the amplitude + cmd = "{}VOLTage:LEVel {}{}".format(self.source, amplitude, unit) + err_msg = "set amplitude {}{}".format(amplitude, unit) + self.fgen.write(cmd, custom_err_message=err_msg) + # Verify that the amplitude has been set + if self.fgen.verify_param_set: actual_amplitude = self.get_amplitude() - # multiply with the appropriate factor according to SI prefix, or + # Multiply with the appropriate factor according to SI prefix, or # if string is empty, use the value looked up from channel_limits earlier check_amplitude = amplitude*SI_prefix_to_factor(unit) if not unit == "" else amplitude if not actual_amplitude == check_amplitude: - raise NotSetError("Amplitude {}{} was not set on channel {}, it is {}Vpp. Check that the number is within the possible range and in the correct format.\nError from the instrument: {}".format(amplitude, unit, self.channel, actual_amplitude, self.func_gen.get_error()))
+ msg = ("Amplitude {}{} was not set on channel {}, it is " + "{}Vpp. Check that the number is within the possible " + "range and in the correct format.\nError from the " + "instrument: {}" + "".format(amplitude, unit, self.channel, + actual_amplitude, self.fgen.get_error())) + raise NotSetError(msg)
-
+
def set_frequency(self, freq, unit='Hz')
@@ -3000,7 +3707,7 @@

Parameters

Raises

NotSetError
-
If self.func_gen.verify_param_set is True and the value after +
If self.fgen.verify_param_set is True and the value after applying the set function does not match the value returned by the get function
@@ -3020,48 +3727,63 @@

Raises

Raises ------ NotSetError - If `self.func_gen.verify_param_set` is `True` and the value after + If `self.fgen.verify_param_set` is `True` and the value after applying the set function does not match the value returned by the get function """ if str(freq).lower() in ["min", "max"]: # handle min and max keywords unit = "" # no unit for MIN/MAX - # look up what the limit is for this keyword + # Look up what the limit is for this keyword freq = self.channel_limits["frequency lims"][0][str(freq).lower()] else: - # check if the given frequency is within the current limits + # Check if the given frequency is within the current limits min_freq, max_freq = self.get_frequency_lims() if freq < min_freq or freq > max_freq: - raise NotSetError("Could not set the frequency {}{} as it is not within the frequency limits set for the instrument [{}, {}]Hz".format(freq, unit, min_freq, max_freq)) - # check that the new amplitude will not violate voltage limits + msg = ("Could not set the frequency {}{} as it is not within " + "the frequency limits set for the instrument [{}, {}]" + "Hz".format(freq, unit, min_freq, max_freq)) + raise NotSetError(msg) + # Check that the new amplitude will not violate voltage limits min_volt, max_volt = self.get_voltage_lims() - # set the frequency - self.func_gen.write("{}FREQuency:FIXed {}{}".format(self.source, freq, unit), + # Set the frequency + self.fgen.write("{}FREQuency:FIXed {}{}".format(self.source, freq, unit), custom_err_message="set frequency {}{}".format(freq, unit)) - # verify that the amplitude has been set - if self.func_gen.verify_param_set: + # Verify that the amplitude has been set + if self.fgen.verify_param_set: actual_freq = self.get_frequency() - # multiply with the appropriate factor according to SI prefix, or + # Multiply with the appropriate factor according to SI prefix, or # if string is empty, use the value looked up from channel_limits earlier check_freq = freq*SI_prefix_to_factor(unit) if not unit == "" else freq if not actual_freq == check_freq: - raise NotSetError("Frequency {}{} was not set on channel {}, it is {}Hz. Check that the number is within the possible range and in the correct format.\nError from the instrument: {}".format(freq, unit, self.channel, actual_freq, self.func_gen.get_error()))
+ msg = ("Frequency {}{} was not set on channel {}, it is {}Hz. " + "Check that the number is within the possible range and in " + "the correct format.\nError from the instrument: {}" + "".format(freq, unit, self.channel, actual_freq, + self.fgen.get_error())) + raise NotSetError(msg)
-
+
def set_function(self, shape)

Set the function shape of the output

Parameters

-
shape : {SINusoid, SQUare, PULSe, RAMP, PRNoise, <Built_in>, USER[0], USER1, ..., USER255, EMEMory, EFILe}
-
::={StairDown|StairUp|Stair Up&Dwn|Trapezoid|RoundHalf|AbsSine|AbsHalfSine|ClippedSine|ChoppedSine|NegRamp|OscRise|OscDecay|CodedPulse|PosPulse|NegPulse|ExpRise|ExpDecay|Sinc|Tan|Cotan|SquareRoot|X^2|HaverSine|Lorentz|Ln(x)|X^3|CauchyDistr|BesselJ|BesselY|ErrorFunc|Airy|Rectangle|Gauss|Hamming|Hanning|Bartlett|Blackman|Laylight|Triangle|DC|Heart|Round|Chirp|Rhombus|Cardiac}
+
shape : {SINusoid, SQUare, PULSe, RAMP, PRNoise, <Built_in>, USER[0],
+
USER1, …, USER255, EMEMory, EFILe} +::={StairDown|StairUp|Stair Up&Dwn|Trapezoid|RoundHalf| +AbsSine|AbsHalfSine|ClippedSine|ChoppedSine|NegRamp|OscRise| +OscDecay|CodedPulse|PosPulse|NegPulse|ExpRise|ExpDecay|Sinc| +Tan|Cotan|SquareRoot|X^2|HaverSine|Lorentz|Ln(x)|X^3|CauchyDistr| +BesselJ|BesselY|ErrorFunc|Airy|Rectangle|Gauss|Hamming|Hanning| +Bartlett|Blackman|Laylight|Triangle|DC|Heart|Round|Chirp|Rhombus| +Cardiac}

Raises

NotSetError
-
If self.func_gen.verify_param_set is True and the value after +
If self.fgen.verify_param_set is True and the value after applying the set function does not match the value returned by the get function
@@ -3074,25 +3796,129 @@

Raises

Parameters ---------- - shape : {SINusoid, SQUare, PULSe, RAMP, PRNoise, <Built_in>, USER[0], USER1, ..., USER255, EMEMory, EFILe} - <Built_in>::={StairDown|StairUp|Stair Up&Dwn|Trapezoid|RoundHalf|AbsSine|AbsHalfSine|ClippedSine|ChoppedSine|NegRamp|OscRise|OscDecay|CodedPulse|PosPulse|NegPulse|ExpRise|ExpDecay|Sinc|Tan|Cotan|SquareRoot|X^2|HaverSine|Lorentz|Ln(x)|X^3|CauchyDistr|BesselJ|BesselY|ErrorFunc|Airy|Rectangle|Gauss|Hamming|Hanning|Bartlett|Blackman|Laylight|Triangle|DC|Heart|Round|Chirp|Rhombus|Cardiac} + shape : {SINusoid, SQUare, PULSe, RAMP, PRNoise, <Built_in>, USER[0], + USER1, ..., USER255, EMEMory, EFILe} + <Built_in>::={StairDown|StairUp|Stair Up&Dwn|Trapezoid|RoundHalf| + AbsSine|AbsHalfSine|ClippedSine|ChoppedSine|NegRamp|OscRise| + OscDecay|CodedPulse|PosPulse|NegPulse|ExpRise|ExpDecay|Sinc| + Tan|Cotan|SquareRoot|X^2|HaverSine|Lorentz|Ln(x)|X^3|CauchyDistr| + BesselJ|BesselY|ErrorFunc|Airy|Rectangle|Gauss|Hamming|Hanning| + Bartlett|Blackman|Laylight|Triangle|DC|Heart|Round|Chirp|Rhombus| + Cardiac} Raises ------ NotSetError - If `self.func_gen.verify_param_set` is `True` and the value after + If `self.fgen.verify_param_set` is `True` and the value after applying the set function does not match the value returned by the get function """ - self.func_gen.write("{}FUNCtion:SHAPe {}".format(self.source, shape), - custom_err_message="set function {}".format(shape)) - if self.func_gen.verify_param_set: + cmd = "{}FUNCtion:SHAPe {}".format(self.source, shape) + self.fgen.write(cmd, custom_err_message="set function {}".format(shape)) + if self.fgen.verify_param_set: actual_shape = self.get_function() if not actual_shape == shape: - raise NotSetError("Function {} was not set on channel {}, it is {}. Check that the function name is correctly spelt. Run set_function.__doc__ to see available shapes.\nError from the instrument: {}".format(shape, self.channel, actual_shape, self.func_gen.get_error()))
+ msg = ("Function {} was not set on channel {}, it is {}. " + "Check that the function name is correctly spelt. " + "Run set_function.__doc__ to see available shapes.\n" + "Error from the instrument: {}" + "".format(shape, self.channel, actual_shape, + self.fgen.get_error())) + raise NotSetError(msg)
-
+
+def set_limit(self, limit_type, bound, new_value, verbose=False) +
+
+

Set a limit if the new value is within the instrument limits and are +self consistent (max larger than min)

+

Parameterers

+
+
limit_type : str
+
The name of the limit in the channel_limits dictionary
+
bound : {"min", "max"}
+
Specifies if it is the max or the min limit that is to be set
+
new_value : float
+
The new value to be used for the limit
+
verbose : bool
+
Print confirmation that the limit was set or reason for why the +limit was not set
+
+

Returns

+
+
bool
+
True if new limit set, False otherwise
+
+
+ +Expand source code + +
def set_limit(self, limit_type, bound, new_value, verbose=False):
+    """Set a limit if the new value is within the instrument limits and are
+    self consistent (max larger than min)
+
+    Parameterers
+    ------------
+    limit_type : str
+        The name of the limit in the channel_limits dictionary
+    bound : {"min", "max"}
+        Specifies if it is the max or the min limit that is to be set
+    new_value : float
+        The new value to be used for the limit
+    verbose : bool
+        Print confirmation that the limit was set or reason for why the
+        limit was not set
+
+    Returns
+    -------
+    bool
+        `True` if new limit set, `False` otherwise
+    """
+    # Short hand references
+    inst_limit_dict = self.fgen.instrument_limits[limit_type]
+    channel_limit_dict = self.channel_limits[limit_type]
+    # Find the instrument limit and unit
+    use_impedance = self.impedance_dependent_limit(limit_type)
+    if use_impedance:
+        inst_value = inst_limit_dict[0][self.impedance][bound]
+    else:
+        inst_value = inst_limit_dict[0][bound]
+    unit = inst_limit_dict[1]
+    # Check that the new value is within the intrument limits
+    acceptable_min = bound == "min" and new_value > inst_value
+    if use_impedance:
+        current_min = channel_limit_dict[0][self.impedance]["min"]
+    else:
+        current_min = channel_limit_dict[0]["min"]
+    larger_than_min = new_value > current_min
+    acceptable_max = bound == "max" and new_value < inst_value and larger_than_min
+    if acceptable_min or acceptable_max: # within the limits
+        # Set the new channel_limit, using the impedance depending on the
+        # limit type. Beware that the shorthand cannot be used, as this
+        # only changes the shorthand not the dictionary itself
+        if use_impedance:
+            self.channel_limits[limit_type][0][self.impedance][bound] = new_value
+        else:
+            self.channel_limits[limit_type][0][bound] = new_value
+        if verbose:
+            print("\tNew limit set {}{}".format(new_value, unit))
+        return True
+    elif verbose: # print description of why the limit was not set
+        if larger_than_min:
+            reason = "larger" if bound == "max" else "smaller"
+            print("\tNew limit NOT set: {}{unit} is {} than the instrument "
+                  "limit ({}{unit})".format(new_value, reason, inst_value,
+                                            unit=unit))
+        else:
+            print("\tNew limit NOT set: {}{unit} is smaller than the "
+                  "current set minimum ({}{unit})".format(new_value,
+                                                          current_min,
+                                                          unit=unit))
+    return False
+
+
+
def set_offset(self, offset, unit='V')
@@ -3107,7 +3933,7 @@

Parameters

Raises

NotSetError
-
If self.func_gen.verify_param_set is True and the value after +
If self.fgen.verify_param_set is True and the value after applying the set function does not match the value returned by the get function
@@ -3127,31 +3953,58 @@

Raises

Raises ------ NotSetError - If `self.func_gen.verify_param_set` is `True` and the value after + If `self.fgen.verify_param_set` is `True` and the value after applying the set function does not match the value returned by the get function """ - # check that the new offset will not violate voltage limits + # Check that the new offset will not violate voltage limits min_volt, max_volt = self.get_voltage_lims() current_amplitude = self.get_amplitude() - if current_amplitude/2-offset < min_volt or current_amplitude/2+offset > max_volt: - raise NotSetError("Could not set the offset {}{unit} as the offset combined with the amplitude ({}V) will be outside the absolute voltage limits [{}, {}]{unit}".format(offset, current_amplitude, min_volt, max_volt, unit=unit)) - # set the offset - self.func_gen.write("{}VOLTage:LEVel:OFFSet {}{}".format(self.source, offset, unit), - custom_err_message="set offset {}{}".format(offset, unit)) - # verify that the offset has been set - if self.func_gen.verify_param_set: + if (current_amplitude/2-offset < min_volt or + current_amplitude/2+offset > max_volt): + msg = ("Could not set the offset {}{unit} as the offset combined" + "with the amplitude ({}V) will be outside the absolute " + "voltage limits [{}, {}]{unit}".format(offset, + current_amplitude, + min_volt, max_volt, + unit=unit)) + raise NotSetError(msg) + # Set the offset + cmd = "{}VOLTage:LEVel:OFFSet {}{}".format(self.source, offset, unit) + err_msg = "set offset {}{}".format(offset, unit) + self.fgen.write(cmd, custom_err_message=err_msg) + # Verify that the offset has been set + if self.fgen.verify_param_set: actual_offset = self.get_offset() - # multiply with the appropriate factor according to SI prefix + # Multiply with the appropriate factor according to SI prefix check_offset = offset*SI_prefix_to_factor(unit) if not actual_offset == check_offset: - raise NotSetError("Offset {}{} was not set on channel {}, it is {}V. Check that the number is within the possible range and in the correct format.\nError from the instrument: {}".format(offset, unit, self.channel, actual_offset, self.func_gen.get_error()))
+ msg = ("Offset {}{} was not set on channel {}, it is {}V. " + "Check that the number is within the possible range and " + "in the correct format.\nError from the instrument: {}" + "".format(offset, unit, self.channel, actual_offset, + self.fgen.get_error())) + raise NotSetError(msg)
-
+
def set_output(self, state)
+

Wrapper for set_output_state

+
+ +Expand source code + +
def set_output(self, state):
+    """Wrapper for set_output_state"""
+    self.set_output_state(state)
+
+
+
+def set_output_state(self, state) +
+

Enables or diables the output of the channel

Parameters

@@ -3162,7 +4015,7 @@

Parameters

Raises

NotSetError
-
If self.func_gen.verify_param_set is True and the value after +
If self.fgen.verify_param_set is True and the value after applying the set function does not match the value returned by the get function
@@ -3170,7 +4023,7 @@

Raises

Expand source code -
def set_output(self, state):
+
def set_output_state(self, state):
     """Enables or diables the output of the channel
 
     Parameters
@@ -3182,19 +4035,64 @@ 

Raises

Raises ------ NotSetError - If `self.func_gen.verify_param_set` is `True` and the value after + If `self.fgen.verify_param_set` is `True` and the value after applying the set function does not match the value returned by the get function """ - self.func_gen.write("OUTPut{}:STATe {}".format(self.channel, state), - custom_err_message="turn channel {} to state {}".format(self.channel, state)) - if self.func_gen.verify_param_set: - actual_state = self.get_output() + err_msg = "turn channel {} to state {}".format(self.channel, state) + self.fgen.write("OUTPut{}:STATe {}".format(self.channel, state), + custom_err_message=err_msg) + if self.fgen.verify_param_set: + actual_state = self.get_output_state() if not actual_state == state: - raise NotSetError("Channel {} was not turned {}, it is {}.\nError from the instrument: {}".format(self.channel, state, self.state_str[actual_state], self.func_gen.get_error()))
+ msg = ("Channel {} was not turned {}, it is {}.\n" + "Error from the instrument: {}" + "".format(self.channel, state, + self.state_str[actual_state], + self.fgen.get_error())) + raise NotSetError(msg)
+ +
+
+def set_settings(self, settings) +
+
+

Set the settings of the channel with a settings dictionary. Will +set the outout to OFF before applyign the settings (and turn the +channel ON or leave it OFF depending on the settings dict)

+

Parameteres

+
+
settings : dict
+
Settings dictionary as returned by get_settings: should have +keys output, function, amplitude, offset, and frequency
+
+
+ +Expand source code + +
def set_settings(self, settings):
+    """Set the settings of the channel with a settings dictionary. Will
+    set the outout to OFF before applyign the settings (and turn the
+    channel ON or leave it OFF depending on the settings dict)
+
+    Parameteres
+    -----------
+    settings : dict
+        Settings dictionary as returned by `get_settings`: should have
+        keys output, function, amplitude, offset, and frequency
+    """
+    # First turn off to ensure no potentially harmful
+    # combination of settings
+    self.set_output_state("OFF")
+    # Set settings according to dictionary
+    self.set_function(settings['function'][0])
+    self.set_amplitude(settings['amplitude'][0])
+    self.set_offset(settings['offset'][0])
+    self.set_frequency(settings['frequency'][0])
+    self.set_output_state(settings['output'][0])
-
+
def set_stricter_limits(self)
@@ -3207,57 +4105,77 @@

Raises

def set_stricter_limits(self):
     """Set limits for the voltage and frequency limits of the channel output
     through a series of prompts"""
-    print("Set stricter voltage and frequency limits for channel {}".format(self.channel))
+    print("Set stricter voltage and frequency limits "
+          "for channel {}".format(self.channel))
     print("Use enter only to leave a limit unchanged.")
     # Go through the different limits in the instrument_limits dict
-    for limit_type, (inst_limit_dict, unit) in self.func_gen.instrument_limits.items():
-        try: # to access the key "min" to check if impedance must be selected
-            _ = inst_limit_dict["min"]
-            use_impedance = False
-        except KeyError: # if the key does not exist
-            # the impedance must be selected
-            use_impedance = True
-            inst_limit_dict = inst_limit_dict[self.impedance]
+    for limit_type, (inst_limit_dict, unit) in self.fgen.instrument_limits.items():
+        use_impedance = self.impedance_dependent_limit(limit_type)
         print("Set {} in {}".format(limit_type, unit), end=" ")
         if use_impedance:
+            inst_limit_dict = inst_limit_dict[self.impedance]
             print("[{} impedance limit]".format(self.impedance))
         else:
             print("") # get new line
         # Go through the min and max for the limit type
         for key, inst_value in inst_limit_dict.items():
             # prompt for new value
-            new_value = input("  {} (instrument limit {}{}): ".format(key, inst_value, unit))
+            new_value = input("  {} (instrument limit {}{}): "
+                              "".format(key, inst_value, unit))
             if new_value == "":
-                # do not change if empty
+                # Do not change if empty
                 print("\tLimit not changed")
             else:
                 try: # to convert to float
                     new_value = float(new_value)
                 except ValueError:
-                    print("\tLimit unchanged: Could not convert {} to float".format(new_value))
+                    print("\tLimit unchanged: Could not convert \'{}\' "
+                          "to float".format(new_value))
                     continue # to next item in dict
-                # check that the new value is within the intrument limits
-                acceptable_min = key == "min" and new_value > inst_value
-                current_min = self.channel_limits[limit_type][0][self.impedance]["min"] if use_impedance else self.channel_limits[limit_type][0]["min"]
-                larger_than_min = new_value > current_min
-                acceptable_max = key == "max" and new_value < inst_value and larger_than_min
-                if acceptable_min or acceptable_max: # within the limits
-                    # set the new channel_limit, using the impedance depending on the limit type
-                    if use_impedance:
-                        self.channel_limits[limit_type][0][self.impedance][key] = new_value
-                    else:
-                        self.channel_limits[limit_type][0][key] = new_value
-                    print("\tNew limit set {}{}".format(new_value, unit))
-                else: # print description of why the limit was not set
-                    if larger_than_min:
-                        reason = "larger" if key == "max" else "smaller"
-                        print("\tNew limit NOT set: {}{unit} is {} than the instrument limit ({}{unit})".format(new_value, reason, inst_value, unit=unit))
-                    else:
-                        print("\tNew limit NOT set: {}{unit} is smaller than the current set minimum ({}{unit})".format(new_value, current_min, unit=unit))
+ # Set the new limit + self.set_limit(limit_type, key, new_value, verbose=True)
+
+class NotCompatibleError +(*args, **kwargs) +
+
+

Error for when the instrument is not compatible with this module

+
+ +Expand source code + +
class NotCompatibleError(Exception):
+    """Error for when the instrument is not compatible with this module"""
+
+

Ancestors

+
    +
  • builtins.Exception
  • +
  • builtins.BaseException
  • +
+
+
+class NotSetError +(*args, **kwargs) +
+
+

Error for when a value cannot be written to the instrument

+
+ +Expand source code + +
class NotSetError(Exception):
+    """Error for when a value cannot be written to the instrument"""
+
+

Ancestors

+
    +
  • builtins.Exception
  • +
  • builtins.BaseException
  • +
+
@@ -3270,65 +4188,72 @@

Index

  • Functions

  • Classes

  • @@ -3341,4 +4266,4 @@

    - + \ No newline at end of file diff --git a/tektronix_func_gen.py b/tektronix_func_gen.py index 1db5538..a7b1246 100644 --- a/tektronix_func_gen.py +++ b/tektronix_func_gen.py @@ -2,7 +2,8 @@ """ Control of Tektronix AFG1022 function generator through PyVISA -To build the documentation use [pdoc3](https://pdoc3.github.io/pdoc/) and run `$ pdoc --html tektronix_func_gen` +To build the documentation use [pdoc3](https://pdoc3.github.io/pdoc/) and +run `$ pdoc --html tektronix_func_gen` Andreas Svela 2019 @@ -10,13 +11,14 @@ * add AM/FM capabilites """ -import numpy as np +import copy import pyvisa +import numpy as np -## ~~~~~~~~~~~~~~~~~~~~~ FUNCTION GENERATOR CLASS ~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## +## ~~~~~~~~~~~~~~~~~~~~~ FUNCTION GENERATOR CLASS ~~~~~~~~~~~~~~~~~~~~~~~~~~ ## -class func_gen(): +class FuncGen(): """Class for interacting with Tektronix function generator Parameters @@ -45,28 +47,32 @@ class func_gen(): address : str The VISA address of the instrument id : str - Comma separated string with maker, model, serial and firmware of the instrument + Comma separated string with maker, model, serial and firmware of + the instrument inst : `pyvisa.resources.Resource` The PyVISA resource - channels : tuple of func_gen_channel + channels : tuple of FuncGenChannel Objects to control the channels - ch1 : func_gen_channel + ch1 : FuncGenChannel Short hand for `channels[0]` Object to control channel 1 - ch2 : func_gen_channel + ch2 : FuncGenChannel Short hand for `channels[1]` Object to control channel 2 instrument_limits : dict Contains the following keys with subdictionaries `frequency lims` - Containing the frequency limits for the instrument where the keys "min" - and "max" have values corresponding to minimum and maximum frequencies in Hz + Containing the frequency limits for the instrument where the keys + "min" and "max" have values corresponding to minimum and maximum + frequencies in Hz `voltage lims` - Contains the maximum absolute voltage the instrument can output for the keys - "50ohm" and "highZ" according to the impedance setting + Contains the maximum absolute voltage the instrument can output + for the keys "50ohm" and "highZ" according to the impedance setting `amplitude lims` - Contains the smallest and largest possible amplitudes where the keys - "50ohm" and "highZ" will have subdictionaries with keys "min" and "max" + Contains the smallest and largest possible amplitudes where the + keys "50ohm" and "highZ" will have subdictionaries with keys + "min" and "max" arbitrary_waveform_length : list - The permitted minimum and maximum length of an arbitrary waveform, e.g. [2, 8192] + The permitted minimum and maximum length of an arbitrary waveform, + e.g. [2, 8192] arbitrary_waveform_resolution : int The vertical resolution of the arbitrary waveform, for instance 14 bit => 2**14-1 = 16383 @@ -76,11 +82,14 @@ class func_gen(): pyvisa.Error If the supplied VISA address cannot be connected to NotCompatibleError - If the instrument limits for the model connected to are not known (Call the class with `override_compatibility=True` to override and use AFG1022 limits) + If the instrument limits for the model connected to are not known + (Call the class with `override_compatibility=True` to override and + use AFG1022 limits) """ def __init__(self, address, impedance=("highZ",)*2, timeout=5000, - verify_param_set=False, override_compatibility=False, verbose=True): + verify_param_set=False, override_compatibility=False, + verbose=True): self.override_compatibility = override_compatibility self.address = address self.timeout = timeout @@ -93,24 +102,26 @@ def __init__(self, address, impedance=("highZ",)*2, timeout=5000, print("\nVisaError: Could not connect to \'{}\'".format(address)) raise self.inst.timeout = self.timeout - # Clear all the event registers and queues used in the instrument status and event reporting system + # Clear all the event registers and queues used in the instrument + # status and event reporting system self.write("*CLS") # Get information about the connected device - self.id = self.query("*IDN?") # get the id of the connected device - if self.id == '': # second query might be needed due to unknown reason (suspecting *CLS does something weird) + self.id = self.query("*IDN?") + # Second query might be needed due to unknown reason + # (suspecting *CLS does something weird) + if self.id == '': self.id = self.query("*IDN?") self.maker, self.model, self.serial = self.id.split(",")[:3] - if self.verbose: print("Connected to {} model {}, serial {}".format(self.maker, self.model, self.serial)) + if self.verbose: + print("Connected to {} model {}, serial" + " {}".format(self.maker, self.model, self.serial)) self.initialise_model_properties() - self.channels = self.spawn_channel(1, impedance[0]), self.spawn_channel(2, impedance[1]) + self.channels = (self.spawn_channel(1, impedance[0]), + self.spawn_channel(2, impedance[1])) self.ch1, self.ch2 = self.channels - # __enter__ and __exit__ functions defined to use the module with a 'with' statement - # with func_gen(verify_param_set=True) as fgen: - # fgen.get_settings() - # This gives automatic handling of closing down the connection when fgen goes out of scope - # The kwargs in __enter__ will be passed on to __init__ def __enter__(self, **kwargs): + # The kwargs will be passed on to __init__ return self def __exit__(self, exc_type, exc_val, exc_tb): @@ -130,20 +141,25 @@ def initialise_model_properties(self): Raises ------ NotCompatibleError - If the connected model is not necessarily compatible with this package, - limits are not known. + If the connected model is not necessarily compatible with this + package, slimits are not known. """ if self.model == 'AFG1022' or self.override_compatibility: self.instrument_limits = { - "frequency lims": ({"min": 1e-6, "max": 25e6}, "Hz"), - "voltage lims": ({"50ohm": {"min": -5, "max": 5}, - "highZ": {"min": -10, "max": 10}}, "V"), - "amplitude lims": ({"50ohm": {"min": 0.001, "max": 10}, - "highZ": {"min": 0.002, "max": 20}}, "Vpp")} - self.arbitrary_waveform_length = [2, 8192] # min length, max length - self.arbitrary_waveform_resolution = 16383 # 14 bit + "frequency lims": ({"min": 1e-6, "max": 25e6}, "Hz"), + "voltage lims": ({"50ohm": {"min": -5, "max": 5}, + "highZ": {"min": -10, "max": 10}}, "V"), + "amplitude lims": ({"50ohm": {"min": 0.001, "max": 10}, + "highZ": {"min": 0.002, "max": 20}}, "Vpp")} + self.arbitrary_waveform_length = [2, 8192] # min length, max length + self.arbitrary_waveform_resolution = 16383 # 14 bit else: - raise NotCompatibleError("Model {} not supported, no limits set!\n\tTo use the limits for AFG1022, call the class with 'override_compatibility=True'\n\tNote that this might lead to unexpected behaviour for custom waveforms and 'MIN'/'MAX' keywords.") + msg = ("Model {} not supported, no limits set!" + "\n\tTo use the limits for AFG1022, call the class with " + "'override_compatibility=True'" + "\n\tNote that this might lead to unexpected behaviour " + "for custom waveforms and 'MIN'/'MAX' keywords.") + raise NotCompatibleError(msg) def write(self, command, custom_err_message=None): """Write a VISA command to the instrument @@ -153,8 +169,10 @@ def write(self, command, custom_err_message=None): command : str The VISA command to be written to the instrument custom_err_message : str, default `None` - When `None`, the RuntimeError message is "Writing command {command} failed: pyvisa returned StatusCode ..". - Otherwise, if a message is supplied "Could not {message}: pyvisa returned StatusCode .." + When `None`, the RuntimeError message is "Writing command {command} + failed: pyvisa returned StatusCode ..". + Otherwise, if a message is supplied "Could not {message}: + pyvisa returned StatusCode .." Returns ------- @@ -166,15 +184,20 @@ def write(self, command, custom_err_message=None): Raises ------ RuntimeError - If status returned by PyVISA write command is not `pyvisa.constants.StatusCode.success` + If status returned by PyVISA write command is not + `pyvisa.constants.StatusCode.success` """ - bytes, status = self.inst.write(command) + num_bytes, status = self.inst.write(command) if not status == pyvisa.constants.StatusCode.success: if custom_err_message is not None: - raise RuntimeError("Could not {}: pyvisa returned StatusCode {} ({})".format(custom_err_message, status, str(status))) + msg = ("Could not {}: pyvisa returned StatusCode {} " + "({})".format(custom_err_message, status, str(status))) + raise RuntimeError(msg) else: - raise RuntimeError("Writing command {} failed: pyvisa returned StatusCode {} ({})".format(command, status, str(status))) - return bytes, status + msg = ("Writing command {} failed: pyvisa returned StatusCode" + " {} ({})".format(command, status, str(status))) + raise RuntimeError(msg) + return num_bytes, status def query(self, command): """Query the instrument @@ -202,18 +225,13 @@ def get_error(self): return self.query("SYSTEM:ERROR:NEXT?") def spawn_channel(self, channel, impedance): - """Wrapper function to create a `func_gen_channel` object for + """Wrapper function to create a `FuncGenChannel` object for a channel -- see the class docstring""" - return func_gen_channel(self, channel, impedance) + return FuncGenChannel(self, channel, impedance) def get_settings(self): """Get dictionaries of the current settings of the two channels - Parameters - ---------- - print_settings : bool, default `True` - `True` prints table of the current setting for both channels - Returns ------- settings : list of dicts @@ -226,25 +244,46 @@ def get_settings(self): def print_settings(self): """Prints table of the current setting for both channels""" settings = self.get_settings() - # find the necessary padding for the table columns + # Find the necessary padding for the table columns # by evaluating the maximum length of the entries key_padding = max([len(key) for key in settings[0].keys()]) - ch_paddings = [max([len(str(val[0])) for val in ch_settings.values()]) for ch_settings in settings] + ch_paddings = [max([len(str(val[0])) for val in ch_settings.values()]) + for ch_settings in settings] padding = [key_padding]+ch_paddings - print("\nCurrent settings for {} {} {}\n".format(self.maker, self.model, self.serial)) - + print("\nCurrent settings for {} {} {}\n".format(self.maker, + self.model, + self.serial)) row_format = "{:>{padd[0]}s} {:{padd[1]}s} {:{padd[2]}s} {}" - table_header = row_format.format("Setting", "Ch1", "Ch2", "Unit", padd=padding) + table_header = row_format.format("Setting", "Ch1", "Ch2", + "Unit", padd=padding) print(table_header) print("="*len(table_header)) - for (ch1key, (ch1val, unit)), (ch2key, (ch2val, _)) in zip(settings[0].items(), settings[1].items()): - print(row_format.format(ch1key, str(ch1val), str(ch2val), unit, padd=padding)) + for (ch1key, (ch1val, unit)), (_, (ch2val, _)) in zip(settings[0].items(), + settings[1].items()): + print(row_format.format(ch1key, str(ch1val), str(ch2val), + unit, padd=padding)) + + def set_settings(self, settings): + """Set the settings of both channels with settings dictionaries + + (Each channel is turned off before applying the changes to avoid + potenitally harmful combinations) + + Parameteres + ----------- + settings : list of dicts + List of settings dictionaries as returned by `get_settings`, first + entry for channel 1, second for channel 2. The dictionaries should + have keys output, function, amplitude, offset, and frequency + """ + for ch, s in zip(self.channels, settings): + ch.set_settings(s) def syncronise_waveforms(self): """Syncronise waveforms of the two channels when using the same frequency - Note: Does NOT enable the frequency lock that can be enabled on the user - interface of the instrument) + Note: Does NOT enable the frequency lock that can be enabled on the + user interface of the instrument) """ self.write(":PHAS:INIT", custom_err_message="syncronise waveforms") @@ -256,39 +295,44 @@ def get_frequency_lock(self): bool `True` if frequency lock enabled """ - # if one is locked so is the other, so just need to check one + # If one is locked so is the other, so just need to check one return int(self.query("SOURCE1:FREQuency:CONCurrent?")) == 1 - def enable_frequency_lock(self, use_channel=1): - """Enable the frequency lock to make the two channels have the same frequency - and phase of their signals, also after adjustments. + def set_frequency_lock(self, state, use_channel=1): + """Enable the frequency lock to make the two channels have the same + frequency and phase of their signals, also after adjustments. - See also `func_gen.syncronise_waveforms` for one-time sync only. + See also `FuncGen.syncronise_waveforms` for one-time sync only. Parameters ---------- + state : {"ON", "OFF"} + ON to enable, OFF to disable the lock use_channel : int, default 1 - The channel whose frequency shall be used as the common freqency + Only relevant if turning the lock ON: The channel whose frequency + shall be used as the common freqency """ - self.write("SOURCE{}:FREQuency:CONCurrent ON".format(use_channel), - custom_err_message="enable frequency lock") - - def disable_frequency_lock(self): - """Disable the frequency lock that makes the two channels have the same - frequency and phase of their signals, also after adjustments - """ - if self.get_frequency_lock(): - # sufficient to disable for only one of the channels - self.write("SOURCE1:FREQuency:CONCurrent OFF") - else: - if self.verbose: print("(!) {}: Tried to disable frequency lock, but frequency lock was not enabled".format(self.model)) + if self.verbose: + if state.lower() == "off" and not self.get_frequency_lock(): + print("(!) {}: Tried to disable frequency lock, but " + "frequency lock was not enabled".format(self.model)) + return + if state.lower() == "on" and self.get_frequency_lock(): + print("(!) {}: Tried to enable frequency lock, but " + "frequency lock was already enabled".format(self.model)) + return + # (Sufficient to disable for only one of the channels) + cmd = "SOURCE{}:FREQuency:CONCurrent {}".format(use_channel, state) + msg = "turn frequency lock {}".format(state) + self.write(cmd, custom_err_message=msg) def software_trig(self): - """NOT TESTED: sends a trigger signal to the device (for bursts or modulations)""" + """NOT TESTED: sends a trigger signal to the device + (for bursts or modulations)""" self.write("*TRG", custom_err_message="send trigger signal") - ## ~~~~~~~~~~~~~~~~~~~~~ CUSTOM WAVEFORM FUNCTIONS ~~~~~~~~~~~~~~~~~~~~~~ ## + ## ~~~~~~~~~~~~~~~~~~~~~ CUSTOM WAVEFORM FUNCTIONS ~~~~~~~~~~~~~~~~~~~~~ ## def get_waveform_catalogue(self): """Get list of the waveforms that are in use (not empty) @@ -299,7 +343,7 @@ def get_waveform_catalogue(self): Strings with the names of the user functions that are not empty """ catalogue = self.query("DATA:CATalog?").split(",") - catalogue = [wf[1:-1] for wf in catalogue] # strip off extra quotes + catalogue = [wf[1:-1] for wf in catalogue] # strip off extra quotes return catalogue def get_custom_waveform(self, memory_num): @@ -316,33 +360,40 @@ def get_custom_waveform(self, memory_num): Waveform as ints spanning the resolution of the function gen or and empty array if waveform not in use """ - # find the wavefroms in use + # Find the wavefroms in use waveforms_in_use = self.get_waveform_catalogue() if "USER{}".format(memory_num) in waveforms_in_use: - # copy the waveform to edit memory + # Copy the waveform to edit memory self.write("DATA:COPY EMEMory,USER{}".format(memory_num)) - # get the length of the waveform + # Get the length of the waveform waveform_length = int(self.query("DATA:POINts? EMEMory")) - # get the waveform (returns binary values) - waveform = self.inst.query_binary_values("DATA:DATA? EMEMory", datatype='H', is_big_endian=True, container=np.ndarray) - assert len(waveform) == waveform_length, "Waveform length from native length command (DATA:POINts?) and the processed binary values do not match, {} and {} respectively".format(waveform_length, len(waveform)) + # Get the waveform (returns binary values) + waveform = self.inst.query_binary_values("DATA:DATA? EMEMory", + datatype='H', + is_big_endian=True, + container=np.ndarray) + msg = ("Waveform length from native length command (DATA:POINts?) " + "and the processed binary values do not match, {} and {} " + "respectively".format(waveform_length, len(waveform))) + assert len(waveform) == waveform_length, msg return waveform else: print("Waveform USER{} is not in use".format(memory_num)) return np.array([]) - def set_custom_waveform(self, waveform, normalise=True, memory_num=0, verify=True, print_progress=True): + def set_custom_waveform(self, waveform, normalise=True, memory_num=0, + verify=True, print_progress=True): """Transfer waveform data to edit memory and then user memory. NOTE: Will overwrite without warnings Parameters ---------- waveform : ndarray - Either unnormalised arbitrary waveform (then use `normalise=True`), or - ints spanning the resolution of the function generator + Either unnormalised arbitrary waveform (then use `normalise=True`), + or ints spanning the resolution of the function generator normalise : bool - Choose whether to normalise the waveform to ints over the resolution - span of the function generator + Choose whether to normalise the waveform to ints over the + resolution span of the function generator memory_num : str or int {0,...,255}, default 0 Select which user memory to copy to verify : bool, default `True` @@ -362,33 +413,46 @@ def set_custom_waveform(self, waveform, normalise=True, memory_num=0, verify=Tru If the waveform transferred to the instrument is of a different length than the waveform supplied """ - # check if waveform data is suitable - if print_progress: print("Check if waveform data is suitable..", end=" ") + # Check if waveform data is suitable + if print_progress: + print("Check if waveform data is suitable..", end=" ") self.check_arb_waveform_length(waveform) try: self.check_arb_waveform_type_and_range(waveform) except ValueError as e: - if print_progress: print("\n "+str(e)) - if print_progress: print("Trying again normalising the waveform..", end=" ") + if print_progress: + print("\n "+str(e)) + print("Trying again normalising the waveform..", end=" ") waveform = self.normalise_to_waveform(waveform) - if print_progress: print("ok") - # transfer waveform - if print_progress: print("Transfer waveform to function generator..", end=" ") - self.inst.write_binary_values("DATA:DATA EMEMory,", waveform, datatype='H', is_big_endian=True) - # The first query after the write_binary_values returns '', so here is a mock query + if print_progress: + print("ok") + print("Transfer waveform to function generator..", end=" ") + # Transfer waveform + self.inst.write_binary_values("DATA:DATA EMEMory,", waveform, + datatype='H', is_big_endian=True) + # The first query after the write_binary_values returns '', + # so here is a mock query self.query("") transfer_error = self.get_error() emem_wf_length = self.query("DATA:POINts? EMEMory") if emem_wf_length == '' or not int(emem_wf_length) == len(waveform): - raise RuntimeError("Waveform in temporary EMEMory has a length of {}, not of the same length as the waveform ({}).\nError from the instrument: {}".format(emem_wf_length, len(waveform), transfer_error)) - if print_progress: print("ok") - if print_progress: print("Copy waveform to USER{}..".format(memory_num), end=" ") + msg = ("Waveform in temporary EMEMory has a length of {}, not of " + "the same length as the waveform ({}).\nError from the " + "instrument: {}".format(emem_wf_length, len(waveform), + transfer_error)) + raise RuntimeError(msg) + if print_progress: + print("ok") + print("Copy waveform to USER{}..".format(memory_num), end=" ") self.write("DATA:COPY USER{},EMEMory".format(memory_num)) - if print_progress: print("ok") + if print_progress: + print("ok") if verify: - if print_progress: print("Verify waveform..".format(memory_num)) + if print_progress: + print("Verify waveform USER{}..".format(memory_num)) if "USER{}".format(memory_num) in self.get_waveform_catalogue(): - self.verify_waveform(waveform, memory_num, normalise=normalise, print_result=print_progress) + self.verify_waveform(waveform, memory_num, normalise=normalise, + print_result=print_progress) else: print("(!) USER{} is empty".format(memory_num)) return waveform @@ -408,15 +472,16 @@ def normalise_to_waveform(self, shape): waveform : ndarray Waveform as ints spanning the resolution of the function gen """ - # check if waveform data is suitable + # Check if waveform data is suitable self.check_arb_waveform_length(shape) - # normalise + # Normalise waveform = shape - np.min(shape) normalisation_factor = np.max(waveform) waveform = waveform/normalisation_factor*self.arbitrary_waveform_resolution return waveform.astype(np.uint16) - def verify_waveform(self, waveform, memory_num, normalise=True, print_result=True): + def verify_waveform(self, waveform, memory_num, normalise=True, + print_result=True): """Compare a waveform in user memory to argument waveform Parameters @@ -438,25 +503,32 @@ def verify_waveform(self, waveform, memory_num, normalise=True, print_result=Tru List of the indices where the waveforms are not equal or `None` if the waveforms were of different lengths """ - if normalise:# make sure test waveform is normalised + if normalise: # make sure test waveform is normalised waveform = self.normalise_to_waveform(waveform) - # get the waveform on the instrument + # Get the waveform on the instrument instrument_waveform = self.get_custom_waveform(memory_num) - # compare lengths + # Compare lengths len_inst_wav, len_wav = len(instrument_waveform), len(waveform) if not len_inst_wav == len_wav: - if print_result: print("The waveform in USER{} and the compared waveform are not of same length (instrument {} vs {})".format(memory_num, len_inst_wav, len_wav)) + if print_result: + print("The waveform in USER{} and the compared waveform are " + "not of same length (instrument {} vs {})" + "".format(memory_num, len_inst_wav, len_wav)) return False, instrument_waveform, None - # compare each element + # Compare each element not_equal = [] for i in range(len_wav): if not instrument_waveform[i] == waveform[i]: not_equal.append(i) - # return depending of whether list is empty or not + # Return depending of whether list is empty or not if not not_equal: # if list is empty - if print_result: print("The waveform in USER{} and the compared waveform are equal".format(memory_num)) + if print_result: + print("The waveform in USER{} and the compared waveform " + "are equal".format(memory_num)) return True, instrument_waveform, not_equal - if print_result: print("The waveform in USER{} and the compared waveform are NOT equal".format(memory_num)) + if print_result: + print("The waveform in USER{} and the compared waveform are " + "NOT equal".format(memory_num)) return False, instrument_waveform, not_equal def check_arb_waveform_length(self, waveform): @@ -472,8 +544,12 @@ def check_arb_waveform_length(self, waveform): ValueError If the waveform is not within the permitted length """ - if (len(waveform) < self.arbitrary_waveform_length[0]) or (len(waveform) > self.arbitrary_waveform_length[1]): - raise ValueError("The waveform is of length {}, which is not within the acceptable length {} < len < {}".format(len(waveform), *self.arbitrary_waveform_length)) + if ((len(waveform) < self.arbitrary_waveform_length[0]) or + (len(waveform) > self.arbitrary_waveform_length[1])): + msg = ("The waveform is of length {}, which is not within the " + "acceptable length {} < len < {}" + "".format(len(waveform), *self.arbitrary_waveform_length)) + raise ValueError(msg) def check_arb_waveform_type_and_range(self, waveform): """Checks if waveform is of int/np.int32 type and within the resolution @@ -492,19 +568,23 @@ def check_arb_waveform_type_and_range(self, waveform): """ for value in waveform: if not isinstance(value, (int, np.uint16, np.int32)): - raise ValueError("The waveform contains values that are not int, np.uint16 or np.int32") + raise ValueError("The waveform contains values that are not" + "int, np.uint16 or np.int32") if (value < 0) or (value > self.arbitrary_waveform_resolution): - raise ValueError("The waveform contains values out of range ({} is not within the resolution [0, {}])".format(value, self.arbitrary_waveform_resolution)) + raise ValueError("The waveform contains values out of range " + "({} is not within the resolution " + "[0, {}])".format(value, + self.arbitrary_waveform_resolution)) -## ~~~~~~~~~~~~~~~~~~~~~~~ CHANNEL CLASS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## +## ~~~~~~~~~~~~~~~~~~~~~~~ CHANNEL CLASS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## -class func_gen_channel: +class FuncGenChannel: """Class for controlling a channel on a function generator object Parameters ---------- - func_gen : `func_gen` + fgen : `FuncGen` The function generator object channel : {1, 2} The channel to be controlled @@ -514,7 +594,7 @@ class func_gen_channel: Attributes ---------- - func_gen : `func_gen` + fgen : `FuncGen` The function generator object for which the channel exists channel : {1, 2} The channel number @@ -524,7 +604,7 @@ class func_gen_channel: source : str "SOURce{}:" where {} is the channel number settings_units : list - The units for the settings produced by `func_gen_channel.get_settings` + The units for the settings produced by `FuncGenChannel.get_settings` state_str : dict For conversion from states 1 and 2 to "ON" and "OFF" """ @@ -534,92 +614,162 @@ class func_gen_channel: 1 : "ON", 0 : "OFF"} """Dictionary for converting output states to "ON" and "OFF" """ - def __init__(self, func_gen, channel, impedance): - self.func_gen = func_gen + def __init__(self, fgen, channel, impedance): + self.fgen = fgen self.channel = channel self.source = "SOURce{}:".format(channel) self.impedance = impedance - # adopt limits dictionary from instrument - self.channel_limits = self.func_gen.instrument_limits + # Adopt limits dictionary from instrument + self.channel_limits = copy.deepcopy(self.fgen.instrument_limits) + + def impedance_dependent_limit(self, limit_type): + """Check if the limit type is impedance dependent (voltages) or + not (frequency) + + Returns + ------- + bool + `True` if the limit is impedance dependent + """ + try: # to access the key "min" to check if impedance must be selected + _ = self.channel_limits[limit_type][0]["min"] + return False + except KeyError: # if the key does not exist + # The impedance must be selected + return True def set_stricter_limits(self): """Set limits for the voltage and frequency limits of the channel output through a series of prompts""" - print("Set stricter voltage and frequency limits for channel {}".format(self.channel)) + print("Set stricter voltage and frequency limits " + "for channel {}".format(self.channel)) print("Use enter only to leave a limit unchanged.") # Go through the different limits in the instrument_limits dict - for limit_type, (inst_limit_dict, unit) in self.func_gen.instrument_limits.items(): - try: # to access the key "min" to check if impedance must be selected - _ = inst_limit_dict["min"] - use_impedance = False - except KeyError: # if the key does not exist - # the impedance must be selected - use_impedance = True - inst_limit_dict = inst_limit_dict[self.impedance] + for limit_type, (inst_limit_dict, unit) in self.fgen.instrument_limits.items(): + use_impedance = self.impedance_dependent_limit(limit_type) print("Set {} in {}".format(limit_type, unit), end=" ") if use_impedance: + inst_limit_dict = inst_limit_dict[self.impedance] print("[{} impedance limit]".format(self.impedance)) else: print("") # get new line # Go through the min and max for the limit type for key, inst_value in inst_limit_dict.items(): # prompt for new value - new_value = input(" {} (instrument limit {}{}): ".format(key, inst_value, unit)) + new_value = input(" {} (instrument limit {}{}): " + "".format(key, inst_value, unit)) if new_value == "": - # do not change if empty + # Do not change if empty print("\tLimit not changed") else: try: # to convert to float new_value = float(new_value) except ValueError: - print("\tLimit unchanged: Could not convert {} to float".format(new_value)) + print("\tLimit unchanged: Could not convert \'{}\' " + "to float".format(new_value)) continue # to next item in dict - # check that the new value is within the intrument limits - acceptable_min = key == "min" and new_value > inst_value - current_min = self.channel_limits[limit_type][0][self.impedance]["min"] if use_impedance else self.channel_limits[limit_type][0]["min"] - larger_than_min = new_value > current_min - acceptable_max = key == "max" and new_value < inst_value and larger_than_min - if acceptable_min or acceptable_max: # within the limits - # set the new channel_limit, using the impedance depending on the limit type - if use_impedance: - self.channel_limits[limit_type][0][self.impedance][key] = new_value - else: - self.channel_limits[limit_type][0][key] = new_value - print("\tNew limit set {}{}".format(new_value, unit)) - else: # print description of why the limit was not set - if larger_than_min: - reason = "larger" if key == "max" else "smaller" - print("\tNew limit NOT set: {}{unit} is {} than the instrument limit ({}{unit})".format(new_value, reason, inst_value, unit=unit)) - else: - print("\tNew limit NOT set: {}{unit} is smaller than the current set minimum ({}{unit})".format(new_value, current_min, unit=unit)) - - # get currently used parameters from function generator + # Set the new limit + self.set_limit(limit_type, key, new_value, verbose=True) + + def set_limit(self, limit_type, bound, new_value, verbose=False): + """Set a limit if the new value is within the instrument limits and are + self consistent (max larger than min) + + Parameterers + ------------ + limit_type : str + The name of the limit in the channel_limits dictionary + bound : {"min", "max"} + Specifies if it is the max or the min limit that is to be set + new_value : float + The new value to be used for the limit + verbose : bool + Print confirmation that the limit was set or reason for why the + limit was not set + + Returns + ------- + bool + `True` if new limit set, `False` otherwise + """ + # Short hand references + inst_limit_dict = self.fgen.instrument_limits[limit_type] + channel_limit_dict = self.channel_limits[limit_type] + # Find the instrument limit and unit + use_impedance = self.impedance_dependent_limit(limit_type) + if use_impedance: + inst_value = inst_limit_dict[0][self.impedance][bound] + else: + inst_value = inst_limit_dict[0][bound] + unit = inst_limit_dict[1] + # Check that the new value is within the intrument limits + acceptable_min = bound == "min" and new_value > inst_value + if use_impedance: + current_min = channel_limit_dict[0][self.impedance]["min"] + else: + current_min = channel_limit_dict[0]["min"] + larger_than_min = new_value > current_min + acceptable_max = bound == "max" and new_value < inst_value and larger_than_min + if acceptable_min or acceptable_max: # within the limits + # Set the new channel_limit, using the impedance depending on the + # limit type. Beware that the shorthand cannot be used, as this + # only changes the shorthand not the dictionary itself + if use_impedance: + self.channel_limits[limit_type][0][self.impedance][bound] = new_value + else: + self.channel_limits[limit_type][0][bound] = new_value + if verbose: + print("\tNew limit set {}{}".format(new_value, unit)) + return True + elif verbose: # print description of why the limit was not set + if larger_than_min: + reason = "larger" if bound == "max" else "smaller" + print("\tNew limit NOT set: {}{unit} is {} than the instrument " + "limit ({}{unit})".format(new_value, reason, inst_value, + unit=unit)) + else: + print("\tNew limit NOT set: {}{unit} is smaller than the " + "current set minimum ({}{unit})".format(new_value, + current_min, + unit=unit)) + return False + + # Get currently used parameters from function generator def get_output_state(self): """Returns "0" for "OFF", "1" for "ON" """ - return self.func_gen.query("OUTPut{}:STATe?".format(self.channel)) + return self.fgen.query("OUTPut{}:STATe?".format(self.channel)) + def get_function(self): """Returns string of function name""" - return self.func_gen.query("{}FUNCtion:SHAPe?".format(self.source)) + return self.fgen.query("{}FUNCtion:SHAPe?".format(self.source)) + def get_amplitude(self): """Returns peak-to-peak voltage in volts""" - return float(self.func_gen.query("{}VOLTage:AMPLitude?".format(self.source))) + return float(self.fgen.query("{}VOLTage:AMPLitude?".format(self.source))) + def get_offset(self): """Returns offset voltage in volts""" - return float(self.func_gen.query("{}VOLTage:OFFSet?".format(self.source))) + return float(self.fgen.query("{}VOLTage:OFFSet?".format(self.source))) + def get_frequency(self): """Returns frequency in Hertz""" - return float(self.func_gen.query("{}FREQuency?".format(self.source))) + return float(self.fgen.query("{}FREQuency?".format(self.source))) - # get limits set in the channel class + # Get limits set in the channel class def get_frequency_lims(self): """Returns list of min and max frequency limits""" - return [self.channel_limits["frequency lims"][0][key] for key in ["min", "max"]] + return [self.channel_limits["frequency lims"][0][key] + for key in ["min", "max"]] + def get_voltage_lims(self): """Returns list of min and max voltage limits for the current impedance""" - return [self.channel_limits["voltage lims"][0][self.impedance][key] for key in ["min", "max"]] + return [self.channel_limits["voltage lims"][0][self.impedance][key] + for key in ["min", "max"]] + def get_amplitude_lims(self): """Returns list of min and max amplitude limits for the current impedance""" - return [self.channel_limits["amplitude lims"][0][self.impedance][key] for key in ["min", "max"]] + return [self.channel_limits["amplitude lims"][0][self.impedance][key] + for key in ["min", "max"]] def get_settings(self): """Get the settings for the channel @@ -627,9 +777,9 @@ def get_settings(self): Returns ------- current_settings : dict - Settings currently in use as a dictionary with keys output, function, - amplitude, offset, and frequency and values tuples of the corresponding - return and unit + Settings currently in use as a dictionary with keys output, + function, amplitude, offset, and frequency and values tuples of + the corresponding return and unit """ return {"output": (self.state_str[self.get_output_state()], ""), "function": (self.get_function(), ""), @@ -638,17 +788,39 @@ def get_settings(self): "frequency": (self.get_frequency(), "Hz")} def print_settings(self): - """Print the settings currently in use for the channel - (Recommended to use the `func_gen.print_settings` for printing both channels) + """Print the settings currently in use for the channel (Recommended + to use the `FuncGen.print_settings` for printing both channels) """ settings = self.get_settings() longest_key = max([len(key) for key in settings.keys()]) print("\nCurrent settings for channel {}".format(self.channel)) print("==============================") - for key, (val, unit) in zip(settings.items()): - print("{:>{num_char}s} {} {}".format(key, val, unit, num_char=longest_key)) - - def set_output(self, state): + for key, (val, unit) in settings.items(): + print("{:>{num_char}s} {} {}".format(key, val, unit, + num_char=longest_key)) + + def set_settings(self, settings): + """Set the settings of the channel with a settings dictionary. Will + set the outout to OFF before applyign the settings (and turn the + channel ON or leave it OFF depending on the settings dict) + + Parameteres + ----------- + settings : dict + Settings dictionary as returned by `get_settings`: should have + keys output, function, amplitude, offset, and frequency + """ + # First turn off to ensure no potentially harmful + # combination of settings + self.set_output_state("OFF") + # Set settings according to dictionary + self.set_function(settings['function'][0]) + self.set_amplitude(settings['amplitude'][0]) + self.set_offset(settings['offset'][0]) + self.set_frequency(settings['frequency'][0]) + self.set_output_state(settings['output'][0]) + + def set_output_state(self, state): """Enables or diables the output of the channel Parameters @@ -660,38 +832,65 @@ def set_output(self, state): Raises ------ NotSetError - If `self.func_gen.verify_param_set` is `True` and the value after + If `self.fgen.verify_param_set` is `True` and the value after applying the set function does not match the value returned by the get function """ - self.func_gen.write("OUTPut{}:STATe {}".format(self.channel, state), - custom_err_message="turn channel {} to state {}".format(self.channel, state)) - if self.func_gen.verify_param_set: - actual_state = self.get_output() + err_msg = "turn channel {} to state {}".format(self.channel, state) + self.fgen.write("OUTPut{}:STATe {}".format(self.channel, state), + custom_err_message=err_msg) + if self.fgen.verify_param_set: + actual_state = self.get_output_state() if not actual_state == state: - raise NotSetError("Channel {} was not turned {}, it is {}.\nError from the instrument: {}".format(self.channel, state, self.state_str[actual_state], self.func_gen.get_error())) + msg = ("Channel {} was not turned {}, it is {}.\n" + "Error from the instrument: {}" + "".format(self.channel, state, + self.state_str[actual_state], + self.fgen.get_error())) + raise NotSetError(msg) + + def get_output(self): + """Wrapper for get_output_state""" + return self.get_output_state() + + def set_output(self, state): + """Wrapper for set_output_state""" + self.set_output_state(state) def set_function(self, shape): """Set the function shape of the output Parameters ---------- - shape : {SINusoid, SQUare, PULSe, RAMP, PRNoise, , USER[0], USER1, ..., USER255, EMEMory, EFILe} - ::={StairDown|StairUp|Stair Up&Dwn|Trapezoid|RoundHalf|AbsSine|AbsHalfSine|ClippedSine|ChoppedSine|NegRamp|OscRise|OscDecay|CodedPulse|PosPulse|NegPulse|ExpRise|ExpDecay|Sinc|Tan|Cotan|SquareRoot|X^2|HaverSine|Lorentz|Ln(x)|X^3|CauchyDistr|BesselJ|BesselY|ErrorFunc|Airy|Rectangle|Gauss|Hamming|Hanning|Bartlett|Blackman|Laylight|Triangle|DC|Heart|Round|Chirp|Rhombus|Cardiac} + shape : {SINusoid, SQUare, PULSe, RAMP, PRNoise, , USER[0], + USER1, ..., USER255, EMEMory, EFILe} + ::={StairDown|StairUp|Stair Up&Dwn|Trapezoid|RoundHalf| + AbsSine|AbsHalfSine|ClippedSine|ChoppedSine|NegRamp|OscRise| + OscDecay|CodedPulse|PosPulse|NegPulse|ExpRise|ExpDecay|Sinc| + Tan|Cotan|SquareRoot|X^2|HaverSine|Lorentz|Ln(x)|X^3|CauchyDistr| + BesselJ|BesselY|ErrorFunc|Airy|Rectangle|Gauss|Hamming|Hanning| + Bartlett|Blackman|Laylight|Triangle|DC|Heart|Round|Chirp|Rhombus| + Cardiac} Raises ------ NotSetError - If `self.func_gen.verify_param_set` is `True` and the value after + If `self.fgen.verify_param_set` is `True` and the value after applying the set function does not match the value returned by the get function """ - self.func_gen.write("{}FUNCtion:SHAPe {}".format(self.source, shape), - custom_err_message="set function {}".format(shape)) - if self.func_gen.verify_param_set: + cmd = "{}FUNCtion:SHAPe {}".format(self.source, shape) + self.fgen.write(cmd, custom_err_message="set function {}".format(shape)) + if self.fgen.verify_param_set: actual_shape = self.get_function() if not actual_shape == shape: - raise NotSetError("Function {} was not set on channel {}, it is {}. Check that the function name is correctly spelt. Run set_function.__doc__ to see available shapes.\nError from the instrument: {}".format(shape, self.channel, actual_shape, self.func_gen.get_error())) + msg = ("Function {} was not set on channel {}, it is {}. " + "Check that the function name is correctly spelt. " + "Run set_function.__doc__ to see available shapes.\n" + "Error from the instrument: {}" + "".format(shape, self.channel, actual_shape, + self.fgen.get_error())) + raise NotSetError(msg) def set_amplitude(self, amplitude): """Set the peak-to-peak amplitude in volts @@ -699,43 +898,60 @@ def set_amplitude(self, amplitude): Parameters ---------- amplitude : float or {"max", "min"} - 0.1mV or four digits resolution, "max" or "min" will set the amplitude - to the maximum or minimum limit given in `channel_limits` + 0.1mV or four digits resolution, "max" or "min" will set the + amplitude to the maximum or minimum limit given in `channel_limits` Raises ------ NotSetError - If `self.func_gen.verify_param_set` is `True` and the value after + If `self.fgen.verify_param_set` is `True` and the value after applying the set function does not match the value returned by the get function """ - # check if keyword min or max is given + # Check if keyword min or max is given if str(amplitude).lower() in ["min", "max"]: unit = "" # no unit for MIN/MAX - # look up what the limit is for this keyword + # Look up what the limit is for this keyword amplitude = self.channel_limits["amplitude lims"][0][self.impedance][str(amplitude).lower()] else: unit = "Vpp" - # check if the given amplitude is within the current limits + # Check if the given amplitude is within the current limits min_ampl, max_ampl = self.get_amplitude_lims() if amplitude < min_ampl or amplitude > max_ampl: - raise NotSetError("Could not set the amplitude {}{unit} as it is not within the amplitude limits set for the instrument [{}, {}]{unit}".format(amplitude, min_ampl, max_ampl, unit=unit)) - # check that the new amplitude will not violate voltage limits + msg = ("Could not set the amplitude {}{unit} as it is not " + "within the amplitude limits set for the instrument " + "[{}, {}]{unit}".format(amplitude, min_ampl, max_ampl, + unit=unit)) + raise NotSetError(msg) + # Check that the new amplitude will not violate voltage limits min_volt, max_volt = self.get_voltage_lims() current_offset = self.get_offset() - if amplitude/2-current_offset < min_volt or amplitude/2+current_offset > max_volt: - raise NotSetError("Could not set the amplitude {}{unit} as the amplitude combined with the offset ({}V) will be outside the absolute voltage limits [{}, {}]{unit}".format(amplitude, current_offset, min_volt, max_volt, unit=unit)) - # set the amplitude - self.func_gen.write("{}VOLTage:LEVel {}{}".format(self.source, amplitude, unit), - custom_err_message="set amplitude {}{}".format(amplitude, unit)) - # verify that the amplitude has been set - if self.func_gen.verify_param_set: + if (amplitude/2-current_offset < min_volt or + amplitude/2+current_offset > max_volt): + msg = ("Could not set the amplitude {}{unit} as the amplitude " + "combined with the offset ({}V) will be outside the " + "absolute voltage limits [{}, {}]{unit}" + "".format(amplitude, current_offset, min_volt, max_volt, + unit=unit)) + raise NotSetError(msg) + # Set the amplitude + cmd = "{}VOLTage:LEVel {}{}".format(self.source, amplitude, unit) + err_msg = "set amplitude {}{}".format(amplitude, unit) + self.fgen.write(cmd, custom_err_message=err_msg) + # Verify that the amplitude has been set + if self.fgen.verify_param_set: actual_amplitude = self.get_amplitude() - # multiply with the appropriate factor according to SI prefix, or + # Multiply with the appropriate factor according to SI prefix, or # if string is empty, use the value looked up from channel_limits earlier check_amplitude = amplitude*SI_prefix_to_factor(unit) if not unit == "" else amplitude if not actual_amplitude == check_amplitude: - raise NotSetError("Amplitude {}{} was not set on channel {}, it is {}Vpp. Check that the number is within the possible range and in the correct format.\nError from the instrument: {}".format(amplitude, unit, self.channel, actual_amplitude, self.func_gen.get_error())) + msg = ("Amplitude {}{} was not set on channel {}, it is " + "{}Vpp. Check that the number is within the possible " + "range and in the correct format.\nError from the " + "instrument: {}" + "".format(amplitude, unit, self.channel, + actual_amplitude, self.fgen.get_error())) + raise NotSetError(msg) def set_offset(self, offset, unit="V"): """Set offset in volts (or mV, see options) @@ -749,25 +965,38 @@ def set_offset(self, offset, unit="V"): Raises ------ NotSetError - If `self.func_gen.verify_param_set` is `True` and the value after + If `self.fgen.verify_param_set` is `True` and the value after applying the set function does not match the value returned by the get function """ - # check that the new offset will not violate voltage limits + # Check that the new offset will not violate voltage limits min_volt, max_volt = self.get_voltage_lims() current_amplitude = self.get_amplitude() - if current_amplitude/2-offset < min_volt or current_amplitude/2+offset > max_volt: - raise NotSetError("Could not set the offset {}{unit} as the offset combined with the amplitude ({}V) will be outside the absolute voltage limits [{}, {}]{unit}".format(offset, current_amplitude, min_volt, max_volt, unit=unit)) - # set the offset - self.func_gen.write("{}VOLTage:LEVel:OFFSet {}{}".format(self.source, offset, unit), - custom_err_message="set offset {}{}".format(offset, unit)) - # verify that the offset has been set - if self.func_gen.verify_param_set: + if (current_amplitude/2-offset < min_volt or + current_amplitude/2+offset > max_volt): + msg = ("Could not set the offset {}{unit} as the offset combined" + "with the amplitude ({}V) will be outside the absolute " + "voltage limits [{}, {}]{unit}".format(offset, + current_amplitude, + min_volt, max_volt, + unit=unit)) + raise NotSetError(msg) + # Set the offset + cmd = "{}VOLTage:LEVel:OFFSet {}{}".format(self.source, offset, unit) + err_msg = "set offset {}{}".format(offset, unit) + self.fgen.write(cmd, custom_err_message=err_msg) + # Verify that the offset has been set + if self.fgen.verify_param_set: actual_offset = self.get_offset() - # multiply with the appropriate factor according to SI prefix + # Multiply with the appropriate factor according to SI prefix check_offset = offset*SI_prefix_to_factor(unit) if not actual_offset == check_offset: - raise NotSetError("Offset {}{} was not set on channel {}, it is {}V. Check that the number is within the possible range and in the correct format.\nError from the instrument: {}".format(offset, unit, self.channel, actual_offset, self.func_gen.get_error())) + msg = ("Offset {}{} was not set on channel {}, it is {}V. " + "Check that the number is within the possible range and " + "in the correct format.\nError from the instrument: {}" + "".format(offset, unit, self.channel, actual_offset, + self.fgen.get_error())) + raise NotSetError(msg) def set_frequency(self, freq, unit="Hz"): """Set the frequency in Hertz (or kHz, MHz, see options) @@ -781,43 +1010,51 @@ def set_frequency(self, freq, unit="Hz"): Raises ------ NotSetError - If `self.func_gen.verify_param_set` is `True` and the value after + If `self.fgen.verify_param_set` is `True` and the value after applying the set function does not match the value returned by the get function """ if str(freq).lower() in ["min", "max"]: # handle min and max keywords unit = "" # no unit for MIN/MAX - # look up what the limit is for this keyword + # Look up what the limit is for this keyword freq = self.channel_limits["frequency lims"][0][str(freq).lower()] else: - # check if the given frequency is within the current limits + # Check if the given frequency is within the current limits min_freq, max_freq = self.get_frequency_lims() if freq < min_freq or freq > max_freq: - raise NotSetError("Could not set the frequency {}{} as it is not within the frequency limits set for the instrument [{}, {}]Hz".format(freq, unit, min_freq, max_freq)) - # check that the new amplitude will not violate voltage limits + msg = ("Could not set the frequency {}{} as it is not within " + "the frequency limits set for the instrument [{}, {}]" + "Hz".format(freq, unit, min_freq, max_freq)) + raise NotSetError(msg) + # Check that the new amplitude will not violate voltage limits min_volt, max_volt = self.get_voltage_lims() - # set the frequency - self.func_gen.write("{}FREQuency:FIXed {}{}".format(self.source, freq, unit), + # Set the frequency + self.fgen.write("{}FREQuency:FIXed {}{}".format(self.source, freq, unit), custom_err_message="set frequency {}{}".format(freq, unit)) - # verify that the amplitude has been set - if self.func_gen.verify_param_set: + # Verify that the amplitude has been set + if self.fgen.verify_param_set: actual_freq = self.get_frequency() - # multiply with the appropriate factor according to SI prefix, or + # Multiply with the appropriate factor according to SI prefix, or # if string is empty, use the value looked up from channel_limits earlier check_freq = freq*SI_prefix_to_factor(unit) if not unit == "" else freq if not actual_freq == check_freq: - raise NotSetError("Frequency {}{} was not set on channel {}, it is {}Hz. Check that the number is within the possible range and in the correct format.\nError from the instrument: {}".format(freq, unit, self.channel, actual_freq, self.func_gen.get_error())) + msg = ("Frequency {}{} was not set on channel {}, it is {}Hz. " + "Check that the number is within the possible range and in " + "the correct format.\nError from the instrument: {}" + "".format(freq, unit, self.channel, actual_freq, + self.fgen.get_error())) + raise NotSetError(msg) -## ~~~~~~~~~~~~~~~~~~~~~~~~~~~ AUXILIARY ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## +## ~~~~~~~~~~~~~~~~~~~~~~~~~~~ AUXILIARY ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## class NotSetError(Exception): """Error for when a value cannot be written to the instrument""" - pass + class NotCompatibleError(Exception): """Error for when the instrument is not compatible with this module""" - pass + def SI_prefix_to_factor(unit): """Convert an SI prefix to a numerical factor @@ -825,75 +1062,127 @@ def SI_prefix_to_factor(unit): Parameters ---------- unit : str - The unit whose first character is checked against the list of prefactors - {"M": 1e6, "k": 1e3, "m": 1e-3} + The unit whose first character is checked against the list of + prefactors {"M": 1e6, "k": 1e3, "m": 1e-3} Returns ------- factor : float or `None` - The appropriate factor or 1 if not found in the list, or `None` if the unit string is empty + The appropriate factor or 1 if not found in the list, or `None` + if the unit string is empty """ # SI prefix to numerical value - SI_conversion = {"M": 1e6, "k": 1e3, "m": 1e-3} - try: # using the unit's first character as key in the dictionary + SI_conversion = {"M":1e6, "k":1e3, "m":1e-3} + try: # using the unit's first character as key in the dictionary factor = SI_conversion[unit[0]] - except KeyError: # if the entry does not exist + except KeyError: # if the entry does not exist factor = 1 - except IndexError: # if the unit string is empty + except IndexError: # if the unit string is empty factor = None return factor -## ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ EXAMPLES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## - -def example_print_current_settings(address): - """Example showing how to connect and get the current settings of the instrument""" - with func_gen(address) as fgen: +## ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ EXAMPLES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## + +def example_basic_control(address): + """Example showing how to connect, and the most basic control of the + instrument parameteres""" + print("\n\n", example_basic_control.__doc__) + with FuncGen(address) as fgen: + fgen.ch1.set_function("SIN") + fgen.ch1.set_frequency(25, unit="Hz") + fgen.ch1.set_offset(50, unit="mV") + fgen.ch1.set_amplitude(0.002) + fgen.ch1.set_output("ON") + fgen.ch2.set_output("OFF") + # Alternatively fgen.ch1.print_settings() to show from one channel only + fgen.print_settings() + + +def example_change_settings(address): + """Example showing how to get the current settings of the instrument, + store them, change a setting and then restore the initial settings""" + print("\n\n", example_change_settings.__doc__) + with FuncGen(address) as fgen: + fgen.print_settings() + print("Saving these settings..") + settings = fgen.get_settings() + print("Change to 1Vpp amplitude for channel 1..") + fgen.ch1.set_amplitude(1) + fgen.print_settings() + print("Reset back to initial settings..") + fgen.set_settings(settings) fgen.print_settings() + def example_lock_frequencies(address): """Example showing the frequency being set to 10Hz and then the frequency lock enabled, using the frequency at ch1 as the common frequency""" - with func_gen(address, verbose=False) as fgen: + print("\n\n", example_lock_frequencies.__doc__) + with FuncGen(address, verbose=False) as fgen: fgen.ch1.set_frequency(10) - fgen.enable_frequency_lock(use_channel=1) + fgen.set_frequency_lock("ON", use_channel=1) + + +def example_changing_limits(address): + """Example showing how limits can be read and changed""" + print("\n\n", example_changing_limits.__doc__) + with FuncGen(address) as fgen: + lims = fgen.ch1.get_frequency_lims() + print("Channel 1 frequency limits: {}".format(lims)) + print("Change the lower limit to 2Hz..") + fgen.ch1.set_limit("frequency lims", "min", 2) + lims = fgen.ch1.get_frequency_lims() + print("Channel 1 frequency limits: {}".format(lims)) + print("Try to set ch1 frequency to 1Hz..") + try: + fgen.ch1.set_frequency(1) + except NotSetError as err: + print(err) + -def example_set_and_use_custom_waveform(fgen=None, channel=1, plot_signal=True): +def example_set_and_use_custom_waveform(fgen=None, address=None, channel=1, + plot_signal=True): """Example showing a waveform being created, transferred to the instrument, and applied to a channel""" - # create a signal + print("\n\n", example_set_and_use_custom_waveform.__doc__) + # Create a signal x = np.linspace(0, 4*np.pi, 8000) signal = np.sin(x)+x/5 if plot_signal: # plot the signal for visual control import matplotlib.pyplot as plt plt.plot(signal) plt.show() - # create initialise fgen if it was not supplied + # Create initialise fgen if it was not supplied if fgen is None: - fgen = func_gen() + fgen = FuncGen(address) close_fgen = True # specify that it should be closed at end of function else: close_fgen = False # do not close the supplied fgen at end print("Current waveform catalogue") - for i, wav in enumerate(fgen.get_waveform_catalogue()): print(" {}: {}".format(i, wav)) - # transfer the waveform + for i, wav in enumerate(fgen.get_waveform_catalogue()): + print(" {}: {}".format(i, wav)) + # Transfer the waveform fgen.set_custom_waveform(signal, memory_num=5, verify=True) print("New waveform catalogue:") - for i, wav in enumerate(fgen.get_waveform_catalogue()): print(" {}: {}".format(i, wav)) + for i, wav in enumerate(fgen.get_waveform_catalogue()): + print(" {}: {}".format(i, wav)) print("Set new wavefrom to channel {}..".format(channel), end=" ") - fgen.channels[channel-1].set_output("OFF") + fgen.channels[channel-1].set_output_state("OFF") fgen.channels[channel-1].set_function("USER5") print("ok") - # print current settings + # Print current settings fgen.get_settings() if close_fgen: fgen.close() -## ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ MAIN FUNCTION ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## +## ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ MAIN FUNCTION ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## if __name__ == '__main__': address = 'USB0::0x0699::0x0353::1731975::INSTR' - example_print_current_settings(address) + example_basic_control(address) + example_change_settings(address) example_lock_frequencies(address) - with func_gen(address) as fgen: + example_changing_limits(address) + with FuncGen(address) as fgen: example_set_and_use_custom_waveform(fgen)