Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
d84064e
add NPLC config function to fluke 8846a driver
Overlord360 Nov 15, 2024
f7bf00c
add NPLC config function to Keithley DMM driver
Overlord360 Nov 15, 2024
abaa0d5
add function to get min, max and avg for a set amount of samples in b…
Overlord360 Nov 15, 2024
4d00493
Updated keithley and fluke nplc settings to the lower common denomina…
Overlord360 Nov 25, 2024
21ee0e5
Add nplc context manager to let test scripts use the "with" statement
Overlord360 Nov 26, 2024
661ef47
add tests for NPLC and min_avg_max functions
Overlord360 Nov 26, 2024
d4a7595
move common DMM code to helper to remove duplicate code
Overlord360 Nov 26, 2024
1b0fcf0
Update tests for keithely DMM
Overlord360 Nov 27, 2024
4bd69b0
change min_avg_max function to return a dataclass instead of a dictio…
Overlord360 Nov 27, 2024
1fdc57f
Simplify exception check in helper.py
Overlord360 Dec 3, 2024
b781b40
move original_nplc to context manager entry instead of init.
Overlord360 Dec 3, 2024
4da962e
add cleanup commands to fluke driver to disable math function and res…
Overlord360 Dec 3, 2024
f657fa2
Parameterize tests to test over multiple modes of the DMMs
Overlord360 Dec 4, 2024
43b8675
removed error checks from nplc context manager as they're redundant
Overlord360 Dec 4, 2024
43a2997
Merge branch 'PyFixate:main' into DMM-NPLC-command
Overlord360 Dec 6, 2024
52a49ec
update incorrect comment about return values
Overlord360 Dec 6, 2024
5d96f54
remove ability to set NPCL for diode measurements from keithley (for …
Overlord360 Dec 6, 2024
3dfa2a1
update release notes
Overlord360 Dec 8, 2024
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
57 changes: 57 additions & 0 deletions src/fixate/drivers/dmm/fluke_8846a.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
self._default_nplc = 10 # Default NPLC setting as per Fluke 8846A manual
self._init_string = "" # Unchanging

@property
Expand Down Expand Up @@ -101,6 +111,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 in a dictionary
"""

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]

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):
"""
Checks for errors and then returns DMM to power up state
Expand Down Expand Up @@ -308,3 +343,25 @@ 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?"))
28 changes: 28 additions & 0 deletions src/fixate/drivers/dmm/helper.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from dataclasses import dataclass


class DMM:
REGEX_ID = "DMM"
is_connected = False
Expand Down Expand Up @@ -101,3 +104,28 @@ 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):
self.dmm = dmm
self.nplc = 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
def __exit__(self, exc_type, exc_val, exc_tb):
self.dmm.set_nplc(self.original_nplc)

def nplc(self, nplc=None):
return self._nplc_context_manager(self, nplc)
57 changes: 56 additions & 1 deletion src/fixate/drivers/dmm/keithley_6500.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_settings = [0.02, 0.2, 1, 10]
self._nplc_default = 1
self._init_string = "" # Unchanging

# Adapted for different DMM behaviour
Expand Down Expand Up @@ -138,6 +147,33 @@ 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 in a dictionary
"""

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
_ = 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]

# cleanup
self._write("SENS:COUNt 1")
self._write('TRAC:DEL "TempTable"')

values = DMM.MeasurementStats(min=min_, avg=avg_, max=max_)

return values

def reset(self):
"""
Checks for errors and then returns DMM to power up state
Expand Down Expand Up @@ -348,3 +384,22 @@ 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._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}")

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?"))
186 changes: 186 additions & 0 deletions test/drivers/test_fluke_8846A.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,192 @@ 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(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(mode, dmm):
getattr(dmm, mode)()
dmm.set_nplc(nplc=1)
query = dmm.get_nplc()
assert query == pytest.approx(1)

dmm.set_nplc(nplc=None) # Set to default
query = dmm.get_nplc()
assert query == pytest.approx(10)

# invalid nplc value
with pytest.raises(ParameterError):
dmm.set_nplc(nplc=999)


@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(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.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(mode, samples, nplc, dmm, rm, funcgen):
# dmm.voltage_dc()
getattr(dmm, mode)()

# only set nplc when able (depends on mode)
if nplc:
dmm.set_nplc(nplc=nplc)

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(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):
iden = dmm.get_identity()
Expand Down
Loading