Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/changes/newsfragments/4611.improved
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
:class:`~qcodes.instrument.VirtualInstrument` has been added for virtual drivers
that do not communicate with hardware. Its ``get_idn`` returns a default IDN
dict without warning. :meth:`~qcodes.instrument.Instrument.get_idn` remains
unchanged and still warns when communication via ``ask_raw`` is unavailable.
1 change: 1 addition & 0 deletions src/qcodes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
Instrument,
InstrumentChannel,
IPInstrument,
VirtualInstrument,
VisaInstrument,
find_or_create_instrument,
)
Expand Down
3 changes: 2 additions & 1 deletion src/qcodes/instrument/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
)

from .channel import ChannelList, ChannelTuple, InstrumentChannel, InstrumentModule
from .instrument import Instrument, find_or_create_instrument
from .instrument import Instrument, VirtualInstrument, find_or_create_instrument
from .instrument_base import InstrumentBase, InstrumentBaseKWArgs
from .ip import IPInstrument
from .visa import VisaInstrument, VisaInstrumentKWArgs
Expand All @@ -30,6 +30,7 @@
"InstrumentBaseKWArgs",
"InstrumentChannel",
"InstrumentModule",
"VirtualInstrument",
"VisaInstrument",
"VisaInstrumentKWArgs",
"find_or_create_instrument",
Expand Down
9 changes: 9 additions & 0 deletions src/qcodes/instrument/instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,15 @@ def ask_raw(self, cmd: str) -> str:
)


class VirtualInstrument(Instrument):
"""
Base class for virtual instruments that do not communicate with hardware.
"""

def get_idn(self) -> dict[str, str | None]:
return {"vendor": None, "model": self.name, "serial": None, "firmware": None}


def find_or_create_instrument(
instrument_class: type[T],
name: str,
Expand Down
69 changes: 69 additions & 0 deletions tests/test_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
Instrument,
InstrumentBase,
InstrumentModule,
VirtualInstrument,
find_or_create_instrument,
)
from qcodes.instrument_drivers.mock_instruments import (
Expand Down Expand Up @@ -256,6 +257,74 @@ def test_get_idn(testdummy: DummyInstrument) -> None:
assert testdummy.get_idn() == idn


def test_get_idn_on_virtual_instrument_does_not_warn(
caplog: pytest.LogCaptureFixture,
) -> None:
"""``VirtualInstrument.get_idn`` should return a default IDN without warning."""

virtual = VirtualInstrument(name="virtual_no_ask")
try:
caplog.clear()
with caplog.at_level("WARNING"):
idn = virtual.get_idn()
assert idn == {
"vendor": None,
"model": "virtual_no_ask",
"serial": None,
"firmware": None,
}
assert "Error getting or interpreting *IDN?" not in caplog.text
finally:
virtual.close()


def test_get_idn_on_base_instrument_warns(
caplog: pytest.LogCaptureFixture,
) -> None:
"""The plain ``Instrument`` base class should still warn without ``ask_raw``."""

instrument = Instrument(name="instrument_no_ask")
try:
caplog.clear()
with caplog.at_level("WARNING"):
idn = instrument.get_idn()
assert idn == {
"vendor": None,
"model": "instrument_no_ask",
"serial": None,
"firmware": None,
}
assert "Error getting or interpreting *IDN?" in caplog.text
finally:
instrument.close()


def test_get_idn_still_warns_on_other_errors(
caplog: pytest.LogCaptureFixture,
) -> None:
"""Unexpected errors in ``*IDN?`` handling should still be surfaced via a
warning so real misbehaviour is not silenced by the virtual-instrument
short-circuit."""

class BrokenInstrument(Instrument):
def ask_raw(self, cmd: str) -> str:
raise RuntimeError("communication failure")

broken = BrokenInstrument(name="broken_ask")
try:
with caplog.at_level("WARNING"):
idn = broken.get_idn()
assert idn == {
"vendor": None,
"model": "broken_ask",
"serial": None,
"firmware": None,
}
assert "Error getting or interpreting *IDN?" in caplog.text
finally:
broken.close()


def test_repr(testdummy: DummyInstrument) -> None:
assert repr(testdummy) == "<DummyInstrument: testdummy>"

Expand Down