diff --git a/docs/changes/newsfragments/8602.improved_driver b/docs/changes/newsfragments/8602.improved_driver new file mode 100644 index 00000000000..8b5df046354 --- /dev/null +++ b/docs/changes/newsfragments/8602.improved_driver @@ -0,0 +1,4 @@ +Refactored the ``CopperMountainM5xxx`` base driver to work more generally +with all S2VNAs (now called ``CopperMountainS2VnaBase``) and added a driver for V0602. +The V0602 VISA simulator supports reset, averaging count, and electrical-distance +units for driver testing. diff --git a/src/qcodes/instrument/sims/CopperMountain_V0602.yaml b/src/qcodes/instrument/sims/CopperMountain_V0602.yaml new file mode 100644 index 00000000000..0da98981a8d --- /dev/null +++ b/src/qcodes/instrument/sims/CopperMountain_V0602.yaml @@ -0,0 +1,141 @@ +spec: "1.0" + +resources: + TCPIP0::localhost::hislip0::INSTR: + device: V0602 + +devices: + V0602: + eom: + TCPIP INSTR: + q: "\n" + r: "\n" + error: ERROR + dialogues: + - q: "*IDN?" + r: "CMT, V0602, 23047001, 25.3.1/2" + - q: "*RST" + properties: + output: + default: 0 + getter: + q: "OUTP:STAT?" + r: "{}" + setter: + q: "OUTP:STAT {}" + specs: + type: int + valid: [0, 1] + power: + default: -20 + getter: + q: "SOUR:POW?" + r: "{}" + setter: + q: "SOUR:POW {}" + specs: + min: -50 + max: 5 + type: float + if_bandwidth: + default: 100000 + getter: + q: "SENS1:BWID?" + r: "{}" + setter: + q: "SENS1:BWID {}" + averages_enabled: + default: 0 + getter: + q: "SENS1:AVER:STAT?" + r: "{}" + setter: + q: "SENS1:AVER:STAT {}" + specs: + type: int + valid: [0, 1] + averages_count: + default: 1 + getter: + q: "SENS1:AVER:COUN?" + r: "{}" + setter: + q: "SENS1:AVER:COUN {}" + specs: + type: int + min: 1 + averages_trigger_enabled: + default: 0 + getter: + q: "TRIG:SEQ:AVER?" + r: "{}" + setter: + q: "TRIG:SEQ:AVER {}" + specs: + type: int + valid: [0, 1] + electrical_delay: + default: 0 + getter: + q: "CALC1:CORR:EDEL:TIME?" + r: "{}" + setter: + q: "CALC1:CORR:EDEL:TIME {}" + electrical_distance: + default: 20 + getter: + q: "CALC1:CORR:EDEL:DIST?" + r: "{}" + setter: + q: "CALC1:CORR:EDEL:DIST {}" + electrical_distance_unit: + default: MET + getter: + q: "CALC1:CORR:EDEL:DIST:UNIT?" + r: "{}" + setter: + q: "CALC1:CORR:EDEL:DIST:UNIT {}" + clock_source: + default: "INT" + getter: + q: "SENSe1:ROSCillator:SOURce?" + r: "{}" + setter: + q: "SENSe1:ROSCillator:SOURce {}" + start: + default: 300000 + getter: + q: "SENS1:FREQ:STAR?" + r: "{}" + setter: + q: "SENS1:FREQ:STAR {}" + specs: + type: float + stop: + default: 6500000000 + getter: + q: "SENS1:FREQ:STOP?" + r: "{}" + setter: + q: "SENS1:FREQ:STOP {}" + specs: + type: float + center: + getter: + q: "SENS1:FREQ:CENT?" + r: "{}" + setter: + q: "SENS1:FREQ:CENT {}" + span: + getter: + q: "SENS1:FREQ:SPAN?" + r: "{}" + setter: + q: "SENS1:FREQ:SPAN {}" + number_of_points: + default: 201 + getter: + q: "SENS1:SWE:POIN?" + r: "{}" + setter: + q: "SENS1:SWE:POIN {}" diff --git a/src/qcodes/instrument_drivers/CopperMountain/_M5065.py b/src/qcodes/instrument_drivers/CopperMountain/_M5065.py index 5b3ea77d828..64024df0428 100644 --- a/src/qcodes/instrument_drivers/CopperMountain/_M5065.py +++ b/src/qcodes/instrument_drivers/CopperMountain/_M5065.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING -from ._M5xxx import CopperMountainM5xxx +from ._s2vna_base import CopperMountainS2VnaBase if TYPE_CHECKING: from typing import Unpack @@ -8,7 +8,7 @@ from qcodes.instrument import VisaInstrumentKWArgs -class CopperMountainM5065(CopperMountainM5xxx): +class CopperMountainM5065(CopperMountainS2VnaBase): """This is the QCoDeS driver for the M5065 VNA from Copper Mountain Technologies""" def __init__( @@ -17,4 +17,13 @@ def __init__( address: str, **kwargs: "Unpack[VisaInstrumentKWArgs]", ): - super().__init__(name, address, min_freq=300e3, max_freq=6.5e9, **kwargs) + super().__init__( + name, + address, + min_freq=300e3, + max_freq=6.5e9, + min_power=-55.0, + max_power=5.0, + max_points_per_sweep=200_001, + **kwargs, + ) diff --git a/src/qcodes/instrument_drivers/CopperMountain/_M5180.py b/src/qcodes/instrument_drivers/CopperMountain/_M5180.py index bbba039b0c6..068eacb9352 100644 --- a/src/qcodes/instrument_drivers/CopperMountain/_M5180.py +++ b/src/qcodes/instrument_drivers/CopperMountain/_M5180.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING -from ._M5xxx import CopperMountainM5xxx +from ._s2vna_base import CopperMountainS2VnaBase if TYPE_CHECKING: from typing import Unpack @@ -8,7 +8,7 @@ from qcodes.instrument import VisaInstrumentKWArgs -class CopperMountainM5180(CopperMountainM5xxx): +class CopperMountainM5180(CopperMountainS2VnaBase): """This is the QCoDeS driver for the M5180 VNA from Copper Mountain Technologies""" def __init__( @@ -17,4 +17,13 @@ def __init__( address: str, **kwargs: "Unpack[VisaInstrumentKWArgs]", ): - super().__init__(name, address, min_freq=300e3, max_freq=18e9, **kwargs) + super().__init__( + name, + address, + min_freq=300e3, + max_freq=18e9, + min_power=-40, + max_power=10, + max_points_per_sweep=200_001, + **kwargs, + ) diff --git a/src/qcodes/instrument_drivers/CopperMountain/__init__.py b/src/qcodes/instrument_drivers/CopperMountain/__init__.py index 7d496f4bef2..4510fc51b1a 100644 --- a/src/qcodes/instrument_drivers/CopperMountain/__init__.py +++ b/src/qcodes/instrument_drivers/CopperMountain/__init__.py @@ -1,5 +1,11 @@ -from ._M5xxx import CopperMountainM5xxx from ._M5065 import CopperMountainM5065 from ._M5180 import CopperMountainM5180 +from ._s2vna_base import CopperMountainS2VnaBase +from ._v0602 import CopperMountainV0602 -__all__ = ["CopperMountainM5xxx", "CopperMountainM5065", "CopperMountainM5180"] +__all__ = [ + "CopperMountainM5065", + "CopperMountainM5180", + "CopperMountainS2VnaBase", + "CopperMountainV0602", +] diff --git a/src/qcodes/instrument_drivers/CopperMountain/_M5xxx.py b/src/qcodes/instrument_drivers/CopperMountain/_s2vna_base.py similarity index 95% rename from src/qcodes/instrument_drivers/CopperMountain/_M5xxx.py rename to src/qcodes/instrument_drivers/CopperMountain/_s2vna_base.py index 90631228608..6fcabf7ff9c 100644 --- a/src/qcodes/instrument_drivers/CopperMountain/_M5xxx.py +++ b/src/qcodes/instrument_drivers/CopperMountain/_s2vna_base.py @@ -17,9 +17,9 @@ from typing import Unpack -class CopperMountainM5xxx(VisaInstrument): +class CopperMountainS2VnaBase(VisaInstrument): """ - Base class for QCoDeS drivers for Copper Mountain M-series VNAs. + Base class for QCoDeS drivers for Copper Mountain S2VNAs. Not to be instantiated directly. Use model specific subclass. https://coppermountaintech.com/help-s2/index.html @@ -37,10 +37,16 @@ def __init__( address: str, min_freq: float, max_freq: float, + min_power: float, + max_power: float, + max_points_per_sweep: int, **kwargs: "Unpack[VisaInstrumentKWArgs]", ): """ - QCoDeS driver for Copper Mountain M-series VNA (M5xxx). + QCoDeS Base driver for Copper Mountain S2VNAs. This should not be + instantiated directly. Please see https://coppermountaintech.com/help-s2/index.html + for list of compatible instruments. + This driver supports only a single channel. Args: @@ -48,15 +54,20 @@ def __init__( address: VISA address of the instrument. min_freq: Minimum frequency supported by the instrument (in Hz). max_freq: Maximum frequency supported by the instrument (in Hz). + min_power: Minimum output power (in dBm). + max_power: Maximum output power (in dBm). + max_points_per_sweep: Maximum number of measurement points per sweep. **kwargs: Additional keyword arguments for VisaInstrument. """ super().__init__(name=name, address=address, **kwargs) - self.min_freq = min_freq - self.max_freq = max_freq - self.min_points = 2 - self.max_points = 200001 + self._min_freq = min_freq + self._max_freq = max_freq + self._min_power = min_power + self._max_power = max_power + self._min_points_per_sweep = 2 + self._max_points_per_sweep = max_points_per_sweep self.output: Parameter = self.add_parameter( name="output", @@ -76,7 +87,7 @@ def __init__( set_cmd="SOUR:POW {}", unit="dBm", docstring="Sets or reads out the power level for the frequency sweep type in dBm.", - vals=Numbers(min_value=-55, max_value=5), + vals=Numbers(min_value=self._min_power, max_value=self._max_power), ) """Sets or reads out the power level for the frequency sweep type in dBm.""" @@ -203,7 +214,7 @@ def __init__( unit="Hz", docstring="Sets or reads out the stimulus start value " "of the sweep range for linear or logarithmic sweep type.", - vals=Numbers(min_value=self.min_freq, max_value=self.max_freq - 1), + vals=Numbers(min_value=self._min_freq, max_value=self._max_freq - 1), ) """Sets or reads out the stimulus start value of the sweep range for linear or logarithmic sweep type.""" @@ -217,7 +228,7 @@ def __init__( unit="Hz", docstring="Sets or reads out the stimulus stop value of the " "sweep range for linear or logarithmic sweep type.", - vals=Numbers(min_value=self.min_freq + 1, max_value=self.max_freq), + vals=Numbers(min_value=self._min_freq + 1, max_value=self._max_freq), ) """Sets or reads out the stimulus stop value of the sweep range for linear or logarithmic sweep type.""" @@ -231,7 +242,7 @@ def __init__( unit="Hz", docstring="Sets or reads out the stimulus center value of " "the sweep range for linear or logarithmic sweep type.", - vals=Numbers(min_value=self.min_freq + 1, max_value=self.max_freq - 1), + vals=Numbers(min_value=self._min_freq + 1, max_value=self._max_freq - 1), ) """Sets or reads out the stimulus center value of the sweep range for linear or logarithmic sweep type.""" @@ -245,7 +256,7 @@ def __init__( unit="Hz", docstring="Sets or reads out the stimulus span value of the " "sweep range for linear or logarithmic sweep type.", - vals=Numbers(min_value=1, max_value=self.max_freq - 1), + vals=Numbers(min_value=1, max_value=self._max_freq - 1), ) """Sets or reads out the stimulus span value of the sweep range for linear or logarithmic sweep type.""" @@ -258,7 +269,10 @@ def __init__( get_cmd="SENS1:SWE:POIN?", set_cmd=self._set_number_of_points, docstring="Sets or reads out the number of measurement points.", - vals=Ints(min_value=self.min_points, max_value=self.max_points), + vals=Ints( + min_value=self._min_points_per_sweep, + max_value=self._max_points_per_sweep, + ), ) """Sets or reads out the number of measurement points.""" @@ -625,7 +639,7 @@ def _db(data: "NDArray") -> "NDArray": class FrequencySweepMagPhase( - MultiParameter[tuple[NDArray, NDArray], CopperMountainM5xxx] + MultiParameter[tuple[NDArray, NDArray], CopperMountainS2VnaBase] ): """ Sweep that returns magnitude and phase. @@ -637,7 +651,7 @@ def __init__( start: float, stop: float, number_of_points: int, - instrument: CopperMountainM5xxx, + instrument: CopperMountainS2VnaBase, expected_measurement_duration: float = 600, **kwargs: Any, ) -> None: @@ -735,7 +749,7 @@ def get_raw(self) -> tuple[NDArray, NDArray]: class PointMagPhase( - MultiParameter[tuple[np.floating, np.floating], CopperMountainM5xxx] + MultiParameter[tuple[np.floating, np.floating], CopperMountainS2VnaBase] ): """ Returns the average Sxx of a frequency sweep. @@ -746,7 +760,7 @@ class PointMagPhase( def __init__( self, name: str, - instrument: CopperMountainM5xxx, + instrument: CopperMountainS2VnaBase, expected_measurement_duration: float = 600, **kwargs: Any, ) -> None: @@ -834,7 +848,7 @@ def get_raw(self) -> tuple[ParamRawDataType, ParamRawDataType]: return 20 * np.log10(abs(sxx_mean)), (np.angle(sxx_mean)) -class PointIQ(MultiParameter[tuple[np.floating, np.floating], CopperMountainM5xxx]): +class PointIQ(MultiParameter[tuple[np.floating, np.floating], CopperMountainS2VnaBase]): """ Returns the average Sxx of a frequency sweep, in terms of I and Q. Work around for a CW mode where only one point is read. @@ -844,7 +858,7 @@ class PointIQ(MultiParameter[tuple[np.floating, np.floating], CopperMountainM5xx def __init__( self, name: str, - instrument: CopperMountainM5xxx, + instrument: CopperMountainS2VnaBase, expected_measurement_duration: float = 600, **kwargs: Any, ) -> None: diff --git a/src/qcodes/instrument_drivers/CopperMountain/_v0602.py b/src/qcodes/instrument_drivers/CopperMountain/_v0602.py new file mode 100644 index 00000000000..6fdd1d12e3e --- /dev/null +++ b/src/qcodes/instrument_drivers/CopperMountain/_v0602.py @@ -0,0 +1,29 @@ +from typing import TYPE_CHECKING + +from ._s2vna_base import CopperMountainS2VnaBase + +if TYPE_CHECKING: + from typing import Unpack + + from qcodes.instrument import VisaInstrumentKWArgs + + +class CopperMountainV0602(CopperMountainS2VnaBase): + """This is the QCoDeS driver for the V0602 VNA from Copper Mountain Technologies""" + + def __init__( + self, + name: str, + address: str, + **kwargs: "Unpack[VisaInstrumentKWArgs]", + ): + super().__init__( + name, + address, + min_freq=100e3, + max_freq=6.5e9, + min_power=-50.0, + max_power=5.0, + max_points_per_sweep=500_001, + **kwargs, + ) diff --git a/tests/drivers/test_coppermountain_m5065.py b/tests/drivers/test_coppermountain_m5065.py index a091001fc79..e3fef838333 100644 --- a/tests/drivers/test_coppermountain_m5065.py +++ b/tests/drivers/test_coppermountain_m5065.py @@ -35,8 +35,17 @@ def test_idn_command(vna): def test_min_and_max_frequency_defaults(vna): - assert vna.min_freq == 300e3 - assert vna.max_freq == 6.5e9 + assert vna.start.vals.min_value == 300e3 + assert vna.stop.vals.max_value == 6.5e9 + + +def test_min_and_max_power_defaults(vna): + assert vna.power.vals.min_value == -55.0 + assert vna.power.vals.max_value == 5.0 + + +def test_number_of_points_max_default(vna): + assert vna.number_of_points.vals.max_value == 200_001 def test_output_param(vna): diff --git a/tests/drivers/test_coppermountain_m5180.py b/tests/drivers/test_coppermountain_m5180.py index 84a55af3107..d51761e8590 100644 --- a/tests/drivers/test_coppermountain_m5180.py +++ b/tests/drivers/test_coppermountain_m5180.py @@ -35,5 +35,14 @@ def test_idn_command(vna): def test_min_and_max_frequency_defaults(vna): - assert vna.min_freq == 300e3 - assert vna.max_freq == 18e9 + assert vna.start.vals.min_value == 300e3 + assert vna.stop.vals.max_value == 18e9 + + +def test_min_and_max_power_defaults(vna): + assert vna.power.vals.min_value == -40 + assert vna.power.vals.max_value == 10 + + +def test_number_of_points_max_default(vna): + assert vna.number_of_points.vals.max_value == 200_001 diff --git a/tests/drivers/test_coppermountain_v0602.py b/tests/drivers/test_coppermountain_v0602.py new file mode 100644 index 00000000000..3b6a856281d --- /dev/null +++ b/tests/drivers/test_coppermountain_v0602.py @@ -0,0 +1,113 @@ +import pytest + +from qcodes.instrument_drivers.CopperMountain import CopperMountainV0602 + + +class DummyM5065: + def __init__(self, *args, **kwargs): + pass + + def start(self): + return 1e6 + + +@pytest.fixture() +def vna(): + instance = CopperMountainV0602( + name="V0602", + address="TCPIP0::localhost::hislip0::INSTR", + pyvisa_sim_file="CopperMountain_V0602.yaml", + ) + instance.reset() + yield instance + instance.close() + + +def test_m5065_instantiation(vna): + assert vna.name == "V0602" + assert vna.address == "TCPIP0::localhost::hislip0::INSTR" + + +def test_idn_command(vna): + idn = vna.get_idn() + assert idn["vendor"] == "CMT" + assert idn["model"] == "V0602" + + +def test_reset_does_not_leave_pending_response(vna: CopperMountainV0602) -> None: + vna.reset() + assert vna.get_idn()["model"] == "V0602" + assert vna.output() is False + + +def test_min_and_max_frequency_defaults(vna): + assert vna.start.vals.min_value == 100e3 + assert vna.stop.vals.max_value == 6.5e9 + + +def test_min_and_max_power_defaults(vna): + assert vna.power.vals.min_value == -50.0 + assert vna.power.vals.max_value == 5.0 + + +def test_number_of_points_max_default(vna): + assert vna.number_of_points.vals.max_value == 500_001 + + +def test_output_param(vna): + vna.output(True) + assert vna.output() is True + vna.output(False) + assert vna.output() is False + + +def test_power_param(vna): + vna.power(-20) + assert vna.power() == -20 + + +def test_if_bandwidth_param(vna): + vna.if_bandwidth(1000) + assert vna.if_bandwidth() == 1000 + + +def test_averages_param(vna): + vna.averages(2) + assert vna.averages() == 2 + + +@pytest.mark.parametrize("units", ["MET", "FEET", "INCH"]) +def test_electrical_distance_units(vna: CopperMountainV0602, units: str) -> None: + vna.electrical_distance_units(units) + assert vna.electrical_distance_units() == units + + +def test_frequency_params(vna): + vna.start(1e6) + vna.stop(2e6) + vna.center(1.5e6) + vna.span(1e6) + assert vna.start() == 1e6 + assert vna.stop() == 2e6 + assert vna.center() == 1.5e6 + assert vna.span() == 1e6 + + +def test_setting_start_raises_error_when_larger_than_stop(vna): + vna.stop(1e6) + with pytest.raises(ValueError, match="Stop frequency"): + vna.start(1e7) + + +def test_number_of_points_param(vna): + vna.number_of_points(201) + assert vna.number_of_points() == 201 + + +def test_trigger_source_param(vna): + vna.trigger_source("bus") + assert vna.trigger_source() == "bus" + + +def test_update_lin_traces(vna): + vna.update_lin_traces()