diff --git a/instruments/abstract_instruments/function_generator.py b/instruments/abstract_instruments/function_generator.py index e9baa64be..f472d85dc 100644 --- a/instruments/abstract_instruments/function_generator.py +++ b/instruments/abstract_instruments/function_generator.py @@ -12,13 +12,13 @@ import abc from enum import Enum +from builtins import range from future.utils import with_metaclass import quantities as pq - from instruments.abstract_instruments import Instrument import instruments.units as u -from instruments.util_fns import assume_units +from instruments.util_fns import assume_units, ProxyList # CLASSES ##################################################################### @@ -32,6 +32,175 @@ class FunctionGenerator(with_metaclass(abc.ABCMeta, Instrument)): provide a consistent interface to the user. """ + def __init__(self, filelike): + super(FunctionGenerator, self).__init__(filelike) + self._channel_count = 1 + + # pylint:disable=protected-access + class Channel(with_metaclass(abc.ABCMeta, object)): + """ + Abstract base class for physical channels on a function generator. + + All applicable concrete instruments should inherit from this ABC to + provide a consistent interface to the user. + + Function generators that only have a single channel do not need to + define their own concrete implementation of this class. Ones with + multiple channels need their own definition of this class, where + this class contains the concrete implementations of the below + abstract methods. Instruments with 1 channel have their concrete + implementations at the parent instrument level. + """ + def __init__(self, parent, name): + self._parent = parent + self._name = name + + # ABSTRACT PROPERTIES # + + @property + def frequency(self): + """ + Gets/sets the the output frequency of the function generator. This is + an abstract property. + + :type: `~quantities.Quantity` + """ + if self._parent._channel_count == 1: + return self._parent.frequency + else: + raise NotImplementedError() + + @frequency.setter + def frequency(self, newval): + if self._parent._channel_count == 1: + self._parent.frequency = newval + else: + raise NotImplementedError() + + @property + def function(self): + """ + Gets/sets the output function mode of the function generator. This is + an abstract property. + + :type: `~enum.Enum` + """ + if self._parent._channel_count == 1: + return self._parent.function + else: + raise NotImplementedError() + + @function.setter + def function(self, newval): + if self._parent._channel_count == 1: + self._parent.function = newval + else: + raise NotImplementedError() + + @property + def offset(self): + """ + Gets/sets the output offset voltage of the function generator. This is + an abstract property. + + :type: `~quantities.Quantity` + """ + if self._parent._channel_count == 1: + return self._parent.offset + else: + raise NotImplementedError() + + @offset.setter + def offset(self, newval): + if self._parent._channel_count == 1: + self._parent.offset = newval + else: + raise NotImplementedError() + + @property + def phase(self): + """ + Gets/sets the output phase of the function generator. This is an + abstract property. + + :type: `~quantities.Quantity` + """ + if self._parent._channel_count == 1: + return self._parent.phase + else: + raise NotImplementedError() + + @phase.setter + def phase(self, newval): + if self._parent._channel_count == 1: + self._parent.phase = newval + else: + raise NotImplementedError() + + def _get_amplitude_(self): + if self._parent._channel_count == 1: + return self._parent._get_amplitude_() + else: + raise NotImplementedError() + + def _set_amplitude_(self, magnitude, units): + if self._parent._channel_count == 1: + self._parent._set_amplitude_(magnitude=magnitude, units=units) + else: + raise NotImplementedError() + + @property + def amplitude(self): + """ + Gets/sets the output amplitude of the function generator. + + If set with units of :math:`\\text{dBm}`, then no voltage mode can + be passed. + + If set with units of :math:`\\text{V}` as a `~quantities.Quantity` or a + `float` without a voltage mode, then the voltage mode is assumed to be + peak-to-peak. + + :units: As specified, or assumed to be :math:`\\text{V}` if not + specified. + :type: Either a `tuple` of a `~quantities.Quantity` and a + `FunctionGenerator.VoltageMode`, or a `~quantities.Quantity` + if no voltage mode applies. + """ + mag, units = self._get_amplitude_() + + if units == self._parent.VoltageMode.dBm: + return pq.Quantity(mag, u.dBm) + + return pq.Quantity(mag, pq.V), units + + @amplitude.setter + def amplitude(self, newval): + # Try and rescale to dBm... if it succeeds, set the magnitude + # and units accordingly, otherwise handle as a voltage. + try: + newval_dbm = newval.rescale(u.dBm) + mag = float(newval_dbm.magnitude) + units = self._parent.VoltageMode.dBm + except (AttributeError, ValueError): + # OK, we have volts. Now, do we have a tuple? If not, assume Vpp. + if not isinstance(newval, tuple): + mag = newval + units = self._parent.VoltageMode.peak_to_peak + else: + mag, units = newval + + # Finally, convert the magnitude out to a float. + mag = float(assume_units(mag, pq.V).rescale(pq.V).magnitude) + + self._set_amplitude_(mag, units) + + def sendcmd(self, cmd): + self._parent.sendcmd(cmd) + + def query(self, cmd, size=-1): + return self._parent.query(cmd, size) + # ENUMS # class VoltageMode(Enum): @@ -53,20 +222,21 @@ class Function(Enum): noise = 'NOIS' arbitrary = 'ARB' - # ABSTRACT METHODS # + @property + def channel(self): + return ProxyList(self, self.Channel, range(self._channel_count)) - @abc.abstractmethod - def _get_amplitude_(self): - pass + # PASSTHROUGH PROPERTIES # - @abc.abstractmethod - def _set_amplitude_(self, magnitude, units): - pass + @property + def amplitude(self): + return self.channel[0].amplitude - # ABSTRACT PROPERTIES # + @amplitude.setter + def amplitude(self, newval): + self.channel[0].amplitude = newval @property - @abc.abstractmethod def frequency(self): """ Gets/sets the the output frequency of the function generator. This is @@ -74,15 +244,19 @@ def frequency(self): :type: `~quantities.Quantity` """ - pass + if self._channel_count > 1: + return self.channel[0].frequency + else: + raise NotImplementedError() @frequency.setter - @abc.abstractmethod def frequency(self, newval): - pass + if self._channel_count > 1: + self.channel[0].frequency = newval + else: + raise NotImplementedError() @property - @abc.abstractmethod def function(self): """ Gets/sets the output function mode of the function generator. This is @@ -90,15 +264,19 @@ def function(self): :type: `~enum.Enum` """ - pass + if self._channel_count > 1: + return self.channel[0].function + else: + raise NotImplementedError() @function.setter - @abc.abstractmethod def function(self, newval): - pass + if self._channel_count > 1: + self.channel[0].function = newval + else: + raise NotImplementedError() @property - @abc.abstractmethod def offset(self): """ Gets/sets the output offset voltage of the function generator. This is @@ -106,15 +284,19 @@ def offset(self): :type: `~quantities.Quantity` """ - pass + if self._channel_count > 1: + return self.channel[0].offset + else: + raise NotImplementedError() @offset.setter - @abc.abstractmethod def offset(self, newval): - pass + if self._channel_count > 1: + self.channel[0].offset = newval + else: + raise NotImplementedError() @property - @abc.abstractmethod def phase(self): """ Gets/sets the output phase of the function generator. This is an @@ -122,57 +304,14 @@ def phase(self): :type: `~quantities.Quantity` """ - pass + if self._channel_count > 1: + return self.channel[0].phase + else: + raise NotImplementedError() @phase.setter - @abc.abstractmethod def phase(self, newval): - pass - - # CONCRETE PROPERTIES # - - @property - def amplitude(self): - """ - Gets/sets the output amplitude of the function generator. - - If set with units of :math:`\\text{dBm}`, then no voltage mode can - be passed. - - If set with units of :math:`\\text{V}` as a `~quantities.Quantity` or a - `float` without a voltage mode, then the voltage mode is assumed to be - peak-to-peak. - - :units: As specified, or assumed to be :math:`\\text{V}` if not - specified. - :type: Either a `tuple` of a `~quantities.Quantity` and a - `FunctionGenerator.VoltageMode`, or a `~quantities.Quantity` - if no voltage mode applies. - """ - mag, units = self._get_amplitude_() - - if units == self.VoltageMode.dBm: - return pq.Quantity(mag, u.dBm) - - return pq.Quantity(mag, pq.V), units - - @amplitude.setter - def amplitude(self, newval): - # Try and rescale to dBm... if it succeeds, set the magnitude - # and units accordingly, otherwise handle as a voltage. - try: - newval_dbm = newval.rescale(u.dBm) - mag = float(newval_dbm.magnitude) - units = self.VoltageMode.dBm - except (AttributeError, ValueError): - # OK, we have volts. Now, do we have a tuple? If not, assume Vpp. - if not isinstance(newval, tuple): - mag = newval - units = self.VoltageMode.peak_to_peak - else: - mag, units = newval - - # Finally, convert the magnitude out to a float. - mag = float(assume_units(mag, pq.V).rescale(pq.V).magnitude) - - self._set_amplitude_(mag, units) + if self._channel_count > 1: + self.channel[0].phase = newval + else: + raise NotImplementedError() diff --git a/instruments/tests/__init__.py b/instruments/tests/__init__.py index ea901d886..b54a355ad 100644 --- a/instruments/tests/__init__.py +++ b/instruments/tests/__init__.py @@ -26,7 +26,7 @@ @contextlib.contextmanager -def expected_protocol(ins_class, host_to_ins, ins_to_host, sep="\n"): +def expected_protocol(ins_class, host_to_ins, ins_to_host, sep="\n", repeat=1): """ Given an instrument class, expected output from the host and expected input from the instrument, asserts that the protocol in a context block proceeds @@ -35,7 +35,8 @@ def expected_protocol(ins_class, host_to_ins, ins_to_host, sep="\n"): For an example of how to write tests using this context manager, see the ``make_name_test`` function below. - :param type ins_class: Instrument class to use for the protocol assertion. + :param ins_class: Instrument class to use for the protocol assertion. + :type ins_class: `~instruments.Instrument` :param host_to_ins: Data to be sent by the host to the instrument; this is checked against the actual data sent by the instrument class during the execution of this context manager. @@ -46,9 +47,17 @@ def expected_protocol(ins_class, host_to_ins, ins_to_host, sep="\n"): be used to assert correct behaviour within the context. :type ins_to_host: ``str`` or ``list``; if ``list``, each line is concatenated with the separator given by ``sep``. + :param str sep: Character to be inserted after each string in both + host_to_ins and ins_to_host parameters. This is typically the + termination character you would like to have inserted. + :param int repeat: The number of times the host_to_ins and + ins_to_host data sets should be duplicated. Typically the default + value of 1 is sufficient, but increasing this is useful when + testing multiple calls in the same test that should have the same + command transactions. """ if isinstance(sep, bytes): - sep = sep.encode("utf-8") + sep = sep.decode("utf-8") # Normalize assertion and playback strings. if isinstance(ins_to_host, list): @@ -60,6 +69,7 @@ def expected_protocol(ins_class, host_to_ins, ins_to_host, sep="\n"): (sep.encode("utf-8") if ins_to_host else b"") elif isinstance(ins_to_host, str): ins_to_host = ins_to_host.encode("utf-8") + ins_to_host *= repeat if isinstance(host_to_ins, list): host_to_ins = [ @@ -70,6 +80,7 @@ def expected_protocol(ins_class, host_to_ins, ins_to_host, sep="\n"): (sep.encode("utf-8") if host_to_ins else b"") elif isinstance(host_to_ins, str): host_to_ins = host_to_ins.encode("utf-8") + host_to_ins *= repeat stdin = BytesIO(ins_to_host) stdout = BytesIO() diff --git a/instruments/tests/test_generic_scpi/test_scpi_function_generator.py b/instruments/tests/test_generic_scpi/test_scpi_function_generator.py index 6920a3f4b..d32456728 100644 --- a/instruments/tests/test_generic_scpi/test_scpi_function_generator.py +++ b/instruments/tests/test_generic_scpi/test_scpi_function_generator.py @@ -31,12 +31,17 @@ def test_scpi_func_gen_amplitude(): ], [ "VPP", "+1.000000E+00" - ] + ], + repeat=2 ) as fg: assert fg.amplitude == (1 * pq.V, fg.VoltageMode.peak_to_peak) fg.amplitude = 2 * pq.V fg.amplitude = (1.5 * pq.V, fg.VoltageMode.dBm) + assert fg.channel[0].amplitude == (1 * pq.V, fg.VoltageMode.peak_to_peak) + fg.channel[0].amplitude = 2 * pq.V + fg.channel[0].amplitude = (1.5 * pq.V, fg.VoltageMode.dBm) + def test_scpi_func_gen_frequency(): with expected_protocol( @@ -46,11 +51,15 @@ def test_scpi_func_gen_frequency(): "FREQ 1.005000e+02" ], [ "+1.234000E+03" - ] + ], + repeat=2 ) as fg: assert fg.frequency == 1234 * pq.Hz fg.frequency = 100.5 * pq.Hz + assert fg.channel[0].frequency == 1234 * pq.Hz + fg.channel[0].frequency = 100.5 * pq.Hz + def test_scpi_func_gen_function(): with expected_protocol( @@ -60,11 +69,15 @@ def test_scpi_func_gen_function(): "FUNC SQU" ], [ "SIN" - ] + ], + repeat=2 ) as fg: assert fg.function == fg.Function.sinusoid fg.function = fg.Function.square + assert fg.channel[0].function == fg.Function.sinusoid + fg.channel[0].function = fg.Function.square + def test_scpi_func_gen_offset(): with expected_protocol( @@ -74,7 +87,11 @@ def test_scpi_func_gen_offset(): "VOLT:OFFS 4.321000e-01" ], [ "+1.234000E+01", - ] + ], + repeat=2 ) as fg: assert fg.offset == 12.34 * pq.V fg.offset = 0.4321 * pq.V + + assert fg.channel[0].offset == 12.34 * pq.V + fg.channel[0].offset = 0.4321 * pq.V