diff --git a/doc/examples/kiprim/ex_kiprim_dc310s.py b/doc/examples/kiprim/ex_kiprim_dc310s.py new file mode 100644 index 00000000..30129444 --- /dev/null +++ b/doc/examples/kiprim/ex_kiprim_dc310s.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python + +import instruments as ik + +psu = ik.kiprim.DC310S.open_serial("COM8", baud=115200, timeout=0.5) + +print(psu.name) +print(f"Setpoint: {psu.voltage}, {psu.current}, output={psu.output}") +print(f"Measured: {psu.voltage_sense}, {psu.current_sense}, {psu.power_sense}") + +# Uncomment to configure the supply explicitly. +# psu.voltage = 5 * ik.units.volt +# psu.current = 0.25 * ik.units.ampere +# psu.output = True diff --git a/doc/source/apiref/index.rst b/doc/source/apiref/index.rst index 4ad370ae..271aaa24 100644 --- a/doc/source/apiref/index.rst +++ b/doc/source/apiref/index.rst @@ -24,6 +24,7 @@ Contents: holzworth hp keithley + kiprim lakeshore minghe mettler_toledo diff --git a/doc/source/apiref/kiprim.rst b/doc/source/apiref/kiprim.rst new file mode 100644 index 00000000..9429cf6c --- /dev/null +++ b/doc/source/apiref/kiprim.rst @@ -0,0 +1,12 @@ +.. currentmodule:: instruments.kiprim + +====== +Kiprim +====== + +:class:`DC310S` Power Supply +============================ + +.. autoclass:: DC310S + :members: + :undoc-members: diff --git a/src/instruments/__init__.py b/src/instruments/__init__.py index 660326bf..2b54bfd1 100644 --- a/src/instruments/__init__.py +++ b/src/instruments/__init__.py @@ -24,6 +24,7 @@ from . import holzworth from . import hp from . import keithley +from . import kiprim from . import lakeshore from . import mettler_toledo from . import minghe diff --git a/src/instruments/kiprim/__init__.py b/src/instruments/kiprim/__init__.py new file mode 100644 index 00000000..baf4c316 --- /dev/null +++ b/src/instruments/kiprim/__init__.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python +""" +Module containing Kiprim instruments. +""" + +from .dc310s import DC310S diff --git a/src/instruments/kiprim/dc310s.py b/src/instruments/kiprim/dc310s.py new file mode 100644 index 00000000..ced6afda --- /dev/null +++ b/src/instruments/kiprim/dc310s.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python +""" +Driver for the Kiprim DC310S single-output power supply. +""" + +# IMPORTS ##################################################################### + +from instruments.abstract_instruments import PowerSupply +from instruments.units import ureg as u +from instruments.util_fns import bounded_unitful_property, unitful_property + +# FUNCTIONS ################################################################### + + +def _parse_output_state(reply): + """ + Normalize the DC310S output-state reply into a boolean value. + + The DC310S has been observed to report either ``ON``/``OFF`` or ``1``/``0`` + depending on firmware and transport state. + """ + + reply = reply.strip().upper() + if reply in {"1", "ON"}: + return True + if reply in {"0", "OFF"}: + return False + raise ValueError(f"Unexpected output-state reply: {reply}") + + +# CLASSES ##################################################################### + + +class DC310S(PowerSupply, PowerSupply.Channel): + """ + The Kiprim DC310S is a single-output programmable DC power supply. + + Because the supply has one programmable output, this object inherits from + both `~instruments.abstract_instruments.power_supply.PowerSupply` and + `~instruments.abstract_instruments.power_supply.PowerSupply.Channel`. + + Example usage: + + >>> import instruments as ik + >>> psu = ik.kiprim.DC310S.open_serial("COM8", baud=115200, timeout=0.5) + >>> psu.voltage = 5 * ik.units.volt + >>> psu.current = 0.25 * ik.units.ampere + >>> psu.output = True + >>> psu.voltage_sense + + """ + + voltage, voltage_min, voltage_max = bounded_unitful_property( + "VOLT", + u.volt, + format_code="{:.3f}", + input_decoration=str.strip, + valid_range=(0 * u.volt, 30 * u.volt), + doc=""" + Gets/sets the programmed output voltage. + + The DC310S product documentation specifies a programmable output range + of 0 V to 30 V. + + :units: As specified, or assumed to be :math:`\\text{V}` otherwise. + :type: `float` or `~pint.Quantity` + """, + ) + + current, current_min, current_max = bounded_unitful_property( + "CURR", + u.amp, + format_code="{:.3f}", + input_decoration=str.strip, + valid_range=(0 * u.amp, 10 * u.amp), + doc=""" + Gets/sets the programmed output current limit. + + The DC310S product documentation specifies a programmable output range + of 0 A to 10 A. + + :units: As specified, or assumed to be :math:`\\text{A}` otherwise. + :type: `float` or `~pint.Quantity` + """, + ) + + voltage_sense = unitful_property( + "MEAS:VOLT", + u.volt, + readonly=True, + input_decoration=str.strip, + doc=""" + Gets the measured output voltage. + + :units: :math:`\\text{V}` + :rtype: `~pint.Quantity` + """, + ) + + current_sense = unitful_property( + "MEAS:CURR", + u.amp, + readonly=True, + input_decoration=str.strip, + doc=""" + Gets the measured output current. + + :units: :math:`\\text{A}` + :rtype: `~pint.Quantity` + """, + ) + + power_sense = unitful_property( + "MEAS:POW", + u.watt, + readonly=True, + input_decoration=str.strip, + doc=""" + Gets the measured output power. + + :units: :math:`\\text{W}` + :rtype: `~pint.Quantity` + """, + ) + + @property + def output(self): + """ + Gets/sets the output state. + + :type: `bool` + """ + + return _parse_output_state(self.query("OUTP?")) + + @output.setter + def output(self, newval): + if not isinstance(newval, bool): + raise TypeError("Output state must be specified with a boolean value.") + self.sendcmd(f"OUTP {'ON' if newval else 'OFF'}") + + @property + def name(self): + """ + Gets the instrument name as reported by ``*IDN?``. + + :rtype: `str` + """ + + idn_string = self.query("*IDN?") + idn_parts = [part.strip() for part in idn_string.split(",")] + if len(idn_parts) >= 2: + return " ".join(idn_parts[:2]) + return idn_string.strip() + + @property + def mode(self): + """ + Unimplemented. + """ + + raise NotImplementedError("The DC310S does not expose a stable mode query.") + + @mode.setter + def mode(self, newval): + """ + Unimplemented. + """ + + raise NotImplementedError("The DC310S does not expose a stable mode query.") + + @property + def channel(self): + """ + Return the single output channel. + + :rtype: `tuple` + """ + + return (self,) diff --git a/tests/test_kiprim/__init__.py b/tests/test_kiprim/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_kiprim/test_dc310s.py b/tests/test_kiprim/test_dc310s.py new file mode 100644 index 00000000..98d3617e --- /dev/null +++ b/tests/test_kiprim/test_dc310s.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python +""" +Unit tests for the Kiprim DC310S single-output power supply. +""" + +# IMPORTS ##################################################################### + +import pytest + +import instruments as ik +from instruments.units import ureg as u +from tests import expected_protocol, unit_eq + +# TESTS ####################################################################### + + +def test_channel(): + with expected_protocol(ik.kiprim.DC310S, [], [], sep="\n") as psu: + assert psu.channel[0] == psu + assert len(psu.channel) == 1 + + +def test_name(): + with expected_protocol( + ik.kiprim.DC310S, ["*IDN?"], ["KIPRIM,DC310S,22371243,FV:V3.7.0"], sep="\n" + ) as psu: + assert psu.name == "KIPRIM DC310S" + + +def test_name_single_field_reply(): + with expected_protocol(ik.kiprim.DC310S, ["*IDN?"], ["DC310S"], sep="\n") as psu: + assert psu.name == "DC310S" + + +def test_voltage(): + with expected_protocol( + ik.kiprim.DC310S, ["VOLT 5.000", "VOLT?"], ["5.000"], sep="\n" + ) as psu: + psu.voltage = 5 * u.volt + unit_eq(psu.voltage, 5 * u.volt) + + +def test_voltage_bounds(): + with expected_protocol(ik.kiprim.DC310S, [], [], sep="\n") as psu: + unit_eq(psu.voltage_min, 0 * u.volt) + unit_eq(psu.voltage_max, 30 * u.volt) + with pytest.raises(ValueError): + psu.voltage = 30.001 * u.volt + with pytest.raises(ValueError): + psu.voltage = -0.001 * u.volt + + +def test_current(): + with expected_protocol( + ik.kiprim.DC310S, ["CURR 0.250", "CURR?"], ["0.250"], sep="\n" + ) as psu: + psu.current = 0.25 * u.amp + unit_eq(psu.current, 0.25 * u.amp) + + +def test_current_bounds(): + with expected_protocol(ik.kiprim.DC310S, [], [], sep="\n") as psu: + unit_eq(psu.current_min, 0 * u.amp) + unit_eq(psu.current_max, 10 * u.amp) + with pytest.raises(ValueError): + psu.current = 10.001 * u.amp + with pytest.raises(ValueError): + psu.current = -0.001 * u.amp + + +def test_voltage_sense(): + with expected_protocol( + ik.kiprim.DC310S, ["MEAS:VOLT?"], ["12.340"], sep="\n" + ) as psu: + unit_eq(psu.voltage_sense, 12.34 * u.volt) + + +def test_current_sense(): + with expected_protocol( + ik.kiprim.DC310S, ["MEAS:CURR?"], ["0.456"], sep="\n" + ) as psu: + unit_eq(psu.current_sense, 0.456 * u.amp) + + +def test_power_sense(): + with expected_protocol(ik.kiprim.DC310S, ["MEAS:POW?"], ["5.624"], sep="\n") as psu: + unit_eq(psu.power_sense, 5.624 * u.watt) + + +def test_output_on(): + with expected_protocol( + ik.kiprim.DC310S, ["OUTP ON", "OUTP?"], ["ON"], sep="\n" + ) as psu: + psu.output = True + assert psu.output + + +def test_output_off_numeric_reply(): + with expected_protocol( + ik.kiprim.DC310S, ["OUTP OFF", "OUTP?"], ["0"], sep="\n" + ) as psu: + psu.output = False + assert psu.output is False + + +def test_output_invalid_reply(): + with expected_protocol(ik.kiprim.DC310S, ["OUTP?"], ["MAYBE"], sep="\n") as psu: + with pytest.raises(ValueError): + _ = psu.output + + +def test_output_setter_requires_bool(): + with expected_protocol(ik.kiprim.DC310S, [], [], sep="\n") as psu: + with pytest.raises(TypeError): + psu.output = 1 + + +def test_mode_getter_unimplemented(): + with expected_protocol(ik.kiprim.DC310S, [], [], sep="\n") as psu: + with pytest.raises(NotImplementedError): + _ = psu.mode + + +def test_mode_setter_unimplemented(): + with expected_protocol(ik.kiprim.DC310S, [], [], sep="\n") as psu: + with pytest.raises(NotImplementedError): + psu.mode = "cv"