Skip to content

Commit 9aa0494

Browse files
Merge pull request pymeasure#1079 from hailegroup/dev/agilent-4284A
Add Agilent 4284A
2 parents b7077af + b893d19 commit 9aa0494

File tree

5 files changed

+443
-0
lines changed

5 files changed

+443
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#######################
2+
Agilent 4284A LCR Meter
3+
#######################
4+
5+
.. autoclass:: pymeasure.instruments.agilent.Agilent4284A
6+
:members:
7+
:show-inheritance:

docs/api/instruments/agilent/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@ If the instrument you are looking for is not here, also check :doc:`HP<../hp/ind
2323
agilent33500
2424
agilent33521A
2525
agilentB1500
26+
agilent4284A

pymeasure/instruments/agilent/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,4 @@
3434
from .agilent33500 import Agilent33500
3535
from .agilent33521A import Agilent33521A
3636
from .agilentB1500 import AgilentB1500
37+
from .agilent4284A import Agilent4284A
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
#
2+
# This file is part of the PyMeasure package.
3+
#
4+
# Copyright (c) 2013-2024 PyMeasure Developers
5+
#
6+
# Permission is hereby granted, free of charge, to any person obtaining a copy
7+
# of this software and associated documentation files (the "Software"), to deal
8+
# in the Software without restriction, including without limitation the rights
9+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
# copies of the Software, and to permit persons to whom the Software is
11+
# furnished to do so, subject to the following conditions:
12+
#
13+
# The above copyright notice and this permission notice shall be included in
14+
# all copies or substantial portions of the Software.
15+
#
16+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
# THE SOFTWARE.
23+
#
24+
25+
import logging
26+
from time import sleep
27+
28+
from pymeasure.instruments import Instrument, SCPIMixin
29+
from pymeasure.instruments.validators import strict_discrete_set, strict_range
30+
31+
log = logging.getLogger(__name__)
32+
log.addHandler(logging.NullHandler())
33+
34+
IMPEDANCE_MODES = (
35+
"CPD", "CPQ", "CPG", "CPRP", "CSD", "CSQ", "CSRS", "LPQ", "LPD", "LPG", "LPRP",
36+
"LSD", "LSQ", "LSRS", "RX", "ZTD", "ZTR", "GB", "YTD", "YTR"
37+
)
38+
39+
40+
class Agilent4284A(SCPIMixin, Instrument):
41+
"""Represents the Agilent 4284A precision LCR meter.
42+
43+
.. code-block:: python
44+
45+
agilent = Agilent4284A("GPIB::1::INSTR")
46+
47+
agilent.reset() # Return instrument settings to default
48+
values
49+
agilent.frequency = 10e3 # Set frequency to 10 kHz
50+
agilent.voltage = 0.02 # Set AC voltage to 20 mV
51+
agilent.mode = 'ZTR' # Set impedance mode to measure
52+
impedance magnitude [Ohm] and phase
53+
[rad]
54+
agilent.sweep_measurement(
55+
'frequency', [1e4, 1e3, 100] # Perform frequency sweep measurement
56+
) # at 10 kHz, 1 kHz, and 100 Hz
57+
agilent.enable_high_power() # Enable upper current, voltage, and
58+
bias limits, if properly configured.
59+
"""
60+
61+
def __init__(self, adapter, name="Agilent 4284A LCR meter", **kwargs):
62+
kwargs.setdefault("read_termination", '\n')
63+
kwargs.setdefault("write_termination", '\n')
64+
kwargs.setdefault("timeout", 10000)
65+
super().__init__(adapter, name, **kwargs)
66+
self._set_ranges(0)
67+
68+
frequency = Instrument.control(
69+
"FREQ?", "FREQ %g",
70+
"""Control AC frequency in Hertz, from 20 Hz to 1 MHz.""",
71+
validator=strict_range,
72+
values=(20, 1e6),
73+
)
74+
75+
ac_current = Instrument.control(
76+
"CURR:LEV?", "CURR:LEV %g",
77+
"""Control AC current level in Amps. Valid range is 50 uA to 20 mA for default,
78+
50 uA to 200 mA in high-power mode.""",
79+
validator=strict_range,
80+
values=(50e-6, 0.02),
81+
dynamic=True
82+
)
83+
84+
ac_voltage = Instrument.control(
85+
"VOLT:LEV?", "VOLT:LEV %g",
86+
"""Control AC voltage level in Volts. Range is 5 mV to 2 V for default, 5 mV to
87+
20 V in high-power mode.""",
88+
validator=strict_range,
89+
values=(0.005, 2),
90+
dynamic=True
91+
)
92+
93+
bias_enabled = Instrument.control(
94+
"BIAS:STAT?", "BIAS:STAT %d",
95+
"""Control whether DC bias is enabled.""",
96+
validator=strict_discrete_set,
97+
values={False: 0, True: 1},
98+
map_values=True
99+
)
100+
101+
bias_voltage = Instrument.control(
102+
"BIAS:VOLT?", "BIAS:VOLT %g",
103+
"""Control the DC bias voltage in Volts.
104+
Maximum is 2 V by default, 40 V in high-power mode.""",
105+
validator=strict_range,
106+
values=(0, 2),
107+
dynamic=True
108+
)
109+
110+
bias_current = Instrument.control(
111+
"BIAS:CURR?", "BIAS:CURR %g",
112+
"""Control the DC bias current in Amps.
113+
Requires Option 001 (power amplifier / DC bias) to be installed.
114+
Maximum is 100 mA.""",
115+
validator=strict_range,
116+
values=(0, 0),
117+
dynamic=True
118+
)
119+
120+
impedance_mode = Instrument.control(
121+
"FUNC:IMP?", "FUNC:IMP %s",
122+
"""Control impedance measurement function.
123+
124+
* CPD: Parallel capacitance [F] and dissipation factor [number]
125+
* CPQ: Parallel capacitance [F] and quality factor [number]
126+
* CPG: Parallel capacitance [F] and parallel conductance [S]
127+
* CPRP: Parallel capacitance [F] and parallel resistance [Ohm]
128+
129+
* CSD: Series capacitance [F] and dissipation factor [number]
130+
* CSQ: Series capacitance [F] and quality factor [number]
131+
* CSRS: Series capacitance [F] and series resistance [Ohm]
132+
133+
* LPQ: Parallel inductance [H] and quality factor [number]
134+
* LPD: Parallel inductance [H] and dissipation factor [number]
135+
* LPG: Parallel inductance [H] and parallel conductance [S]
136+
* LPRP: Parallel inductance [H] and parallel resistance [Ohm]
137+
138+
* LSD: Series inductance [H] and dissipation factor [number]
139+
* LSQ: Seriesinductance [H] and quality factor [number]
140+
* LSRS: Series inductance [H] and series resistance [Ohm]
141+
142+
* RX: Resistance [Ohm] and reactance [Ohm]
143+
* ZTD: Impedance, magnitude [Ohm] and phase [deg]
144+
* ZTR: Impedance, magnitude [Ohm] and phase [rad]
145+
* GB: Conductance [S] and susceptance [S]
146+
* YTD: Admittance, magnitude [Ohm] and phase [deg]
147+
* YTR: Admittance magnitude [Ohm] and phase [rad]
148+
""",
149+
validator=strict_discrete_set,
150+
values=IMPEDANCE_MODES
151+
)
152+
153+
impedance_range = Instrument.control(
154+
"FUNC:IMP:RANG?", "FUNC:IMP:RANG %g",
155+
"""Control the impedance measurement range. The 4284A will select an appropriate
156+
measurement range for the setting value."""
157+
)
158+
159+
auto_range_enabled = Instrument.control(
160+
"FUNC:IMP:RANG:AUTO?", "FUNC:IMP:RANG:AUTO %d",
161+
"""Control whether the impedance auto range is enabled.""",
162+
validator=strict_discrete_set,
163+
values={False: 0, True: 1},
164+
map_values=True
165+
)
166+
167+
trigger_source = Instrument.control(
168+
"TRIG:SOUR?", "TRIG:SOUR %s",
169+
"""Control trigger mode. Valid options are `INT`, `EXT`, `BUS`, or `HOLD`.""",
170+
validator=strict_discrete_set,
171+
values=('INT', 'EXT', 'BUS', 'HOLD'),
172+
cast=str
173+
)
174+
175+
trigger_delay = Instrument.control(
176+
"TRIG:DEL?", "TRIG:DEL %g",
177+
"""Control trigger delay in seconds. Valid range is 0 to 60, with 1 ms resolution.""",
178+
validator=strict_range,
179+
values=(0, 60)
180+
)
181+
182+
trigger_continuous_enabled = Instrument.control(
183+
"TRIG:CONT?", "TRIG:CONT %d",
184+
"""Control whether trigger state automatically returns to WAIT FOR TRIGGER
185+
after measurement.""",
186+
validator=strict_discrete_set,
187+
values={False: 0, True: 1},
188+
map_values=True
189+
)
190+
191+
def _set_ranges(self, high_power_mode):
192+
"""Set dynamic property values and make copies for sweep_measurement to reference."""
193+
if high_power_mode:
194+
self.ac_current_values = (50e-6, 0.2)
195+
self.ac_voltage_values = (0.005, 20)
196+
self.bias_voltage_values = (0, 40)
197+
self.bias_current_values = (0, 0.1)
198+
self._ac_current_values = (50e-6, 0.2)
199+
self._ac_voltage_values = (0.005, 20)
200+
self._bias_voltage_values = (0, 40)
201+
self._bias_current_values = (0, 0.1)
202+
else:
203+
self.ac_current_values = (50e-6, 0.02)
204+
self.ac_voltage_values = (0.005, 2)
205+
self.bias_voltage_values = (0, 2)
206+
self.bias_current_values = (0, 0)
207+
self._ac_current_values = (50e-6, 0.02)
208+
self._ac_voltage_values = (0.005, 2)
209+
self._bias_voltage_values = (0, 2)
210+
self._bias_current_values = (0, 0)
211+
212+
@property
213+
def high_power_enabled(self):
214+
"""Control whether high power mode is enabled.
215+
Enabling requires option 001 (power amplifier / DC bias) to be installed.
216+
"""
217+
mode = self.values("OUTP:HPOW?", cast=int)
218+
return bool(mode)
219+
220+
@high_power_enabled.setter
221+
def high_power_enabled(self, val):
222+
if not val:
223+
self._set_ranges(0)
224+
self.write("OUTP:HPOW 0")
225+
elif val and self.options[0] == '0':
226+
raise AttributeError("Agilent 4284A power amplifier is not installed.")
227+
else:
228+
self._set_ranges(1)
229+
self.write("OUTP:HPOW 1")
230+
231+
def sweep_measurement(self, sweep_mode, sweep_values):
232+
"""Run list sweep measurement using sequential trigger.
233+
234+
:param str sweep_mode: parameter to sweep across. Must be one of `frequency`,
235+
`voltage`, `current`, `bias_voltage`, or `bias_current`.
236+
:param sweep_values: list of parameter values to sweep across.
237+
238+
:returns: values as configured with :attr:`~.Agilent4284A.impedance_mode` and
239+
list of sweep parameters in format ([val A], [val B], [sweep_values])
240+
"""
241+
param_dict = {
242+
"frequency": ("FREQ", (20, 1e6)),
243+
"voltage": ("VOLT", self._ac_voltage_values),
244+
"current": ("CURR", self._ac_current_values),
245+
"bias_voltage": ("BIAS:VOLT", self._bias_voltage_values),
246+
"bias_current": ("BIAS:CURR", self._bias_current_values)
247+
}
248+
249+
if sweep_mode not in param_dict:
250+
raise KeyError(
251+
f"Sweep mode but be one of {list(param_dict.keys())}, not '{sweep_mode}'."
252+
)
253+
254+
low_limit = param_dict[sweep_mode][1][0]
255+
high_limit = param_dict[sweep_mode][1][1]
256+
if (min(sweep_values) < low_limit or max(sweep_values) > high_limit):
257+
log.warning(
258+
"%s values are outside valid Agilent 4284A range of %g and %g "
259+
"and will be truncated.", sweep_mode, low_limit, high_limit
260+
)
261+
sweep_truncated = []
262+
for val in sweep_values:
263+
if low_limit <= val <= high_limit:
264+
sweep_truncated.append(val)
265+
sweep_values = sweep_truncated
266+
267+
loops = (len(sweep_values) - 1) // 10 # 4284A sweeps 10 points at a time
268+
param_div = []
269+
for i in range(loops):
270+
param_div.append(sweep_values[10*i:10*(i+1)])
271+
param_div.append(sweep_values[loops*10:])
272+
273+
self.clear()
274+
self.write("TRIG:SOUR BUS;:DISP:PAGE LIST;:FORM ASC;:LIST:MODE SEQ;:INIT:CONT ON")
275+
276+
a_data = []
277+
b_data = []
278+
sweep_return = []
279+
for i in range(loops + 1):
280+
param_str = ",".join(['%g' % p for p in param_div[i]])
281+
self.write(f"LIST:{param_dict[sweep_mode][0]} {param_str};:TRIG:IMM")
282+
status_event_register = int(self.ask("STAT:OPER?"))
283+
while (status_event_register & 8) != 8: # Sweep bit no. 3
284+
sleep(0.1)
285+
status_event_register = int(self.ask("STAT:OPER?"))
286+
measured = self.values("FETCH?")
287+
# gets 4-ples of numbers, first two are data A and B
288+
a_data += [measured[_] for _ in range(0, 4 * len(param_div[i]), 4)]
289+
b_data += [measured[_] for _ in range(1, 4 * len(param_div[i]), 4)]
290+
sweep_return += self.values(f"LIST:{param_dict[sweep_mode][0]}?")
291+
292+
# Return to manual trigger and reset display
293+
self.write(":TRIG:SOUR HOLD;:DISP:PAGE MEAS")
294+
self.check_errors()
295+
return a_data, b_data, sweep_return
296+
297+
def trigger(self):
298+
"""Execute a bus trigger, regardless of trigger state.
299+
Can be used when :attr:`trigger_source` is set to `BUS`.
300+
Returns result of triggered measurement.
301+
"""
302+
return self.values("*TRG")
303+
304+
def trigger_immediate(self):
305+
"""Execute a bus trigger, regardless of trigger state.
306+
Can be used when :attr:`trigger_source` is set to `BUS`.
307+
Measurement result must be retrieved with `FETCH?` command.
308+
"""
309+
self.write("TRIG:IMM")
310+
311+
def trigger_initiate(self):
312+
"""Change the trigger state from IDLE to WAIT FOR TRIGGER for one trigger sequence."""
313+
self.write("TRIG:INIT:IMM")

0 commit comments

Comments
 (0)