From d84064ec00e29798dee410b1ce4aba504f5acfae Mon Sep 17 00:00:00 2001 From: Overlord360 <49475695+Overlord360@users.noreply.github.com> Date: Fri, 15 Nov 2024 12:47:46 +1100 Subject: [PATCH 01/17] add NPLC config function to fluke 8846a driver --- src/fixate/drivers/dmm/fluke_8846a.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/fixate/drivers/dmm/fluke_8846a.py b/src/fixate/drivers/dmm/fluke_8846a.py index 42b2243a..1f6a4c52 100644 --- a/src/fixate/drivers/dmm/fluke_8846a.py +++ b/src/fixate/drivers/dmm/fluke_8846a.py @@ -42,6 +42,16 @@ def __init__(self, instrument, *args, **kwargs): "continuity": "CONF:CONTinuity", "diode": "CONF:DIODe", } + self._nplc_modes = [ + "resistance", + "fresistance", + "voltage_dc", + "current_dc", + "temperature", + "ftemperature", + ] + self._nplc_settings = [0.02, 0.2, 1, 10, 100] + self._default_nplc = 10 # Default NPLC setting as per Fluke 8846A manual self._init_string = "" # Unchanging @property @@ -222,6 +232,23 @@ def _set_measurement_mode(self, mode, _range=None, suffix=None): ) self._is_error() + def set_nplc(self, nplc=None, reset=False): + if nplc not in self._nplc_settings: + raise ParameterError(f"Invalid NPLC setting {nplc}") + + if self._mode not in self._nplc_modes: + raise ParameterError(f"NPLC setting not available for mode {self._mode}") + + if reset is True or nplc is None: + nplc = self._default_nplc + + mode_str = f"{self._modes[self._mode]}" + mode_str = mode_str.replace( + "CONF:", "" + ) # Remove the CONF: from the start of the string + + self._write(f"{mode_str}:NPLC {nplc}") # e.g. VOLT:DC:NPLC 10 + def voltage_ac(self, _range=None): self._set_measurement_mode("voltage_ac", _range) From f7bf00ccaf08c19ae76dfe5f4b999cd2d33c79a1 Mon Sep 17 00:00:00 2001 From: Overlord360 <49475695+Overlord360@users.noreply.github.com> Date: Fri, 15 Nov 2024 13:02:35 +1100 Subject: [PATCH 02/17] add NPLC config function to Keithley DMM driver --- src/fixate/drivers/dmm/fluke_8846a.py | 6 +++--- src/fixate/drivers/dmm/keithley_6500.py | 24 +++++++++++++++++++++++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/fixate/drivers/dmm/fluke_8846a.py b/src/fixate/drivers/dmm/fluke_8846a.py index 1f6a4c52..f56990fe 100644 --- a/src/fixate/drivers/dmm/fluke_8846a.py +++ b/src/fixate/drivers/dmm/fluke_8846a.py @@ -243,9 +243,9 @@ def set_nplc(self, nplc=None, reset=False): nplc = self._default_nplc mode_str = f"{self._modes[self._mode]}" - mode_str = mode_str.replace( - "CONF:", "" - ) # Remove the CONF: from the start of the string + + # Remove the CONF: from the start of the string + mode_str = mode_str.replace("CONF:", "") self._write(f"{mode_str}:NPLC {nplc}") # e.g. VOLT:DC:NPLC 10 diff --git a/src/fixate/drivers/dmm/keithley_6500.py b/src/fixate/drivers/dmm/keithley_6500.py index b6c0e6db..a24e6f63 100644 --- a/src/fixate/drivers/dmm/keithley_6500.py +++ b/src/fixate/drivers/dmm/keithley_6500.py @@ -40,7 +40,16 @@ def __init__(self, instrument, *args, **kwargs): "continuity": "CONT", "diode": "DIOD", } - + self._nplc_modes = [ + "voltage_dc", + "current_dc", + "resistance", + "fresistance", + "diode", + "temperature", + ] + self._nplc_min = 0.0005 # Minimum NPLC setting for the DMM + self._nplc_max = 12 # Maximum NPLC setting for the DMM self._init_string = "" # Unchanging # Adapted for different DMM behaviour @@ -255,6 +264,19 @@ def _set_measurement_mode(self, mode, _range=None, suffix=None): self._write(f":COUN {self.samples}") self._is_error() + def set_nplc(self, nplc=None, reset=False): + if nplc <= self._nplc_min or nplc >= self._nplc_max: + raise ParameterError(f"NPLC setting out of range for Keithley 6500") + + if self._mode not in self._nplc_modes: + raise ParameterError(f"NPLC setting not available for mode {self._mode}") + + if reset is True or nplc is None: + nplc = "DEF" # keithley supports sending the "DEF" string to reset the NPLC value + + mode_str = f"{self._modes[self._mode]}" + self._write(f":SENS:{mode_str}:NPLC {nplc}") + def voltage_ac(self, _range=None): self._set_measurement_mode("voltage_ac", _range) From abaa0d5084a9c74f2996ab9205a65b8e7e9d7a72 Mon Sep 17 00:00:00 2001 From: Overlord360 <49475695+Overlord360@users.noreply.github.com> Date: Fri, 15 Nov 2024 14:10:10 +1100 Subject: [PATCH 03/17] add function to get min, max and avg for a set amount of samples in both DMM drivers --- src/fixate/drivers/dmm/fluke_8846a.py | 26 ++++++++++++++++---- src/fixate/drivers/dmm/keithley_6500.py | 32 +++++++++++++++++++++---- 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/src/fixate/drivers/dmm/fluke_8846a.py b/src/fixate/drivers/dmm/fluke_8846a.py index f56990fe..f79048f5 100644 --- a/src/fixate/drivers/dmm/fluke_8846a.py +++ b/src/fixate/drivers/dmm/fluke_8846a.py @@ -111,6 +111,25 @@ def measurements(self): with self.lock: return self._read_measurements() + def min_avg_max(self, samples=1, sample_time=1): + """ + automatically samples the DMM for a given number of samples and returns the min, max, and average values + :param samples: number of samples to take + :param sample_time: time to wait for the DMM to take the samples + return: min, avg, max values as floats + """ + + self._write(f"SAMP:COUN {samples}") + self._write("CALC:FUNC AVER") + self._write("CALC:STAT ON") + self._write("INIT") + time.sleep(sample_time) + _min = self.instrument.query_ascii_values("CALC:AVER:MIN?")[0] + _avg = self.instrument.query_ascii_values("CALC:AVER:AVER?")[0] + _max = self.instrument.query_ascii_values("CALC:AVER:MAX?")[0] + + return _min, _avg, _max + def reset(self): """ Checks for errors and then returns DMM to power up state @@ -233,15 +252,14 @@ def _set_measurement_mode(self, mode, _range=None, suffix=None): self._is_error() def set_nplc(self, nplc=None, reset=False): - if nplc not in self._nplc_settings: + if reset is True or nplc is None: + nplc = self._default_nplc + elif nplc not in self._nplc_settings: raise ParameterError(f"Invalid NPLC setting {nplc}") if self._mode not in self._nplc_modes: raise ParameterError(f"NPLC setting not available for mode {self._mode}") - if reset is True or nplc is None: - nplc = self._default_nplc - mode_str = f"{self._modes[self._mode]}" # Remove the CONF: from the start of the string diff --git a/src/fixate/drivers/dmm/keithley_6500.py b/src/fixate/drivers/dmm/keithley_6500.py index a24e6f63..4443b203 100644 --- a/src/fixate/drivers/dmm/keithley_6500.py +++ b/src/fixate/drivers/dmm/keithley_6500.py @@ -147,6 +147,31 @@ def measurements(self): with self.lock: return self._read_measurements() + def min_avg_max(self, samples=1, sample_time=1): + """ + automatically samples the DMM for a given number of samples and returns the min, max, and average values + :param samples: number of samples to take + :param sample_time: time to wait for the DMM to take the samples + return: min, avg, max values as floats + """ + + self._write(f'TRAC:MAKE "TempTable", {samples}') + self._write(f"SENS:COUNt {samples}") + + # we don't actually want the results, this is just to tell the DMM to start sampling + _tmp = self.instrument.query_ascii_values('READ? "TempTable"')[0] + time.sleep(sample_time) + + _avg = self.instrument.query_ascii_values('TRAC:STAT:AVER? "TempTable"')[0] + _min = self.instrument.query_ascii_values('TRAC:STAT:MIN? "TempTable"')[0] + _max = self.instrument.query_ascii_values('TRAC:STAT:MAX? "TempTable"')[0] + + # cleanup + self._write("SENS:COUNt 1") + self._write('TRAC:DEL "TempTable"') + + return _min, _avg, _max + def reset(self): """ Checks for errors and then returns DMM to power up state @@ -265,15 +290,14 @@ def _set_measurement_mode(self, mode, _range=None, suffix=None): self._is_error() def set_nplc(self, nplc=None, reset=False): - if nplc <= self._nplc_min or nplc >= self._nplc_max: + if reset is True or nplc is None: + nplc = "DEF" # keithley supports sending the "DEF" string to reset the NPLC value + elif nplc <= self._nplc_min or nplc >= self._nplc_max: raise ParameterError(f"NPLC setting out of range for Keithley 6500") if self._mode not in self._nplc_modes: raise ParameterError(f"NPLC setting not available for mode {self._mode}") - if reset is True or nplc is None: - nplc = "DEF" # keithley supports sending the "DEF" string to reset the NPLC value - mode_str = f"{self._modes[self._mode]}" self._write(f":SENS:{mode_str}:NPLC {nplc}") From 4d004933ac8e062bc2e0f821ab67fe49ef3f22ab Mon Sep 17 00:00:00 2001 From: Overlord360 <49475695+Overlord360@users.noreply.github.com> Date: Tue, 26 Nov 2024 10:33:00 +1100 Subject: [PATCH 04/17] Updated keithley and fluke nplc settings to the lower common denominator. updated keithley unused variable to be an underscore to be more correct --- src/fixate/drivers/dmm/fluke_8846a.py | 2 +- src/fixate/drivers/dmm/keithley_6500.py | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/fixate/drivers/dmm/fluke_8846a.py b/src/fixate/drivers/dmm/fluke_8846a.py index f79048f5..bdb0673b 100644 --- a/src/fixate/drivers/dmm/fluke_8846a.py +++ b/src/fixate/drivers/dmm/fluke_8846a.py @@ -50,7 +50,7 @@ def __init__(self, instrument, *args, **kwargs): "temperature", "ftemperature", ] - self._nplc_settings = [0.02, 0.2, 1, 10, 100] + self._nplc_settings = [0.02, 0.2, 1, 10] self._default_nplc = 10 # Default NPLC setting as per Fluke 8846A manual self._init_string = "" # Unchanging diff --git a/src/fixate/drivers/dmm/keithley_6500.py b/src/fixate/drivers/dmm/keithley_6500.py index 4443b203..18a3ceae 100644 --- a/src/fixate/drivers/dmm/keithley_6500.py +++ b/src/fixate/drivers/dmm/keithley_6500.py @@ -48,8 +48,7 @@ def __init__(self, instrument, *args, **kwargs): "diode", "temperature", ] - self._nplc_min = 0.0005 # Minimum NPLC setting for the DMM - self._nplc_max = 12 # Maximum NPLC setting for the DMM + self._nplc_settings = [0.02, 0.2, 1, 10] self._init_string = "" # Unchanging # Adapted for different DMM behaviour @@ -159,7 +158,7 @@ def min_avg_max(self, samples=1, sample_time=1): self._write(f"SENS:COUNt {samples}") # we don't actually want the results, this is just to tell the DMM to start sampling - _tmp = self.instrument.query_ascii_values('READ? "TempTable"')[0] + _ = self.instrument.query_ascii_values('READ? "TempTable"') time.sleep(sample_time) _avg = self.instrument.query_ascii_values('TRAC:STAT:AVER? "TempTable"')[0] @@ -292,8 +291,8 @@ def _set_measurement_mode(self, mode, _range=None, suffix=None): def set_nplc(self, nplc=None, reset=False): if reset is True or nplc is None: nplc = "DEF" # keithley supports sending the "DEF" string to reset the NPLC value - elif nplc <= self._nplc_min or nplc >= self._nplc_max: - raise ParameterError(f"NPLC setting out of range for Keithley 6500") + elif nplc not in self._nplc_settings: + raise ParameterError(f"Invalid NPLC setting {nplc}") if self._mode not in self._nplc_modes: raise ParameterError(f"NPLC setting not available for mode {self._mode}") From 21ee0e5dd1e7741219dc5ae7ed4b7263e217f184 Mon Sep 17 00:00:00 2001 From: Overlord360 <49475695+Overlord360@users.noreply.github.com> Date: Tue, 26 Nov 2024 14:43:13 +1100 Subject: [PATCH 05/17] Add nplc context manager to let test scripts use the "with" statement add get_nplc function to query the nplc from the dmm this applies to both the fluke and keithley dmm drivers --- src/fixate/drivers/dmm/fluke_8846a.py | 67 +++++++++++++++++-------- src/fixate/drivers/dmm/keithley_6500.py | 56 +++++++++++++++------ 2 files changed, 87 insertions(+), 36 deletions(-) diff --git a/src/fixate/drivers/dmm/fluke_8846a.py b/src/fixate/drivers/dmm/fluke_8846a.py index bdb0673b..79593208 100644 --- a/src/fixate/drivers/dmm/fluke_8846a.py +++ b/src/fixate/drivers/dmm/fluke_8846a.py @@ -124,11 +124,11 @@ def min_avg_max(self, samples=1, sample_time=1): self._write("CALC:STAT ON") self._write("INIT") time.sleep(sample_time) - _min = self.instrument.query_ascii_values("CALC:AVER:MIN?")[0] - _avg = self.instrument.query_ascii_values("CALC:AVER:AVER?")[0] - _max = self.instrument.query_ascii_values("CALC:AVER:MAX?")[0] + min_ = self.instrument.query_ascii_values("CALC:AVER:MIN?")[0] + avg_ = self.instrument.query_ascii_values("CALC:AVER:AVER?")[0] + max_ = self.instrument.query_ascii_values("CALC:AVER:MAX?")[0] - return _min, _avg, _max + return min_, avg_, max_ def reset(self): """ @@ -251,22 +251,6 @@ def _set_measurement_mode(self, mode, _range=None, suffix=None): ) self._is_error() - def set_nplc(self, nplc=None, reset=False): - if reset is True or nplc is None: - nplc = self._default_nplc - elif nplc not in self._nplc_settings: - raise ParameterError(f"Invalid NPLC setting {nplc}") - - if self._mode not in self._nplc_modes: - raise ParameterError(f"NPLC setting not available for mode {self._mode}") - - mode_str = f"{self._modes[self._mode]}" - - # Remove the CONF: from the start of the string - mode_str = mode_str.replace("CONF:", "") - - self._write(f"{mode_str}:NPLC {nplc}") # e.g. VOLT:DC:NPLC 10 - def voltage_ac(self, _range=None): self._set_measurement_mode("voltage_ac", _range) @@ -353,3 +337,46 @@ def get_identity(self) -> str: (example: FLUKE, 45, 9080025, 2.0, D2.0) """ return self.instrument.query("*IDN?").strip() + + def set_nplc(self, nplc=None, reset=False): + if reset is True or nplc is None: + nplc = self._default_nplc + elif nplc not in self._nplc_settings: + raise ParameterError(f"Invalid NPLC setting {nplc}") + + if self._mode not in self._nplc_modes: + raise ParameterError(f"NPLC setting not available for mode {self._mode}") + + mode_str = f"{self._modes[self._mode]}" + + # Remove the CONF: from the start of the string + mode_str = mode_str.replace("CONF:", "") + + self._write(f"{mode_str}:NPLC {nplc}") # e.g. VOLT:DC:NPLC 10 + + def get_nplc(self): + mode_str = f"{self._modes[self._mode]}" + # Remove the CONF: from the start of the string + mode_str = mode_str.replace("CONF:", "") + return float(self.instrument.query(f"{mode_str}:NPLC?")) + + # context manager for setting NPLC + class _nplc_context_manager(object): + def __init__(self, dmm, nplc=None): + self.dmm = dmm + self.nplc = nplc + self.original_nplc = self.dmm.get_nplc() + + def __enter__(self): + self.dmm.set_nplc(self.nplc) + + # return to default NPLC setting + def __exit__(self, exc_type, exc_val, exc_tb): + # check if an exception was raised + if exc_type is not None or exc_val is not None or exc_tb is not None: + return False # re-raise the exception + # continue with the exit process + self.dmm.set_nplc(self.original_nplc) + + def nplc(self, nplc=None): + return self._nplc_context_manager(self, nplc) diff --git a/src/fixate/drivers/dmm/keithley_6500.py b/src/fixate/drivers/dmm/keithley_6500.py index 18a3ceae..2b9a9610 100644 --- a/src/fixate/drivers/dmm/keithley_6500.py +++ b/src/fixate/drivers/dmm/keithley_6500.py @@ -161,15 +161,15 @@ def min_avg_max(self, samples=1, sample_time=1): _ = self.instrument.query_ascii_values('READ? "TempTable"') time.sleep(sample_time) - _avg = self.instrument.query_ascii_values('TRAC:STAT:AVER? "TempTable"')[0] - _min = self.instrument.query_ascii_values('TRAC:STAT:MIN? "TempTable"')[0] - _max = self.instrument.query_ascii_values('TRAC:STAT:MAX? "TempTable"')[0] + avg_ = self.instrument.query_ascii_values('TRAC:STAT:AVER? "TempTable"')[0] + min_ = self.instrument.query_ascii_values('TRAC:STAT:MIN? "TempTable"')[0] + max_ = self.instrument.query_ascii_values('TRAC:STAT:MAX? "TempTable"')[0] # cleanup self._write("SENS:COUNt 1") self._write('TRAC:DEL "TempTable"') - return _min, _avg, _max + return min_, avg_, max_ def reset(self): """ @@ -288,18 +288,6 @@ def _set_measurement_mode(self, mode, _range=None, suffix=None): self._write(f":COUN {self.samples}") self._is_error() - def set_nplc(self, nplc=None, reset=False): - if reset is True or nplc is None: - nplc = "DEF" # keithley supports sending the "DEF" string to reset the NPLC value - elif nplc not in self._nplc_settings: - raise ParameterError(f"Invalid NPLC setting {nplc}") - - if self._mode not in self._nplc_modes: - raise ParameterError(f"NPLC setting not available for mode {self._mode}") - - mode_str = f"{self._modes[self._mode]}" - self._write(f":SENS:{mode_str}:NPLC {nplc}") - def voltage_ac(self, _range=None): self._set_measurement_mode("voltage_ac", _range) @@ -393,3 +381,39 @@ def get_identity(self) -> str: (example: FLUKE, 45, 9080025, 2.0, D2.0) """ return self.instrument.query("*IDN?").strip() + + def set_nplc(self, nplc=None, reset=False): + if reset is True or nplc is None: + nplc = "DEF" # keithley supports sending the "DEF" string to reset the NPLC value + elif nplc not in self._nplc_settings: + raise ParameterError(f"Invalid NPLC setting {nplc}") + + if self._mode not in self._nplc_modes: + raise ParameterError(f"NPLC setting not available for mode {self._mode}") + + mode_str = f"{self._modes[self._mode]}" + self._write(f":SENS:{mode_str}:NPLC {nplc}") + + def get_nplc(self): + return float(self.instrument.query(f":SENS:{self._modes[self._mode]}:NPLC?")) + + # context manager for setting NPLC + class _nplc_context_manager(object): + def __init__(self, dmm, nplc=None): + self.dmm = dmm + self.nplc = nplc + self.original_nplc = dmm.get_nplc() + + def __enter__(self): + self.dmm.set_nplc(self.nplc) + + # return to default NPLC setting + def __exit__(self, exc_type, exc_val, exc_tb): + # check if an exception was raised + if exc_type is not None or exc_val is not None or exc_tb is not None: + return False # re-raise the exception + # continue with the exit process + self.dmm.set_nplc(self.original_nplc) + + def nplc(self, nplc=None): + return self._nplc_context_manager(self, nplc) From 661ef47211148696420d5403fdce42c85be5d5b1 Mon Sep 17 00:00:00 2001 From: Overlord360 <49475695+Overlord360@users.noreply.github.com> Date: Wed, 27 Nov 2024 09:18:04 +1100 Subject: [PATCH 06/17] add tests for NPLC and min_avg_max functions --- test/drivers/test_fluke_8846A.py | 52 ++++++++++++++++++++++++++++++ test/drivers/test_keithley_6500.py | 52 ++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) diff --git a/test/drivers/test_fluke_8846A.py b/test/drivers/test_fluke_8846A.py index ceaacb72..1280a926 100644 --- a/test/drivers/test_fluke_8846A.py +++ b/test/drivers/test_fluke_8846A.py @@ -316,6 +316,58 @@ def test_measurement_diode(funcgen, dmm, rm): assert meas == pytest.approx(TEST_DIODE, abs=TEST_DIODE_TOL) +@pytest.mark.drivertest +def test_get_nplc(dmm): + query = dmm.get_nplc() + assert query == pytest.approx(10) + + +@pytest.mark.drivertest +def test_set_nplc(dmm): + dmm.voltage_dc() + dmm.set_nplc(nplc=1) + query = dmm.get_nplc() + assert query == pytest.approx(1) + + dmm.set_nplc(reset=True) # Set to default + query = dmm.get_nplc() + assert query == pytest.approx(10) + + # invalid nplc value + with pytest.raises(ParameterError): + dmm.set_nplc(nplc=999) + + # invalid mode + dmm.voltage_ac() + with pytest.raises(ParameterError): + dmm.set_nplc(nplc=1) + + +@pytest.mark.drivertest +def test_nplc_context_manager(dmm): + dmm.voltage_dc() + dmm.set_nplc(nplc=0.02) + with dmm.nplc(1): + query = dmm.get_nplc() + assert query == pytest.approx(1) + query = dmm.get_nplc() + assert query == pytest.approx(0.02) + + with pytest.raises(ZeroDivisionError): + with dmm.nplc(1): + _ = 1 / 0 # make sure exception is not swallowed + + +@pytest.mark.drivertest +def test_min_avg_max(dmm): + dmm.voltage_dc() + dmm.set_nplc(nplc=0.02) + min_, avg_, max_ = dmm.min_avg_max(995, 1.1) + + # does not guarantee that there is any input to the DMM so we can't guarantee that the min, avg, max are different + assert min_ <= avg_ <= max_ + + @pytest.mark.drivertest def test_get_identity(dmm): iden = dmm.get_identity() diff --git a/test/drivers/test_keithley_6500.py b/test/drivers/test_keithley_6500.py index 476239c2..989dce5f 100644 --- a/test/drivers/test_keithley_6500.py +++ b/test/drivers/test_keithley_6500.py @@ -324,6 +324,58 @@ def test_measurement_diode(funcgen, dmm, rm): assert meas == pytest.approx(TEST_DIODE, abs=TEST_DIODE_TOL) +@pytest.mark.drivertest +def test_get_nplc(dmm): + query = dmm.get_nplc() + assert query == pytest.approx(10) + + +@pytest.mark.drivertest +def test_set_nplc(dmm): + dmm.voltage_dc() + dmm.set_nplc(nplc=1) + query = dmm.get_nplc() + assert query == pytest.approx(1) + + dmm.set_nplc(reset=True) # Set to default + query = dmm.get_nplc() + assert query == pytest.approx(10) + + # invalid nplc value + with pytest.raises(ParameterError): + dmm.set_nplc(nplc=999) + + # invalid mode + dmm.voltage_ac() + with pytest.raises(ParameterError): + dmm.set_nplc(nplc=1) + + +@pytest.mark.drivertest +def test_nplc_context_manager(dmm): + dmm.voltage_dc() + dmm.set_nplc(nplc=0.02) + with dmm.nplc(1): + query = dmm.get_nplc() + assert query == pytest.approx(1) + query = dmm.get_nplc() + assert query == pytest.approx(0.02) + + with pytest.raises(ZeroDivisionError): + with dmm.nplc(1): + _ = 1 / 0 # make sure exception is not swallowed + + +@pytest.mark.drivertest +def test_min_avg_max(dmm): + dmm.voltage_dc() + dmm.set_nplc(nplc=0.02) + min_, avg_, max_ = dmm.min_avg_max(995, 1.1) + + # does not guarantee that there is any input to the DMM so we can't guarantee that the min, avg, max are different + assert min_ <= avg_ <= max_ + + @pytest.mark.drivertest def test_get_identity(dmm): iden = dmm.get_identity() From d4a7595ae9bd4bd57d2dbad54347267ee9793e80 Mon Sep 17 00:00:00 2001 From: Overlord360 <49475695+Overlord360@users.noreply.github.com> Date: Wed, 27 Nov 2024 09:48:23 +1100 Subject: [PATCH 07/17] move common DMM code to helper to remove duplicate code made min_avg_max return dictionary instead of the individual values --- src/fixate/drivers/dmm/fluke_8846a.py | 27 ++++--------------------- src/fixate/drivers/dmm/helper.py | 21 +++++++++++++++++++ src/fixate/drivers/dmm/keithley_6500.py | 27 ++++--------------------- 3 files changed, 29 insertions(+), 46 deletions(-) diff --git a/src/fixate/drivers/dmm/fluke_8846a.py b/src/fixate/drivers/dmm/fluke_8846a.py index 79593208..2eecf438 100644 --- a/src/fixate/drivers/dmm/fluke_8846a.py +++ b/src/fixate/drivers/dmm/fluke_8846a.py @@ -116,7 +116,7 @@ def min_avg_max(self, samples=1, sample_time=1): automatically samples the DMM for a given number of samples and returns the min, max, and average values :param samples: number of samples to take :param sample_time: time to wait for the DMM to take the samples - return: min, avg, max values as floats + return: min, avg, max values as floats in a dictionary """ self._write(f"SAMP:COUN {samples}") @@ -128,7 +128,9 @@ def min_avg_max(self, samples=1, sample_time=1): avg_ = self.instrument.query_ascii_values("CALC:AVER:AVER?")[0] max_ = self.instrument.query_ascii_values("CALC:AVER:MAX?")[0] - return min_, avg_, max_ + values = {"min": min_, "avg": avg_, "max": max_} + + return values def reset(self): """ @@ -359,24 +361,3 @@ def get_nplc(self): # Remove the CONF: from the start of the string mode_str = mode_str.replace("CONF:", "") return float(self.instrument.query(f"{mode_str}:NPLC?")) - - # context manager for setting NPLC - class _nplc_context_manager(object): - def __init__(self, dmm, nplc=None): - self.dmm = dmm - self.nplc = nplc - self.original_nplc = self.dmm.get_nplc() - - def __enter__(self): - self.dmm.set_nplc(self.nplc) - - # return to default NPLC setting - def __exit__(self, exc_type, exc_val, exc_tb): - # check if an exception was raised - if exc_type is not None or exc_val is not None or exc_tb is not None: - return False # re-raise the exception - # continue with the exit process - self.dmm.set_nplc(self.original_nplc) - - def nplc(self, nplc=None): - return self._nplc_context_manager(self, nplc) diff --git a/src/fixate/drivers/dmm/helper.py b/src/fixate/drivers/dmm/helper.py index f81bc88a..1bc28ed2 100644 --- a/src/fixate/drivers/dmm/helper.py +++ b/src/fixate/drivers/dmm/helper.py @@ -101,3 +101,24 @@ def reset(self): def get_identity(self): raise NotImplementedError + + # context manager for setting NPLC + class _nplc_context_manager(object): + def __init__(self, dmm, nplc=None): + self.dmm = dmm + self.nplc = nplc + self.original_nplc = dmm.get_nplc() + + def __enter__(self): + self.dmm.set_nplc(self.nplc) + + # return to default NPLC setting + def __exit__(self, exc_type, exc_val, exc_tb): + # check if an exception was raised + if exc_type is not None or exc_val is not None or exc_tb is not None: + return False # re-raise the exception + # continue with the exit process + self.dmm.set_nplc(self.original_nplc) + + def nplc(self, nplc=None): + return self._nplc_context_manager(self, nplc) diff --git a/src/fixate/drivers/dmm/keithley_6500.py b/src/fixate/drivers/dmm/keithley_6500.py index 2b9a9610..a2aa98a6 100644 --- a/src/fixate/drivers/dmm/keithley_6500.py +++ b/src/fixate/drivers/dmm/keithley_6500.py @@ -151,7 +151,7 @@ def min_avg_max(self, samples=1, sample_time=1): automatically samples the DMM for a given number of samples and returns the min, max, and average values :param samples: number of samples to take :param sample_time: time to wait for the DMM to take the samples - return: min, avg, max values as floats + return: min, avg, max values as floats in a dictionary """ self._write(f'TRAC:MAKE "TempTable", {samples}') @@ -169,7 +169,9 @@ def min_avg_max(self, samples=1, sample_time=1): self._write("SENS:COUNt 1") self._write('TRAC:DEL "TempTable"') - return min_, avg_, max_ + values = {"min": min_, "avg": avg_, "max": max_} + + return values def reset(self): """ @@ -396,24 +398,3 @@ def set_nplc(self, nplc=None, reset=False): def get_nplc(self): return float(self.instrument.query(f":SENS:{self._modes[self._mode]}:NPLC?")) - - # context manager for setting NPLC - class _nplc_context_manager(object): - def __init__(self, dmm, nplc=None): - self.dmm = dmm - self.nplc = nplc - self.original_nplc = dmm.get_nplc() - - def __enter__(self): - self.dmm.set_nplc(self.nplc) - - # return to default NPLC setting - def __exit__(self, exc_type, exc_val, exc_tb): - # check if an exception was raised - if exc_type is not None or exc_val is not None or exc_tb is not None: - return False # re-raise the exception - # continue with the exit process - self.dmm.set_nplc(self.original_nplc) - - def nplc(self, nplc=None): - return self._nplc_context_manager(self, nplc) From 1b0fcf0a99286eea2d960f7f751d2b0813fdc80a Mon Sep 17 00:00:00 2001 From: Overlord360 <49475695+Overlord360@users.noreply.github.com> Date: Thu, 28 Nov 2024 09:03:04 +1100 Subject: [PATCH 08/17] Update tests for keithely DMM add funcgen to min_avg_max test to verify that it's working update keithley driver to manually default to an NPLC of 1 as the "DEF" keyword is incorrect --- src/fixate/drivers/dmm/keithley_6500.py | 7 ++++- test/drivers/test_fluke_8846A.py | 24 +++++++++++++-- test/drivers/test_keithley_6500.py | 40 +++++++++++++++++++------ 3 files changed, 59 insertions(+), 12 deletions(-) diff --git a/src/fixate/drivers/dmm/keithley_6500.py b/src/fixate/drivers/dmm/keithley_6500.py index a2aa98a6..bdeb1cf4 100644 --- a/src/fixate/drivers/dmm/keithley_6500.py +++ b/src/fixate/drivers/dmm/keithley_6500.py @@ -49,6 +49,7 @@ def __init__(self, instrument, *args, **kwargs): "temperature", ] self._nplc_settings = [0.02, 0.2, 1, 10] + self._nplc_default = 1 self._init_string = "" # Unchanging # Adapted for different DMM behaviour @@ -386,7 +387,11 @@ def get_identity(self) -> str: def set_nplc(self, nplc=None, reset=False): if reset is True or nplc is None: - nplc = "DEF" # keithley supports sending the "DEF" string to reset the NPLC value + nplc = self._nplc_default + # note: keithley 6500 supports using "DEF" to reset to default NPLC setting + # however this sets the NPLC to 0.02 which is not the default setting + # the datasheet specifies 1 as the default (and this has been confirmed by resetting the dmm) + # so this behaviour is confusing. So we just manually set the default value to 1 elif nplc not in self._nplc_settings: raise ParameterError(f"Invalid NPLC setting {nplc}") diff --git a/test/drivers/test_fluke_8846A.py b/test/drivers/test_fluke_8846A.py index 1280a926..e58b3d96 100644 --- a/test/drivers/test_fluke_8846A.py +++ b/test/drivers/test_fluke_8846A.py @@ -359,14 +359,34 @@ def test_nplc_context_manager(dmm): @pytest.mark.drivertest -def test_min_avg_max(dmm): +def test_min_avg_max(dmm, rm, funcgen): dmm.voltage_dc() dmm.set_nplc(nplc=0.02) - min_, avg_, max_ = dmm.min_avg_max(995, 1.1) + values = dmm.min_avg_max(995, 1.1) + min_ = values["min"] + avg_ = values["avg"] + max_ = values["max"] # does not guarantee that there is any input to the DMM so we can't guarantee that the min, avg, max are different assert min_ <= avg_ <= max_ + v = 50e-3 + f = 50 + rm.mux.connectionMap("DMM_SIG") + funcgen.channel1.waveform.sin() + funcgen.channel1.vrms(v) + funcgen.channel1.frequency(f) + funcgen.channel1(True) + + time.sleep(0.5) + + values = dmm.min_avg_max(995, 1.1) + min_ = values["min"] + avg_ = values["avg"] + max_ = values["max"] + + assert min_ < avg_ < max_ + @pytest.mark.drivertest def test_get_identity(dmm): diff --git a/test/drivers/test_keithley_6500.py b/test/drivers/test_keithley_6500.py index 989dce5f..1df3883f 100644 --- a/test/drivers/test_keithley_6500.py +++ b/test/drivers/test_keithley_6500.py @@ -326,20 +326,22 @@ def test_measurement_diode(funcgen, dmm, rm): @pytest.mark.drivertest def test_get_nplc(dmm): + dmm.voltage_dc() + dmm.set_nplc(reset=True) query = dmm.get_nplc() - assert query == pytest.approx(10) + assert query == pytest.approx(1) @pytest.mark.drivertest def test_set_nplc(dmm): dmm.voltage_dc() - dmm.set_nplc(nplc=1) + dmm.set_nplc(nplc=10) query = dmm.get_nplc() - assert query == pytest.approx(1) + assert query == pytest.approx(10) - dmm.set_nplc(reset=True) # Set to default + dmm.set_nplc(nplc=None) # Set to default query = dmm.get_nplc() - assert query == pytest.approx(10) + assert query == pytest.approx(1) # invalid nplc value with pytest.raises(ParameterError): @@ -354,12 +356,12 @@ def test_set_nplc(dmm): @pytest.mark.drivertest def test_nplc_context_manager(dmm): dmm.voltage_dc() - dmm.set_nplc(nplc=0.02) + dmm.set_nplc(nplc=0.2) with dmm.nplc(1): query = dmm.get_nplc() assert query == pytest.approx(1) query = dmm.get_nplc() - assert query == pytest.approx(0.02) + assert query == pytest.approx(0.2) with pytest.raises(ZeroDivisionError): with dmm.nplc(1): @@ -367,14 +369,34 @@ def test_nplc_context_manager(dmm): @pytest.mark.drivertest -def test_min_avg_max(dmm): +def test_min_avg_max(dmm, rm, funcgen): dmm.voltage_dc() dmm.set_nplc(nplc=0.02) - min_, avg_, max_ = dmm.min_avg_max(995, 1.1) + values = dmm.min_avg_max(995, 1.1) + min_ = values["min"] + avg_ = values["avg"] + max_ = values["max"] # does not guarantee that there is any input to the DMM so we can't guarantee that the min, avg, max are different assert min_ <= avg_ <= max_ + v = 50e-3 + f = 50 + rm.mux.connectionMap("DMM_SIG") + funcgen.channel1.waveform.sin() + funcgen.channel1.vrms(v) + funcgen.channel1.frequency(f) + funcgen.channel1(True) + + time.sleep(0.5) + + values = dmm.min_avg_max(995, 1.1) + min_ = values["min"] + avg_ = values["avg"] + max_ = values["max"] + + assert min_ < avg_ < max_ + @pytest.mark.drivertest def test_get_identity(dmm): From 4bd69b01ad0052044b89370cd800c051467c7614 Mon Sep 17 00:00:00 2001 From: Overlord360 <49475695+Overlord360@users.noreply.github.com> Date: Thu, 28 Nov 2024 09:27:06 +1100 Subject: [PATCH 09/17] change min_avg_max function to return a dataclass instead of a dictionary --- src/fixate/drivers/dmm/fluke_8846a.py | 2 +- src/fixate/drivers/dmm/helper.py | 9 +++++++++ src/fixate/drivers/dmm/keithley_6500.py | 2 +- test/drivers/test_fluke_8846A.py | 16 ++++++++-------- test/drivers/test_keithley_6500.py | 17 +++++++++-------- 5 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/fixate/drivers/dmm/fluke_8846a.py b/src/fixate/drivers/dmm/fluke_8846a.py index 2eecf438..e732e653 100644 --- a/src/fixate/drivers/dmm/fluke_8846a.py +++ b/src/fixate/drivers/dmm/fluke_8846a.py @@ -128,7 +128,7 @@ def min_avg_max(self, samples=1, sample_time=1): avg_ = self.instrument.query_ascii_values("CALC:AVER:AVER?")[0] max_ = self.instrument.query_ascii_values("CALC:AVER:MAX?")[0] - values = {"min": min_, "avg": avg_, "max": max_} + values = DMM.MeasurementStats(min=min_, avg=avg_, max=max_) return values diff --git a/src/fixate/drivers/dmm/helper.py b/src/fixate/drivers/dmm/helper.py index 1bc28ed2..d1790fd3 100644 --- a/src/fixate/drivers/dmm/helper.py +++ b/src/fixate/drivers/dmm/helper.py @@ -1,3 +1,6 @@ +from dataclasses import dataclass + + class DMM: REGEX_ID = "DMM" is_connected = False @@ -102,6 +105,12 @@ def reset(self): def get_identity(self): raise NotImplementedError + @dataclass + class MeasurementStats: + min: float + max: float + avg: float + # context manager for setting NPLC class _nplc_context_manager(object): def __init__(self, dmm, nplc=None): diff --git a/src/fixate/drivers/dmm/keithley_6500.py b/src/fixate/drivers/dmm/keithley_6500.py index bdeb1cf4..f8f92c8c 100644 --- a/src/fixate/drivers/dmm/keithley_6500.py +++ b/src/fixate/drivers/dmm/keithley_6500.py @@ -170,7 +170,7 @@ def min_avg_max(self, samples=1, sample_time=1): self._write("SENS:COUNt 1") self._write('TRAC:DEL "TempTable"') - values = {"min": min_, "avg": avg_, "max": max_} + values = DMM.MeasurementStats(min=min_, avg=avg_, max=max_) return values diff --git a/test/drivers/test_fluke_8846A.py b/test/drivers/test_fluke_8846A.py index e58b3d96..a7252b75 100644 --- a/test/drivers/test_fluke_8846A.py +++ b/test/drivers/test_fluke_8846A.py @@ -363,12 +363,12 @@ def test_min_avg_max(dmm, rm, funcgen): dmm.voltage_dc() dmm.set_nplc(nplc=0.02) values = dmm.min_avg_max(995, 1.1) - min_ = values["min"] - avg_ = values["avg"] - max_ = values["max"] + min_val = values.min + avg_val = values.avg + max_val = values.max # does not guarantee that there is any input to the DMM so we can't guarantee that the min, avg, max are different - assert min_ <= avg_ <= max_ + assert min_val <= avg_val <= max_val v = 50e-3 f = 50 @@ -381,11 +381,11 @@ def test_min_avg_max(dmm, rm, funcgen): time.sleep(0.5) values = dmm.min_avg_max(995, 1.1) - min_ = values["min"] - avg_ = values["avg"] - max_ = values["max"] + min_val = values.min + avg_val = values.avg + max_val = values.max - assert min_ < avg_ < max_ + assert min_val < avg_val < max_val @pytest.mark.drivertest diff --git a/test/drivers/test_keithley_6500.py b/test/drivers/test_keithley_6500.py index 1df3883f..9a4fcbb8 100644 --- a/test/drivers/test_keithley_6500.py +++ b/test/drivers/test_keithley_6500.py @@ -372,13 +372,14 @@ def test_nplc_context_manager(dmm): def test_min_avg_max(dmm, rm, funcgen): dmm.voltage_dc() dmm.set_nplc(nplc=0.02) + values = dmm.min_avg_max(995, 1.1) - min_ = values["min"] - avg_ = values["avg"] - max_ = values["max"] + min_val = values.min + avg_val = values.avg + max_val = values.max # does not guarantee that there is any input to the DMM so we can't guarantee that the min, avg, max are different - assert min_ <= avg_ <= max_ + assert min_val <= avg_val <= max_val v = 50e-3 f = 50 @@ -391,11 +392,11 @@ def test_min_avg_max(dmm, rm, funcgen): time.sleep(0.5) values = dmm.min_avg_max(995, 1.1) - min_ = values["min"] - avg_ = values["avg"] - max_ = values["max"] + min_val = values.min + avg_val = values.avg + max_val = values.max - assert min_ < avg_ < max_ + assert min_val < avg_val < max_val @pytest.mark.drivertest From 1fdc57ff17b6d748d7cfd6e16a12f8a371c4ad0f Mon Sep 17 00:00:00 2001 From: Overlord360 <49475695+Overlord360@users.noreply.github.com> Date: Tue, 3 Dec 2024 15:02:55 +1100 Subject: [PATCH 10/17] Simplify exception check in helper.py --- src/fixate/drivers/dmm/helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fixate/drivers/dmm/helper.py b/src/fixate/drivers/dmm/helper.py index d1790fd3..a2165d20 100644 --- a/src/fixate/drivers/dmm/helper.py +++ b/src/fixate/drivers/dmm/helper.py @@ -124,7 +124,7 @@ def __enter__(self): # return to default NPLC setting def __exit__(self, exc_type, exc_val, exc_tb): # check if an exception was raised - if exc_type is not None or exc_val is not None or exc_tb is not None: + if exc_type: return False # re-raise the exception # continue with the exit process self.dmm.set_nplc(self.original_nplc) From b781b40da0be1776ea619b6b062a29b1de93cf6a Mon Sep 17 00:00:00 2001 From: Overlord360 <49475695+Overlord360@users.noreply.github.com> Date: Tue, 3 Dec 2024 15:33:44 +1100 Subject: [PATCH 11/17] move original_nplc to context manager entry instead of init. --- src/fixate/drivers/dmm/helper.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/fixate/drivers/dmm/helper.py b/src/fixate/drivers/dmm/helper.py index a2165d20..63d46b74 100644 --- a/src/fixate/drivers/dmm/helper.py +++ b/src/fixate/drivers/dmm/helper.py @@ -116,9 +116,11 @@ class _nplc_context_manager(object): def __init__(self, dmm, nplc=None): self.dmm = dmm self.nplc = nplc - self.original_nplc = dmm.get_nplc() + self.original_nplc = None def __enter__(self): + # store the original NPLC setting + self.original_nplc = self.dmm.get_nplc() self.dmm.set_nplc(self.nplc) # return to default NPLC setting From 4da962e58635665eb3ed82f44f8ad402ffe55dc7 Mon Sep 17 00:00:00 2001 From: Overlord360 <49475695+Overlord360@users.noreply.github.com> Date: Wed, 4 Dec 2024 08:53:54 +1100 Subject: [PATCH 12/17] add cleanup commands to fluke driver to disable math function and reset sample count after the function is complete --- src/fixate/drivers/dmm/fluke_8846a.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/fixate/drivers/dmm/fluke_8846a.py b/src/fixate/drivers/dmm/fluke_8846a.py index e732e653..2bb493bb 100644 --- a/src/fixate/drivers/dmm/fluke_8846a.py +++ b/src/fixate/drivers/dmm/fluke_8846a.py @@ -130,6 +130,10 @@ def min_avg_max(self, samples=1, sample_time=1): values = DMM.MeasurementStats(min=min_, avg=avg_, max=max_) + # clean up + self._write("CALC:STAT OFF") + self._write("SAMP:COUN 1") + return values def reset(self): From f657fa22f586c0f8e7998413a662b77da32182ea Mon Sep 17 00:00:00 2001 From: Overlord360 <49475695+Overlord360@users.noreply.github.com> Date: Wed, 4 Dec 2024 11:07:18 +1100 Subject: [PATCH 13/17] Parameterize tests to test over multiple modes of the DMMs --- test/drivers/test_fluke_8846A.py | 160 ++++++++++++++++++++++++----- test/drivers/test_keithley_6500.py | 158 ++++++++++++++++++++++++---- 2 files changed, 273 insertions(+), 45 deletions(-) diff --git a/test/drivers/test_fluke_8846A.py b/test/drivers/test_fluke_8846A.py index a7252b75..da8b7486 100644 --- a/test/drivers/test_fluke_8846A.py +++ b/test/drivers/test_fluke_8846A.py @@ -316,20 +316,76 @@ def test_measurement_diode(funcgen, dmm, rm): assert meas == pytest.approx(TEST_DIODE, abs=TEST_DIODE_TOL) +@pytest.mark.parametrize( + "mode", + [ + ("voltage_dc"), + ("current_dc"), + ("resistance"), + ("fresistance"), + pytest.param( + "voltage_ac", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), + pytest.param( + "current_ac", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), + pytest.param( + "period", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), + pytest.param( + "frequency", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), + pytest.param( + "capacitance", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), + pytest.param( + "continuity", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), + ], +) @pytest.mark.drivertest -def test_get_nplc(dmm): +def test_get_nplc(mode, dmm): + getattr(dmm, mode)() + dmm.set_nplc(reset=True) query = dmm.get_nplc() assert query == pytest.approx(10) +@pytest.mark.parametrize( + "mode", + [ + ("voltage_dc"), + ("current_dc"), + ("resistance"), + ("fresistance"), + pytest.param( + "voltage_ac", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), + pytest.param( + "current_ac", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), + pytest.param( + "period", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), + pytest.param( + "frequency", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), + pytest.param( + "capacitance", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), + pytest.param( + "continuity", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), + ], +) @pytest.mark.drivertest -def test_set_nplc(dmm): - dmm.voltage_dc() +def test_set_nplc(mode, dmm): + getattr(dmm, mode)() dmm.set_nplc(nplc=1) query = dmm.get_nplc() assert query == pytest.approx(1) - dmm.set_nplc(reset=True) # Set to default + dmm.set_nplc(nplc=None) # Set to default query = dmm.get_nplc() assert query == pytest.approx(10) @@ -337,38 +393,69 @@ def test_set_nplc(dmm): with pytest.raises(ParameterError): dmm.set_nplc(nplc=999) - # invalid mode - dmm.voltage_ac() - with pytest.raises(ParameterError): - dmm.set_nplc(nplc=1) - +@pytest.mark.parametrize( + "mode", + [ + ("voltage_dc"), + ("current_dc"), + ("resistance"), + ("fresistance"), + pytest.param( + "voltage_ac", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), + pytest.param( + "current_ac", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), + pytest.param( + "period", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), + pytest.param( + "frequency", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), + pytest.param( + "capacitance", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), + pytest.param( + "continuity", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), + ], +) @pytest.mark.drivertest -def test_nplc_context_manager(dmm): - dmm.voltage_dc() - dmm.set_nplc(nplc=0.02) +def test_nplc_context_manager(mode, dmm): + getattr(dmm, mode)() + + dmm.set_nplc(nplc=0.2) with dmm.nplc(1): query = dmm.get_nplc() assert query == pytest.approx(1) query = dmm.get_nplc() - assert query == pytest.approx(0.02) + assert query == pytest.approx(0.2) with pytest.raises(ZeroDivisionError): with dmm.nplc(1): _ = 1 / 0 # make sure exception is not swallowed +@pytest.mark.parametrize( + "mode, samples, nplc", + [ + ("voltage_ac", 10, None), + ("voltage_dc", 995, 0.02), + ("current_dc", 995, 0.02), + ("current_ac", 10, None), + ("period", 10, None), + ("frequency", 10, None), + ], +) @pytest.mark.drivertest -def test_min_avg_max(dmm, rm, funcgen): - dmm.voltage_dc() - dmm.set_nplc(nplc=0.02) - values = dmm.min_avg_max(995, 1.1) - min_val = values.min - avg_val = values.avg - max_val = values.max +def test_min_avg_max(mode, samples, nplc, dmm, rm, funcgen): + # dmm.voltage_dc() + getattr(dmm, mode)() - # does not guarantee that there is any input to the DMM so we can't guarantee that the min, avg, max are different - assert min_val <= avg_val <= max_val + # only set nplc when able (depends on mode) + if nplc: + dmm.set_nplc(nplc=nplc) v = 50e-3 f = 50 @@ -380,13 +467,40 @@ def test_min_avg_max(dmm, rm, funcgen): time.sleep(0.5) - values = dmm.min_avg_max(995, 1.1) + values = dmm.min_avg_max(samples, 1.1) min_val = values.min avg_val = values.avg max_val = values.max assert min_val < avg_val < max_val + v = 100e-3 + f = 60 + funcgen.channel1.vrms(v) + funcgen.channel1.frequency(f) + time.sleep(0.5) + + values = dmm.min_avg_max(samples, 1.1) + min_val2 = values.min + avg_val2 = values.avg + max_val2 = values.max + + 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 + if mode == "voltage_dc": + assert min_val2 < min_val + assert max_val2 > max_val + + if mode == "frequency": + assert min_val2 > min_val + assert max_val2 > max_val + + if mode == "period": + assert min_val2 < min_val + assert max_val2 < max_val + @pytest.mark.drivertest def test_get_identity(dmm): diff --git a/test/drivers/test_keithley_6500.py b/test/drivers/test_keithley_6500.py index 9a4fcbb8..19761478 100644 --- a/test/drivers/test_keithley_6500.py +++ b/test/drivers/test_keithley_6500.py @@ -324,17 +324,73 @@ def test_measurement_diode(funcgen, dmm, rm): assert meas == pytest.approx(TEST_DIODE, abs=TEST_DIODE_TOL) +@pytest.mark.parametrize( + "mode", + [ + ("voltage_dc"), + ("current_dc"), + ("diode"), + ("resistance"), + ("fresistance"), + pytest.param( + "voltage_ac", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), + pytest.param( + "current_ac", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), + pytest.param( + "period", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), + pytest.param( + "frequency", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), + pytest.param( + "capacitance", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), + pytest.param( + "continuity", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), + ], +) @pytest.mark.drivertest -def test_get_nplc(dmm): - dmm.voltage_dc() +def test_get_nplc(mode, dmm): + getattr(dmm, mode)() dmm.set_nplc(reset=True) query = dmm.get_nplc() assert query == pytest.approx(1) +@pytest.mark.parametrize( + "mode", + [ + ("voltage_dc"), + ("current_dc"), + ("diode"), + ("resistance"), + ("fresistance"), + pytest.param( + "voltage_ac", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), + pytest.param( + "current_ac", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), + pytest.param( + "period", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), + pytest.param( + "frequency", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), + pytest.param( + "capacitance", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), + pytest.param( + "continuity", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), + ], +) @pytest.mark.drivertest -def test_set_nplc(dmm): - dmm.voltage_dc() +def test_set_nplc(mode, dmm): + getattr(dmm, mode)() dmm.set_nplc(nplc=10) query = dmm.get_nplc() assert query == pytest.approx(10) @@ -347,15 +403,39 @@ def test_set_nplc(dmm): with pytest.raises(ParameterError): dmm.set_nplc(nplc=999) - # invalid mode - dmm.voltage_ac() - with pytest.raises(ParameterError): - dmm.set_nplc(nplc=1) - +@pytest.mark.parametrize( + "mode", + [ + ("voltage_dc"), + ("current_dc"), + ("diode"), + ("resistance"), + ("fresistance"), + pytest.param( + "voltage_ac", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), + pytest.param( + "current_ac", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), + pytest.param( + "period", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), + pytest.param( + "frequency", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), + pytest.param( + "capacitance", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), + pytest.param( + "continuity", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), + ], +) @pytest.mark.drivertest -def test_nplc_context_manager(dmm): - dmm.voltage_dc() +def test_nplc_context_manager(mode, dmm): + getattr(dmm, mode)() + dmm.set_nplc(nplc=0.2) with dmm.nplc(1): query = dmm.get_nplc() @@ -368,18 +448,25 @@ def test_nplc_context_manager(dmm): _ = 1 / 0 # make sure exception is not swallowed +@pytest.mark.parametrize( + "mode, samples, nplc", + [ + ("voltage_ac", 10, None), + ("voltage_dc", 995, 0.02), + ("current_dc", 995, 0.02), + ("current_ac", 10, None), + ("period", 10, None), + ("frequency", 10, None), + ], +) @pytest.mark.drivertest -def test_min_avg_max(dmm, rm, funcgen): - dmm.voltage_dc() - dmm.set_nplc(nplc=0.02) - - values = dmm.min_avg_max(995, 1.1) - min_val = values.min - avg_val = values.avg - max_val = values.max +def test_min_avg_max(mode, samples, nplc, dmm, rm, funcgen): + # dmm.voltage_dc() + getattr(dmm, mode)() - # does not guarantee that there is any input to the DMM so we can't guarantee that the min, avg, max are different - assert min_val <= avg_val <= max_val + # only set nplc when able (depends on mode) + if nplc: + dmm.set_nplc(nplc=nplc) v = 50e-3 f = 50 @@ -391,13 +478,40 @@ def test_min_avg_max(dmm, rm, funcgen): time.sleep(0.5) - values = dmm.min_avg_max(995, 1.1) + values = dmm.min_avg_max(samples, 1.1) min_val = values.min avg_val = values.avg max_val = values.max assert min_val < avg_val < max_val + v = 100e-3 + f = 60 + funcgen.channel1.vrms(v) + funcgen.channel1.frequency(f) + time.sleep(0.5) + + values = dmm.min_avg_max(samples, 1.1) + min_val2 = values.min + avg_val2 = values.avg + max_val2 = values.max + + 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 + if mode == "voltage_dc": + assert min_val2 < min_val + assert max_val2 > max_val + + if mode == "frequency": + assert min_val2 > min_val + assert max_val2 > max_val + + if mode == "period": + assert min_val2 < min_val + assert max_val2 < max_val + @pytest.mark.drivertest def test_get_identity(dmm): From 43b8675f892f5e8097a294491ca772e54e0ebc33 Mon Sep 17 00:00:00 2001 From: Overlord360 <49475695+Overlord360@users.noreply.github.com> Date: Wed, 4 Dec 2024 11:13:30 +1100 Subject: [PATCH 14/17] removed error checks from nplc context manager as they're redundant --- src/fixate/drivers/dmm/helper.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/fixate/drivers/dmm/helper.py b/src/fixate/drivers/dmm/helper.py index 63d46b74..3e5699e6 100644 --- a/src/fixate/drivers/dmm/helper.py +++ b/src/fixate/drivers/dmm/helper.py @@ -125,10 +125,6 @@ def __enter__(self): # return to default NPLC setting def __exit__(self, exc_type, exc_val, exc_tb): - # check if an exception was raised - if exc_type: - return False # re-raise the exception - # continue with the exit process self.dmm.set_nplc(self.original_nplc) def nplc(self, nplc=None): From 52a49ecade85700f5340d65fcd8cb837d11b5428 Mon Sep 17 00:00:00 2001 From: Overlord360 <49475695+Overlord360@users.noreply.github.com> Date: Fri, 6 Dec 2024 11:13:19 +1100 Subject: [PATCH 15/17] update incorrect comment about return values --- src/fixate/drivers/dmm/fluke_8846a.py | 2 +- src/fixate/drivers/dmm/keithley_6500.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fixate/drivers/dmm/fluke_8846a.py b/src/fixate/drivers/dmm/fluke_8846a.py index 2bb493bb..9677a7b3 100644 --- a/src/fixate/drivers/dmm/fluke_8846a.py +++ b/src/fixate/drivers/dmm/fluke_8846a.py @@ -116,7 +116,7 @@ def min_avg_max(self, samples=1, sample_time=1): automatically samples the DMM for a given number of samples and returns the min, max, and average values :param samples: number of samples to take :param sample_time: time to wait for the DMM to take the samples - return: min, avg, max values as floats in a dictionary + return: min, avg, max values as floats in a dataclass """ self._write(f"SAMP:COUN {samples}") diff --git a/src/fixate/drivers/dmm/keithley_6500.py b/src/fixate/drivers/dmm/keithley_6500.py index f8f92c8c..5d0acd9a 100644 --- a/src/fixate/drivers/dmm/keithley_6500.py +++ b/src/fixate/drivers/dmm/keithley_6500.py @@ -152,7 +152,7 @@ def min_avg_max(self, samples=1, sample_time=1): automatically samples the DMM for a given number of samples and returns the min, max, and average values :param samples: number of samples to take :param sample_time: time to wait for the DMM to take the samples - return: min, avg, max values as floats in a dictionary + return: min, avg, max values as floats in a dataclass """ self._write(f'TRAC:MAKE "TempTable", {samples}') From 5d96f544bc8b1c920e52e461bcb9ad65c72677b5 Mon Sep 17 00:00:00 2001 From: Overlord360 <49475695+Overlord360@users.noreply.github.com> Date: Fri, 6 Dec 2024 11:31:36 +1100 Subject: [PATCH 16/17] remove ability to set NPCL for diode measurements from keithley (for compatibility between DMMs) --- src/fixate/drivers/dmm/keithley_6500.py | 3 ++- test/drivers/test_fluke_8846A.py | 9 +++++++++ test/drivers/test_keithley_6500.py | 12 +++++++++--- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/fixate/drivers/dmm/keithley_6500.py b/src/fixate/drivers/dmm/keithley_6500.py index 5d0acd9a..9fe3813a 100644 --- a/src/fixate/drivers/dmm/keithley_6500.py +++ b/src/fixate/drivers/dmm/keithley_6500.py @@ -40,14 +40,15 @@ def __init__(self, instrument, *args, **kwargs): "continuity": "CONT", "diode": "DIOD", } + # note: the keithley 6500 also supports changing NPLC for diode measurements, but this has been removed as the fluke does not support it. self._nplc_modes = [ "voltage_dc", "current_dc", "resistance", "fresistance", - "diode", "temperature", ] + # note: the keithley supports setting NPLC to any value between 0.0005 and 12 (with 50hz mains power) but for compatibility with the fluke, we only support the following values self._nplc_settings = [0.02, 0.2, 1, 10] self._nplc_default = 1 self._init_string = "" # Unchanging diff --git a/test/drivers/test_fluke_8846A.py b/test/drivers/test_fluke_8846A.py index da8b7486..98da3f76 100644 --- a/test/drivers/test_fluke_8846A.py +++ b/test/drivers/test_fluke_8846A.py @@ -323,6 +323,9 @@ def test_measurement_diode(funcgen, dmm, rm): ("current_dc"), ("resistance"), ("fresistance"), + pytest.param( + "diode", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), pytest.param( "voltage_ac", marks=pytest.mark.xfail(raises=ParameterError, strict=True) ), @@ -358,6 +361,9 @@ def test_get_nplc(mode, dmm): ("current_dc"), ("resistance"), ("fresistance"), + pytest.param( + "diode", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), pytest.param( "voltage_ac", marks=pytest.mark.xfail(raises=ParameterError, strict=True) ), @@ -401,6 +407,9 @@ def test_set_nplc(mode, dmm): ("current_dc"), ("resistance"), ("fresistance"), + pytest.param( + "diode", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), pytest.param( "voltage_ac", marks=pytest.mark.xfail(raises=ParameterError, strict=True) ), diff --git a/test/drivers/test_keithley_6500.py b/test/drivers/test_keithley_6500.py index 19761478..0481b2ea 100644 --- a/test/drivers/test_keithley_6500.py +++ b/test/drivers/test_keithley_6500.py @@ -329,9 +329,11 @@ def test_measurement_diode(funcgen, dmm, rm): [ ("voltage_dc"), ("current_dc"), - ("diode"), ("resistance"), ("fresistance"), + pytest.param( + "diode", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), pytest.param( "voltage_ac", marks=pytest.mark.xfail(raises=ParameterError, strict=True) ), @@ -365,9 +367,11 @@ def test_get_nplc(mode, dmm): [ ("voltage_dc"), ("current_dc"), - ("diode"), ("resistance"), ("fresistance"), + pytest.param( + "diode", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), pytest.param( "voltage_ac", marks=pytest.mark.xfail(raises=ParameterError, strict=True) ), @@ -409,9 +413,11 @@ def test_set_nplc(mode, dmm): [ ("voltage_dc"), ("current_dc"), - ("diode"), ("resistance"), ("fresistance"), + pytest.param( + "diode", marks=pytest.mark.xfail(raises=ParameterError, strict=True) + ), pytest.param( "voltage_ac", marks=pytest.mark.xfail(raises=ParameterError, strict=True) ), From 3dfa2a13dccc5972df2f982a4f181ec950721a22 Mon Sep 17 00:00:00 2001 From: Overlord360 <49475695+Overlord360@users.noreply.github.com> Date: Mon, 9 Dec 2024 08:30:52 +1100 Subject: [PATCH 17/17] update release notes --- docs/release-notes.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/release-notes.rst b/docs/release-notes.rst index 48a86a73..bbad7252 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -9,6 +9,8 @@ Release Date xx/xx/24 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 use the DMM's internal statistics function to take multiple measurements and return the mean, minimum and maximum values. Improvements ############