diff --git a/docs/release-notes.rst b/docs/release-notes.rst index 67238cce..8b3c0ba9 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -2,6 +2,19 @@ Release Notes ================================== +************* +Version 0.6.5 +************* +Release Date xx/xx/xx + +New Features +############ + +Improvements +############ +- DMM current mode functions now raise an exception when an incompatible port and range combination is used. The range and port parameters are now required parameters. + + ************* Version 0.6.4 ************* @@ -9,7 +22,7 @@ Release Date 14/01/25 New Features ############ -- DMM drivers now have a new function to set NPLC (Number of Power Line Cycles) for the DMM. +- DMM drivers now have a new function to set NPLC (Number of Powr Line Cycles) for the DMM. - DMM drivers now have a new function to use the DMM's internal statistics function to take multiple measurements and return the mean, minimum and maximum values. Improvements diff --git a/src/fixate/drivers/dmm/fluke_8846a.py b/src/fixate/drivers/dmm/fluke_8846a.py index 9677a7b3..40fbc0c8 100644 --- a/src/fixate/drivers/dmm/fluke_8846a.py +++ b/src/fixate/drivers/dmm/fluke_8846a.py @@ -2,6 +2,7 @@ from fixate.core.exceptions import InstrumentError, ParameterError from fixate.drivers.dmm.helper import DMM import time +from typing import Literal class Fluke8846A(DMM): @@ -54,6 +55,10 @@ def __init__(self, instrument, *args, **kwargs): self._default_nplc = 10 # Default NPLC setting as per Fluke 8846A manual self._init_string = "" # Unchanging + # High and low current port definition. Each definition encodes the maximum current able to + # be measured by the port (in amps) + self._current_ports = {"HIGH": 10, "LOW": 400e-3} + @property def samples(self): return self._samples @@ -268,10 +273,36 @@ def voltage_dc(self, _range=None, auto_impedance=False): command = "; :SENS:VOLT:DC:IMP:AUTO OFF" self._set_measurement_mode("voltage_dc", _range, suffix=command) - def current_ac(self, _range=None): + def current_ac(self, _range, port): + """ + Set the measurement mode on the DMM to AC current. + + If the range and port selection are not compatible, i.e. someone has requested to measure + 1 A on the low range port with a maximum capability of 400 mA, an exception is raised. + + If the range requested can be measured by the low port, but the high port is selected, an + exception is raised. + """ + + # Check the port and range combination is compatible for the instrument: + self._check_current_port_range(_range, port) + self._set_measurement_mode("current_ac", _range) - def current_dc(self, _range=None): + def current_dc(self, _range, port: Literal["HIGH", "LOW"]): + """ + Set the measurement mode on the DMM to DC current. + + If the range and port selection are not compatible, i.e. someone has requested to measure + 1A on the low range port with a maximum capability of 400 mA, an exception is raised. + + If the range requested can be measured by the low port, but the high port is selected, an + exception is raised. + """ + + # Check the port and range combination is compatible for the instrument: + self._check_current_port_range(_range, port) + self._set_measurement_mode("current_dc", _range) def resistance(self, _range=None): diff --git a/src/fixate/drivers/dmm/helper.py b/src/fixate/drivers/dmm/helper.py index 3e5699e6..bb00b413 100644 --- a/src/fixate/drivers/dmm/helper.py +++ b/src/fixate/drivers/dmm/helper.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from typing import Literal class DMM: @@ -60,13 +61,23 @@ def voltage_dc(self, _range=None, auto_impedance=False): """ raise NotImplementedError - def current_ac(self, _range): + def current_ac(self, _range, port: Literal["HIGH", "LOW"]): + """ + Sets the DMM in AC current measurement mode. + + Args: + _range: The measurement range to set on the instrument. + port: Informs the driver what physical port on the DMM is intended to be used for the measurement + """ raise NotImplementedError - def current_dc(self, _range): + def current_dc(self, _range, port: Literal["HIGH", "LOW"]): """ - Sets the DMM in DC current measurement mode and puts it in the range given - by the argument _range. Signals expected to be measured must be < _range. + Sets the DMM in DC current measurement mode. + + Args: + _range: The measurement range to set on the instrument. + port: Informs the driver what physical port on the DMM is intended to be used for the measurement """ raise NotImplementedError @@ -129,3 +140,23 @@ def __exit__(self, exc_type, exc_val, exc_tb): def nplc(self, nplc=None): return self._nplc_context_manager(self, nplc) + + def _check_current_port_range(self, range, port): + """ + Checks that the requested port and range are going to be compatible for the instrument. + Raises: + ValueError + Returns: + None + """ + # Check the requested range is not more than the port capability: + if range > self._current_ports[port]: + raise ValueError( + "The selected port and range combination is not available for this instrument. Consider using a different multimeter" + ) + + # Raise an error if the high port is selected when the low port should be used: + if range < self._current_ports["LOW"] and port == "HIGH": + raise ValueError( + "High range port selected when the low range port should be used! Consider using a different multimeter." + ) diff --git a/src/fixate/drivers/dmm/keithley_6500.py b/src/fixate/drivers/dmm/keithley_6500.py index 9fe3813a..fbded8c0 100644 --- a/src/fixate/drivers/dmm/keithley_6500.py +++ b/src/fixate/drivers/dmm/keithley_6500.py @@ -53,6 +53,10 @@ def __init__(self, instrument, *args, **kwargs): self._nplc_default = 1 self._init_string = "" # Unchanging + # High and low current port definition. Each definition encodes the maximum current able to + # be measured by the port (in amps) + self._current_ports = {"HIGH": 10, "LOW": 3} + # Adapted for different DMM behaviour @property def display(self): @@ -303,10 +307,34 @@ def voltage_dc(self, _range=None, auto_impedance=False): command = "; :SENS:VOLT:DC:INP MOHM10" self._set_measurement_mode("voltage_dc", _range, suffix=command) - def current_ac(self, _range=None): + def current_ac(self, _range, port): + """ + Set the measurement mode on the DMM to AC current. + + If the range and port selection are not compatible, i.e. someone has requested to measure + 1A on the low range port with a maximum capability of 400 mA, an exception is raised. + + If the range requested can be measured by the low port, but the high port is selected, an + exception is raised. + """ + # Check the port and range combination is compatible for the instrument: + self._check_current_port_range(_range, port) + self._set_measurement_mode("current_ac", _range) - def current_dc(self, _range=None): + def current_dc(self, _range, port): + """ + Set the measurement mode on the DMM to DC current. + + If the range and port selection are not compatible, i.e. someone has requested to measure + 1A on the low range port with a maximum capability of 400 mA, an exception is raised. + + If the range requested can be measured by the low port, but the high port is selected, an + exception is raised. + """ + # Check the port and range combination is compatible for the instrument: + self._check_current_port_range(_range, port) + self._set_measurement_mode("current_dc", _range) def resistance(self, _range=None): diff --git a/test/drivers/test_fluke_8846A.py b/test/drivers/test_fluke_8846A.py index 98da3f76..cb091325 100644 --- a/test/drivers/test_fluke_8846A.py +++ b/test/drivers/test_fluke_8846A.py @@ -48,8 +48,6 @@ def test_reset(dmm): [ ("voltage_ac", "VOLT:AC"), ("voltage_dc", "VOLT"), - ("current_dc", "CURR"), - ("current_ac", "CURR:AC"), ("resistance", "RES"), ("fresistance", "FRES"), ("period", "PER"), @@ -69,6 +67,63 @@ def test_mode(mode, expected, dmm): assert query.strip('"\r\n') == expected +@pytest.mark.parametrize( + "mode, expected", + [ + ("current_dc", "CURR"), + ("current_ac", "CURR:AC"), + ], +) +@pytest.mark.drivertest +def test_mode_current(mode, expected, dmm): + """ + The current mode has an additional 'port' required parameter. + So we need to test this differently than the other modes. + """ + getattr(dmm, mode)(_range=100e-3, port="LOW") + + query = dmm.instrument.query("SENS:FUNC?") + assert query.strip('"\r\n') == expected + + +@pytest.mark.parametrize( + "mode, expected", + [ + ("current_dc", "CURR"), + ("current_ac", "CURR:AC"), + ], +) +@pytest.mark.drivertest +def test_current_incompatible_port_and_range(mode, expected, dmm): + """ + The current mode has an additional 'port' required parameter. + So we need to test this differently than the other modes. + """ + with pytest.raises(ValueError) as excinfo: + getattr(dmm, mode)(_range=7, port="LOW") + + assert re.search("port and range combination is not available", str(excinfo.value)) + + +@pytest.mark.parametrize( + "mode, expected", + [ + ("current_dc", "CURR"), + ("current_ac", "CURR:AC"), + ], +) +@pytest.mark.drivertest +def test_current_should_use_low_port(mode, expected, dmm): + """ + The current mode has an additional 'port' required parameter. + So we need to test this differently than the other modes. + """ + with pytest.raises(ValueError) as excinfo: + getattr(dmm, mode)(_range=100e-3, port="HIGH") + + assert re.search("low range port should be used", str(excinfo.value)) + + @pytest.mark.parametrize("nsample", [50000, 1]) @pytest.mark.drivertest def test_samples(nsample, dmm): @@ -86,50 +141,141 @@ def test_samples_over_range(nsample, dmm): @pytest.mark.parametrize( - "mode, range", + "mode, args", [ - ("voltage_ac", 1), - ("voltage_ac", 10), - ("voltage_dc", 1), - ("voltage_dc", 10), - ("current_dc", 1), - ("current_dc", 10), - ("current_ac", 1), - ("current_ac", 10), - ("resistance", 10), - ("resistance", 10e6), - ("fresistance", 10), - ("fresistance", 10e6), - ("capacitance", 1e-6), - ("capacitance", 1e-9), + ( + "voltage_ac", + [ + 1, + ], + ), + ( + "voltage_ac", + [ + 10, + ], + ), + ( + "voltage_dc", + [ + 1, + ], + ), + ( + "voltage_dc", + [ + 10, + ], + ), + ("current_dc", [1, "HIGH"]), + ("current_dc", [10, "HIGH"]), + ("current_ac", [1, "HIGH"]), + ("current_ac", [10, "HIGH"]), + ( + "resistance", + [ + 10, + ], + ), + ( + "resistance", + [ + 10e6, + ], + ), + ( + "fresistance", + [ + 10, + ], + ), + ( + "fresistance", + [ + 10e6, + ], + ), + ( + "capacitance", + [ + 1e-6, + ], + ), + ( + "capacitance", + [ + 1e-9, + ], + ), ], ) @pytest.mark.drivertest -def test_range(mode, range, dmm): - getattr(dmm, mode)(_range=range) +def test_range(mode, args, dmm): + getattr(dmm, mode)(*args) mod = dmm.instrument.query("SENS:FUNC?").strip('"\r\n') query = dmm.instrument.query(mod + ":RANG?") - assert float(query) == pytest.approx(range) + assert float(query) == pytest.approx(args[0]) # DMM does not return an error for under range. It just clips the value. # It does return an error for over range however.. + + @pytest.mark.parametrize( - "mode, range", + "mode, args", [ - ("voltage_ac", 10000), - ("voltage_dc", 10000), - ("current_dc", 100), - ("current_ac", 100), - ("resistance", 10e9), - ("fresistance", 10e9), - ("capacitance", 1), + ( + "voltage_ac", + [ + 10000, + ], + ), + ( + "voltage_dc", + [ + 10000, + ], + ), + pytest.param( + "current_dc", + [100, "HIGH"], + marks=pytest.mark.xfail( + raises=ValueError, + reason="API now manages the range checks for the current functions.", + ), + ), + pytest.param( + "current_ac", + [100, "HIGH"], + marks=pytest.mark.xfail( + raises=ValueError, + reason="API now manages the range checks for the current functions.", + ), + ), + ( + "resistance", + [ + 10e9, + ], + ), + ( + "fresistance", + [ + 10e9, + ], + ), + ( + "capacitance", + [ + 1, + ], + ), ], ) @pytest.mark.drivertest -def test_range_over_range(mode, range, dmm): +def test_range_over_range(mode, args, dmm): with pytest.raises(InstrumentError) as excinfo: - getattr(dmm, mode)(_range=range) + getattr(dmm, mode)(*args) assert re.search("Invalid parameter", str(excinfo.value)) @@ -243,7 +389,7 @@ def test_measurement_voltage_ac(funcgen, dmm, rm): @pytest.mark.drivertest def test_measurement_current_dc(funcgen, dmm, rm): rm.mux.connectionMap("DMM_R1_2w") - dmm.current_dc(_range=100e-3) + dmm.current_dc(_range=100e-3, port="LOW") idc = dmm.measurement() assert idc == pytest.approx(TEST_CURRENT_DC, abs=TEST_CURRENT_DC_TOL) @@ -253,7 +399,7 @@ def test_measurement_current_dc(funcgen, dmm, rm): @pytest.mark.drivertest def test_measurement_current_ac(funcgen, dmm, rm): rm.mux.connectionMap("DMM_R1_2w") - dmm.current_ac(_range=100e-3) + dmm.current_ac(_range=100e-3, port="LOW") iac = dmm.measurement() assert iac == pytest.approx(TEST_CURRENT_AC, abs=TEST_CURRENT_AC_TOL) @@ -348,7 +494,12 @@ def test_measurement_diode(funcgen, dmm, rm): ) @pytest.mark.drivertest def test_get_nplc(mode, dmm): - getattr(dmm, mode)() + if "current" in mode: + # Range and port are un-important here. + getattr(dmm, mode)(_range=100e-3, port="LOW") + else: + getattr(dmm, mode)() + dmm.set_nplc(reset=True) query = dmm.get_nplc() assert query == pytest.approx(10) @@ -386,7 +537,11 @@ def test_get_nplc(mode, dmm): ) @pytest.mark.drivertest def test_set_nplc(mode, dmm): - getattr(dmm, mode)() + if "current" in mode: + getattr(dmm, mode)(_range=100e-3, port="LOW") + else: + getattr(dmm, mode)() + dmm.set_nplc(nplc=1) query = dmm.get_nplc() assert query == pytest.approx(1) @@ -432,7 +587,10 @@ def test_set_nplc(mode, dmm): ) @pytest.mark.drivertest def test_nplc_context_manager(mode, dmm): - getattr(dmm, mode)() + if "current" in mode: + getattr(dmm, mode)(_range=100e-3, port="LOW") + else: + getattr(dmm, mode)() dmm.set_nplc(nplc=0.2) with dmm.nplc(1): @@ -459,8 +617,10 @@ def test_nplc_context_manager(mode, dmm): ) @pytest.mark.drivertest def test_min_avg_max(mode, samples, nplc, dmm, rm, funcgen): - # dmm.voltage_dc() - getattr(dmm, mode)() + if "current" in mode: + getattr(dmm, mode)(_range=100e-3, port="LOW") + else: + getattr(dmm, mode)() # only set nplc when able (depends on mode) if nplc: @@ -481,7 +641,7 @@ def test_min_avg_max(mode, samples, nplc, dmm, rm, funcgen): avg_val = values.avg max_val = values.max - assert min_val < avg_val < max_val + assert min_val <= avg_val <= max_val v = 100e-3 f = 60 @@ -494,7 +654,7 @@ def test_min_avg_max(mode, samples, nplc, dmm, rm, funcgen): avg_val2 = values.avg max_val2 = values.max - assert min_val2 < avg_val2 < max_val2 + assert min_val2 <= avg_val2 <= max_val2 # check if values from the two runs are different # We can only really do this for certain modes and the checks depend on the mode diff --git a/test/drivers/test_keithley_6500.py b/test/drivers/test_keithley_6500.py index 0481b2ea..e7555188 100644 --- a/test/drivers/test_keithley_6500.py +++ b/test/drivers/test_keithley_6500.py @@ -48,8 +48,6 @@ def test_reset(dmm): [ ("voltage_ac", "VOLT:AC"), ("voltage_dc", "VOLT:DC"), - ("current_dc", "CURR:DC"), - ("current_ac", "CURR:AC"), ("resistance", "RES"), ("fresistance", "FRES"), ("period", "PER:VOLT"), @@ -57,7 +55,7 @@ def test_reset(dmm): ("capacitance", "CAP"), ("continuity", "CONT"), ("diode", "DIOD"), - ("temperature", "TEMP"), + pytest.param("temperature", "TEMP", marks=pytest.mark.xfail), pytest.param("ftemperature", "TEMP", marks=pytest.mark.xfail), ], ) @@ -69,6 +67,63 @@ def test_mode(mode, expected, dmm): assert query.strip('"\r\n') == expected +@pytest.mark.parametrize( + "mode, expected", + [ + ("current_dc", "CURR:DC"), + ("current_ac", "CURR:AC"), + ], +) +@pytest.mark.drivertest +def test_mode_current(mode, expected, dmm): + """ + The current mode has an additional 'port' required parameter. + So we need to test this differently than the other modes. + """ + getattr(dmm, mode)(_range=100e-3, port="LOW") + + query = dmm.instrument.query("SENS:FUNC?") + assert query.strip('"\r\n') == expected + + +@pytest.mark.parametrize( + "mode", + [ + "current_dc", + "current_ac", + ], +) +@pytest.mark.drivertest +def test_current_incompatible_port_and_range(mode, dmm): + """ + The current mode has an additional 'port' required parameter. + So we need to test this differently than the other modes. + """ + with pytest.raises(ValueError) as excinfo: + getattr(dmm, mode)(_range=7, port="LOW") + + assert re.search("port and range combination is not available", str(excinfo.value)) + + +@pytest.mark.parametrize( + "mode", + [ + "current_dc", + "current_ac", + ], +) +@pytest.mark.drivertest +def test_current_should_use_low_port(mode, dmm): + """ + The current mode has an additional 'port' required parameter. + So we need to test this differently than the other modes. + """ + with pytest.raises(ValueError) as excinfo: + getattr(dmm, mode)(_range=100e-3, port="HIGH") + + assert re.search("low range port should be used", str(excinfo.value)) + + @pytest.mark.parametrize("nsample", [50000, 1]) @pytest.mark.drivertest def test_samples(nsample, dmm): @@ -86,48 +141,137 @@ def test_samples_over_range(nsample, dmm): @pytest.mark.parametrize( - "mode, range", + "mode, args", [ - ("voltage_ac", 1), - ("voltage_ac", 10), - ("voltage_dc", 1), - ("voltage_dc", 10), - ("current_dc", 10e-6), - ("current_dc", 3), - ("current_ac", 100e-6), - ("current_ac", 3), - ("resistance", 10), - ("resistance", 10e6), - ("fresistance", 10), - ("fresistance", 10e6), - ("capacitance", 1e-6), - ("capacitance", 1e-9), + ( + "voltage_ac", + [ + 1, + ], + ), + ( + "voltage_ac", + [ + 10, + ], + ), + ( + "voltage_dc", + [ + 1, + ], + ), + ( + "voltage_dc", + [ + 10, + ], + ), + ("current_dc", [10e-6, "LOW"]), + ("current_dc", [3, "HIGH"]), + ("current_ac", [100e-6, "LOW"]), + ("current_ac", [3, "HIGH"]), + ( + "resistance", + [ + 10, + ], + ), + ( + "resistance", + [ + 10e6, + ], + ), + ( + "fresistance", + [ + 10, + ], + ), + ( + "fresistance", + [ + 10e6, + ], + ), + ( + "capacitance", + [ + 1e-6, + ], + ), + ( + "capacitance", + [ + 1e-9, + ], + ), ], ) @pytest.mark.drivertest -def test_range(mode, range, dmm): - getattr(dmm, mode)(_range=range) +def test_range(mode, args, dmm): + getattr(dmm, mode)(*args) mod = dmm.instrument.query("SENS:FUNC?").strip('"\r\n') query = dmm.instrument.query(mod + ":RANG?") - assert float(query) == pytest.approx(range) + assert float(query) == pytest.approx(args[0]) @pytest.mark.parametrize( - "mode, range", + "mode, args", [ - ("voltage_ac", 10000), - ("voltage_dc", 10000), - ("current_dc", 100), - ("current_ac", 100), - ("resistance", 10e9), - ("fresistance", 10e9), - ("capacitance", 1), + ( + "voltage_ac", + [ + 10000, + ], + ), + ( + "voltage_dc", + [ + 10000, + ], + ), + pytest.param( + "current_dc", + [100, "HIGH"], + marks=pytest.mark.xfail( + raises=ValueError, + reason="API now manages the range checks for the current functions.", + ), + ), + pytest.param( + "current_ac", + [100, "HIGH"], + marks=pytest.mark.xfail( + raises=ValueError, + reason="API now manages the range checks for the current functions.", + ), + ), + ( + "resistance", + [ + 10e9, + ], + ), + ( + "fresistance", + [ + 10e9, + ], + ), + ( + "capacitance", + [ + 1, + ], + ), ], ) @pytest.mark.drivertest -def test_range_over_range(mode, range, dmm): +def test_range_over_range(mode, args, dmm): with pytest.raises(InstrumentError) as excinfo: - getattr(dmm, mode)(_range=range) + getattr(dmm, mode)(*args) assert re.search("Parameter, measure range, expected value", str(excinfo.value)) @@ -251,7 +395,7 @@ def test_measurement_voltage_ac(funcgen, dmm, rm): @pytest.mark.drivertest def test_measurement_current_dc(funcgen, dmm, rm): rm.mux.connectionMap("DMM_R1_2w") - dmm.current_dc(_range=100e-3) + dmm.current_dc(_range=100e-3, port="LOW") idc = dmm.measurement() assert idc == pytest.approx(TEST_CURRENT_DC, abs=TEST_CURRENT_DC_TOL) @@ -261,7 +405,7 @@ def test_measurement_current_dc(funcgen, dmm, rm): @pytest.mark.drivertest def test_measurement_current_ac(funcgen, dmm, rm): rm.mux.connectionMap("DMM_R1_2w") - dmm.current_ac(_range=100e-3) + dmm.current_ac(_range=100e-3, port="LOW") iac = dmm.measurement() assert iac == pytest.approx(TEST_CURRENT_AC, abs=TEST_CURRENT_AC_TOL) @@ -356,7 +500,12 @@ def test_measurement_diode(funcgen, dmm, rm): ) @pytest.mark.drivertest def test_get_nplc(mode, dmm): - getattr(dmm, mode)() + if "current" in mode: + # Current modes have 2 required parameters. + # In this case, the values are un-important + getattr(dmm, mode)(_range=100e-3, port="LOW") + else: + getattr(dmm, mode)() dmm.set_nplc(reset=True) query = dmm.get_nplc() assert query == pytest.approx(1) @@ -394,7 +543,13 @@ def test_get_nplc(mode, dmm): ) @pytest.mark.drivertest def test_set_nplc(mode, dmm): - getattr(dmm, mode)() + if "current" in mode: + # Current modes have 2 required parameters. + # In this case the values are un-important + getattr(dmm, mode)(_range=100e-3, port="LOW") + else: + getattr(dmm, mode)() + dmm.set_nplc(nplc=10) query = dmm.get_nplc() assert query == pytest.approx(10) @@ -440,7 +595,12 @@ def test_set_nplc(mode, dmm): ) @pytest.mark.drivertest def test_nplc_context_manager(mode, dmm): - getattr(dmm, mode)() + if "current" in mode: + # Current modes have 2 required parameters. + # In this case the values are un-important + getattr(dmm, mode)(_range=100e-3, port="LOW") + else: + getattr(dmm, mode)() dmm.set_nplc(nplc=0.2) with dmm.nplc(1): @@ -467,8 +627,12 @@ def test_nplc_context_manager(mode, dmm): ) @pytest.mark.drivertest def test_min_avg_max(mode, samples, nplc, dmm, rm, funcgen): - # dmm.voltage_dc() - getattr(dmm, mode)() + if "current" in mode: + # The range and port are arbitrary. + # In this case the values are un-important + getattr(dmm, mode)(_range=100e-3, port="LOW") + else: + getattr(dmm, mode)() # only set nplc when able (depends on mode) if nplc: @@ -489,7 +653,7 @@ def test_min_avg_max(mode, samples, nplc, dmm, rm, funcgen): avg_val = values.avg max_val = values.max - assert min_val < avg_val < max_val + assert min_val <= avg_val <= max_val v = 100e-3 f = 60 @@ -502,7 +666,7 @@ def test_min_avg_max(mode, samples, nplc, dmm, rm, funcgen): avg_val2 = values.avg max_val2 = values.max - assert min_val2 < avg_val2 < max_val2 + assert min_val2 <= avg_val2 <= max_val2 # check if values from the two runs are different # We can only really do this for certain modes and the checks depend on the mode