diff --git a/src/instruments/abstract_instruments/optical_spectrum_analyzer.py b/src/instruments/abstract_instruments/optical_spectrum_analyzer.py index 7a432d1b..b8b30cd4 100644 --- a/src/instruments/abstract_instruments/optical_spectrum_analyzer.py +++ b/src/instruments/abstract_instruments/optical_spectrum_analyzer.py @@ -5,10 +5,10 @@ # IMPORTS ##################################################################### - import abc from instruments.abstract_instruments import Instrument +from instruments.util_fns import ProxyList # CLASSES ##################################################################### @@ -21,14 +21,29 @@ class OpticalSpectrumAnalyzer(Instrument, metaclass=abc.ABCMeta): provide a consistent interface to the user. """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._channel_count = 1 + class Channel(metaclass=abc.ABCMeta): """ Abstract base class for physical channels on an optical spectrum analyzer. All applicable concrete instruments should inherit from this ABC to provide a consistent interface to the user. + + Optical spectrum analyzers that only have a single channel do not need to + define their own concrete implementation of this class. Ones with + multiple channels need their own definition of this class, where + this class contains the concrete implementations of the below + abstract methods. Instruments with 1 channel have their concrete + implementations at the parent instrument level. """ + def __init__(self, parent, name): + self._parent = parent + self._name = name + # METHODS # @abc.abstractmethod @@ -42,7 +57,10 @@ def wavelength(self, bin_format=True): :return: The wavelength component of the waveform. :rtype: `numpy.ndarray` """ - raise NotImplementedError + if self._parent._channel_count == 1: + return self._parent.wavelength(bin_format=bin_format) + else: + raise NotImplementedError @abc.abstractmethod def data(self, bin_format=True): @@ -55,22 +73,23 @@ def data(self, bin_format=True): :return: The y-component of the waveform. :rtype: `numpy.ndarray` """ - raise NotImplementedError + if self._parent._channel_count == 1: + return self._parent.data(bin_format=bin_format) + else: + raise NotImplementedError # PROPERTIES # @property - @abc.abstractmethod def channel(self): """ Gets an iterator or list for easy Pythonic access to the various channel objects on the OSA instrument. Typically generated by the `~instruments.util_fns.ProxyList` helper. """ - raise NotImplementedError + return ProxyList(self, self.Channel, range(self._channel_count)) @property - @abc.abstractmethod def start_wl(self): """ Gets/sets the the start wavelength of the OSA. This is @@ -81,12 +100,10 @@ def start_wl(self): raise NotImplementedError @start_wl.setter - @abc.abstractmethod def start_wl(self, newval): raise NotImplementedError @property - @abc.abstractmethod def stop_wl(self): """ Gets/sets the the stop wavelength of the OSA. This is @@ -97,12 +114,10 @@ def stop_wl(self): raise NotImplementedError @stop_wl.setter - @abc.abstractmethod def stop_wl(self, newval): raise NotImplementedError @property - @abc.abstractmethod def bandwidth(self): """ Gets/sets the the bandwidth of the OSA. This is @@ -113,7 +128,6 @@ def bandwidth(self): raise NotImplementedError @bandwidth.setter - @abc.abstractmethod def bandwidth(self, newval): raise NotImplementedError @@ -125,3 +139,33 @@ def start_sweep(self): Forces a start sweep on the attached OSA. """ raise NotImplementedError + + def wavelength(self, bin_format=True): + """ + Gets the x-axis of the specified data source channel. This is an + abstract property. + + :param bool bin_format: If the waveform should be transfered in binary + (``True``) or ASCII (``False``) formats. + :return: The wavelength component of the waveform. + :rtype: `numpy.ndarray` + """ + if self._channel_count > 1: + return self.channel[0].wavelength(bin_format=bin_format) + else: + raise NotImplementedError + + def data(self, bin_format=True): + """ + Gets the y-axis of the specified data source channel. This is an + abstract property. + + :param bool bin_format: If the waveform should be transfered in binary + (``True``) or ASCII (``False``) formats. + :return: The y-component of the waveform. + :rtype: `numpy.ndarray` + """ + if self._channel_count > 1: + return self.channel[0].data(bin_format=bin_format) + else: + raise NotImplementedError diff --git a/src/instruments/yokogawa/yokogawa6370.py b/src/instruments/yokogawa/yokogawa6370.py index eb4255d7..c7c5b16e 100644 --- a/src/instruments/yokogawa/yokogawa6370.py +++ b/src/instruments/yokogawa/yokogawa6370.py @@ -7,7 +7,6 @@ from enum import IntEnum, Enum -import hashlib from instruments.units import ureg as u @@ -46,6 +45,7 @@ class Yokogawa6370(OpticalSpectrumAnalyzer): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self._channel_count = len(self.Traces) if isinstance(self._file, SocketCommunicator): self.terminator = "\r\n" # TCP IP connection terminator diff --git a/tests/test_abstract_inst/test_optical_spectrum_analyzer.py b/tests/test_abstract_inst/test_optical_spectrum_analyzer.py index 6efb1c97..9050e097 100644 --- a/tests/test_abstract_inst/test_optical_spectrum_analyzer.py +++ b/tests/test_abstract_inst/test_optical_spectrum_analyzer.py @@ -19,15 +19,9 @@ def osa(monkeypatch): """Patch and return Optical Spectrum Analyzer class for access.""" inst = ik.abstract_instruments.OpticalSpectrumAnalyzer + chan = ik.abstract_instruments.OpticalSpectrumAnalyzer.Channel monkeypatch.setattr(inst, "__abstractmethods__", set()) - return inst - - -@pytest.fixture -def osc(monkeypatch): - """Patch and return OSAChannel class for access.""" - inst = ik.abstract_instruments.OpticalSpectrumAnalyzer.Channel - monkeypatch.setattr(inst, "__abstractmethods__", set()) + monkeypatch.setattr(chan, "__abstractmethods__", set()) return inst @@ -37,8 +31,8 @@ def osc(monkeypatch): def test_osa_channel(osa): """Get channel: ensure existence.""" with expected_protocol(osa, [], []) as inst: - with pytest.raises(NotImplementedError): - _ = inst.channel + ch = inst.channel[0] + assert isinstance(ch, ik.abstract_instruments.OpticalSpectrumAnalyzer.Channel) def test_osa_start_wl(osa): @@ -78,15 +72,25 @@ def test_osa_start_sweep(osa): # OSAChannel # -def test_osa_channel_wavelength(osc): +@pytest.mark.parametrize("num_ch", [1, 5]) +def test_osa_channel_wavelength(osa, num_ch): """Channel wavelength method: ensure existence.""" - inst = osc() - with pytest.raises(NotImplementedError): - inst.wavelength() + with expected_protocol(osa, [], []) as inst: + inst._channel_count = num_ch + ch = inst.channel[0] + with pytest.raises(NotImplementedError): + ch.wavelength() + with pytest.raises(NotImplementedError): + inst.wavelength() # single channel instrument -def test_osa_channel_data(osc): +@pytest.mark.parametrize("num_ch", [1, 5]) +def test_osa_channel_data(osa, num_ch): """Channel data method: ensure existence.""" - inst = osc() - with pytest.raises(NotImplementedError): - inst.data() + with expected_protocol(osa, [], []) as inst: + inst._channel_count = num_ch + ch = inst.channel[0] + with pytest.raises(NotImplementedError): + ch.data() + with pytest.raises(NotImplementedError): + inst.data() # single channel instrument diff --git a/tests/test_yokogawa/test_yokogawa_6370.py b/tests/test_yokogawa/test_yokogawa_6370.py index 5137eb52..fabf33a2 100644 --- a/tests/test_yokogawa/test_yokogawa_6370.py +++ b/tests/test_yokogawa/test_yokogawa_6370.py @@ -5,7 +5,6 @@ # IMPORTS ##################################################################### -import hashlib import struct from hypothesis import ( @@ -15,7 +14,6 @@ import socket import instruments as ik -from instruments.abstract_instruments.comm import SocketCommunicator from instruments.optional_dep_finder import numpy from tests import ( expected_protocol, @@ -31,6 +29,7 @@ def test_channel_is_channel_class(): inst = ik.yokogawa.Yokogawa6370.open_test() + assert inst._channel_count == len(inst.Traces) assert isinstance(inst.channel["A"], inst.Channel) is True