"""Interface to the Power-Measuremet-Unit (SMU) NI PXIe-41xx (e.q. 4138, 4141).
:Date: |today|
:Author: Semi-ATE <info@Semi-ATE.org>
"""
import os
import numpy as np
import math
import time
import hightime
import matplotlib.pyplot as plt
from pylab_ml.collate_instrument import Interface
from pylab_ml.base_instrument import InvalidInstrumentConnection
from pylab_ml.baseclass.base_natinst import NatInst
from pylab_ml.base_instrument import logger
[docs]
class PXIe41xx(NatInst):
"""
Interface to the Power-Measuremet-Unit (SMU) NI PXIe-41xx (e.q. 4138, 4141).
.. image:: ../_static/pxie_4138.jpg
:Date: |today|
:Author: Semi-ATE <info@Semi-ATE.org>
todo:
add sequence_loop_count
The National Instruments PXIe-41xx can source and sink power in all four voltage/current quadrants and
measure voltage and current precisely
This Module supports (=tested) the National Instruments
* PXIe-4138
* PXIe-4141
known Bugs:
* wenn eine mqtt message von extern empfangen wird, so kann ein aktuelles Artribute
set/get unterbrochen werden, und es kann dadurch zu Problemen mit dem aktuellen state geben.
z.bsp aperture_time=2, es kommt aber ein voltage von extern (weil gui aufgerufen wird),
state war auf uncommited, voltage setzt state auf running
aperture_time wird weiter verarbeitet, brauch state aber uncommited
==> setzen von Attribute darf nicht unterbrochen werden...
* Source Mode must be configured to Single Point when multiple channels are present
in the same session (e.q. PXIe4141) -> Workaround for stair_sweep, which is implemented :
1. rescue all adjustments
2. close session (source remain their state)
3. reopen only one channel
4. write adjustements to this channel
5. run stair_sweep
6. close session
7. reopen the channels again and write all adjustements to the channels
* --> if you know a better solution, please improve the stair_sweep
some functions of the PXIe41xx are not implemented now,
see: http://nimi-python.readthedocs.io/en/master/nidcpower.html
or read the manual http://zone.ni.com/reference/en-XX/help/370736U-01/
* self.ch[0].source_delay = 0.01667
* self.ch[0].sequence_loop_count = run count x sequence
for generell measurement: http://www.ni.com/de-de/innovations/white-papers/14/top-measurement-considerations-for-modular-source-measure-units-.html
"""
try:
import nidcpower
_has_nidcpower = True
except ImportError:
_has_nidcpower = False
if os.sys.platform != "win32":
logger.error("import nidcpower not found")
else:
logger.error("no import nidcpower can be found on non Windows platforms")
# create functions or proberty and wrap it to the inst.funcname:
# if state != necessary state -> switch to the necessary state
# proberty/function name -> inst.funcname (get,set) , range, call functions
# range : None, Enum or range value
# call functions: see help in attributes
_properties = {
"auto_zero": ("auto_zero", "backend.AutoZero", {"sac": "checkstate(uncommitted)"}),
"aperture_time_units": ("aperture_time_units", "backend.ApertureTimeUnits", {"sac": "checkstate(uncommitted)"}),
"aperture_time": ("aperture_time", None, {"sac": "checkstate(uncommitted)"}),
"dc_noise_rejection": ("dc_noise_rejection", "backend.DCNoiseRejection", {"sac": "checkstate(uncommitted)"}),
"output_function": ("output_function", "backend.OutputFunction", {"sac": "checkstate(uncommitted)"}),
"sense": ("sense", "backend.Sense", {"sac": "checkstate(uncommitted)"}),
"i_autorange": ("current_level_autorange", None, {"sac": "checkstate(uncommitted)"}),
"I_autorange": ("current_limit_autorange", None, {"sac": "checkstate(uncommitted)"}),
"v_autorange": ("voltage_level_autorange", None, {"sac": "checkstate(uncommitted)"}),
"V_autorange": ("voltage_limit_autorange", None, {"sac": "checkstate(uncommitted)"}),
"sequenceLoopCount": ("sequence_loop_count", [1, 134217727], {"sac": "checkstate(uncommitted)"}),
"transientResponse": ("transient_response", "backend.TransientResponse", {"sac": "checkstate(uncommitted)"}),
"onoff": ("output_enabled", [0, 1, True, False], {"sac": "checkstate(running)"}),
}
interchoices = [Interface.pxie]
[docs]
def __init__(self, addr=None, channels="0", identify=False, instName=None, runningmode="auto"):
"""
Initialise.
Args:
addr (string):
name from the PXI-Slot e.q. 'PXI1Slot3' or 'SMU'.
channels (string, optional): (only 4141).
Specifies which output channel(s) to include to this module.
Specify multiple channels by using a channel list or a channel range.
A channel list is a comma (,) separated sequence of channel names.
For example, '0,2' specifies channels 0 and 2.
identify (bool, optional):
Defaults to False.
instName (string, optional):
Instance Name from top.
runningmode (string, optional):
handle state of the instrument automaticall or manual. Defaults to 'auto'.
Raises:
InvalidInstrumentConnection: something is wrong with the connection.
.. note::
For Instrument handling it is much easier to set runningmode = 'auto' (default).
Otherwise you have to handle commit, initiate and abort by yourselve,
and often you will raise an Excepetion by the instrument.
*Examples:*
* Initialization
>>> vdd = PXIe41xx(addr=3,instName='vdd') # connect and initialize instrument PXI-4138 or one channel fro PXI-4141
>>> vdd = PXIe41xx(addr=3,channel='0,1',instName='vdd') # connect and initialize instrument PXI-4141 with channel 0,1
* Use instrument as voltage source
>>> vdd.i_clamp = 0.01 # current protection
>>> vdd.voltage = 3.3 # set output voltage
>>> i = vdd.current # measure (supply) current
* Use instrument as current source
>>> vdd.v_clamp = 5 # voltage protection
>>> vdd.current = 0.1 # set output current_range
>>> v = vdd.voltage # measure voltage
* detailed example of usage:
* common for PXIe4138: :download:`examples/smu/PXIe4138.py <../../../examples/smu/PXIe4138.py>`
* common for PXIe4141: :download:`examples/smu/PXIe4141.py <../../../examples/smu/PXIe4141.py>`
* measure loops : :download:`examples/smu/PXIe4141_2.py <../../../examples/smu/PXIe4141_2.py>`
* loops and stairsweep combined (2x faster as the example before), for PXIe4141/PXIe4138: :download:`examples/smu/PXIe4141_3.py <../../../examples/smu/PXIe4141_3.py>`
* and an example, how you should not used this module: :download:`examples/smu/PXIe4141_badexample.py <../../../examples/smu/PXIe4141_badexample.py>`
"""
if not self._has_nidcpower:
msg = "\nPXIe41xx not usable!! missing nidcpower\n"
msg = msg + "for installing nidcpower:\n"
msg = msg + " Start anaconda prompt and write: python –m pip install nidcpower\n"
msg = msg + "For more infomation see: http://nimi-python.readthedocs.io\n"
raise InvalidInstrumentConnection(msg)
kwargs = {"addr": addr, "channels": channels, "backend": self.nidcpower, "identify": identify, "instName": instName, "runningmode": runningmode}
self.gui = "pylab_ml.gui.instruments.smu.smu"
super().__init__(**kwargs)
logger.debug("Class {}".format(self.__class__.__name__))
self.msg_row_col = (1, 20)
[docs]
def setup_inst(self):
"""Set the instrument settings and intialise some variable."""
super().setup_inst()
self.mqtt_all = [
"id",
"output_function",
"onoff",
"on()",
"off()",
"voltage",
"current",
"Voltage",
"Current",
"v_autorange",
"i_autorange",
"V_autorange",
"I_autorange",
"v_clamp",
"i_clamp",
"v_range",
"i_range",
"V_range",
"I_range",
"sense",
"cmpl",
"channel",
"measure",
"stime",
"sweepto",
]
# self.inst.supported_instrument_models
self.onAfterset = True
self.compliance = True
"""Check compliance after each single measurment.
| True: check compliance after each single measurment (voltage or current), (default=True)
| False: no compliance after measurement (if you want more speed)
"""
self._measure = "vir"
self._sweepto = 0.0
self._v_clamp = 6.0 # reset value = 6V
self._v_range = 6.0 # reset value = 6V
self._stair_delay = 10 # in us, for calculate rise/fall time stair_sweep
self._stair_mintime = 100 # in microseconds, mninimum rise/fall time for stair_sweep
self.stair_step_set = [[0, 0, 0.1], {"typ": "V"}]
self.stair_step_get = np.array([])
self.stair_measure = ""
channels = []
for ch in self.channels.split(","):
channels.append(int(ch))
self.channels = channels
self._create_channelinst(channels)
self.channel = channels[0]
# ----------------------------------------------------------
# doc strings for _properties, propertie themselve will create form dictionary _properties
@property
def aperture_time(self):
"""Specifiy the measurement aperture time for the channel configuration.
Aperture time is specified in the units set by the aperture_time_units property
"""
@property
def aperture_time_units(self):
"""Specifiy the units of the aperture_time property for the channel configuration.
SECONDS or POWER_LINE_CYCLES.
see: http://nimi-python.readthedocs.io/en/master/nidcpower/class.html#aperture-time-units
"""
@property
def auto_zero(self):
"""Specifiy the auto-zero method to use on the device.
OFF, ON or ONCE.
see: http://nimi-python.readthedocs.io/en/master/nidcpower/class.html#auto-zero
"""
@property
def dc_noise_rejection(self):
"""Determine the relative weighting of samples in a measurement.
NORMAL or SECOND_ORDER
see: https://nimi-python.readthedocs.io/en/master/nidcpower/class.html#dc-noise-rejection
"""
@property
def i_autorange(self):
"""Get/set current measurement autorange."""
@property
def I_autorange(self):
"""Get/set current drive autorange."""
@property
def onoff(self):
"""Get/set on state (True), for set 1 or 'on' always possible."""
@property
def output_function(self):
"""Configure the output funkction.
DC_CURRENT or DC_VOLTAGE
"""
@property
def sense(self):
"""Get/set local or remote sensing of the output voltage.
LOCAL or 'REMOTE'
"""
@property
def v_autorange(self):
"""Get/set voltage measurement autorange.
True or False
"""
@property
def V_autorange(self):
"""Get/set voltage drive autorange."""
# end doc strings for _properties
# ----------------------------------------------------------'
[docs]
def checkstate(self, value):
"""
Compare actual state with value and set it to value if compare false.
Parameters
----------
value : :mod:`NatInst.State`
expected status.
Returns
-------
None.
"""
self.state = value
[docs]
def reset(self):
"""
reset, and all channels set to.
* current_limit_autorange=True
* voltage_limit_autorange=True
* power_line_frequency = 50.0
* aperture_time_units=POWER_LINE_CYCLES
* aperture_time=2
* inst.auto_zero=OFF
* dc_noise_rejection=SECOND_ORDER
"""
super().reset()
self.inst.reset_device()
for inst in self.ch:
if inst is not None:
inst.current_limit_autorange = True # set to default, it ist much easiere to set the current/voltage limit with limit_autorange=True
inst.voltage_limit_autorange = True
inst.power_line_frequency = 50.0
inst.aperture_time_units = self.nidcpower.ApertureTimeUnits.POWER_LINE_CYCLES
inst.aperture_time = 2 # set aperture time to 2 PLC
inst.auto_zero = self.nidcpower.AutoZero.OFF # disable auto zero
try: # not possible for PXI-4138
inst.dc_noise_rejection = self.nidcpower.DCNoiseRejection.SECOND_ORDER # set dc noise rejection to "second order"
except Exception:
pass
inst.output_enabled = False
# self.initiate()
# self.commit()
[docs]
def close(self):
"""Disable all outputs and terminate interface."""
self.disable()
super().close()
[docs]
def on(self):
"""Switch output on."""
self.onoff = True
[docs]
def off(self):
"""Switch output off."""
self.onoff = False
@property
def onAfterset(self):
"""
Define behaving after change voltage/curret setting.
Args: if set
value (bool):
* True : enable output after voltage/current setting (default).
* False : output not switching after voltage/current setting.
Returns: if get
value (bool):
True or False.
"""
return self._onAfterset
@onAfterset.setter
def onAfterset(self, value):
self._onAfterset = value
@property
def measure(self):
"""Get voltage and current together without delay, or set the measure typ.
Returns:
* float: mesasured voltage.
* float: measured current.
Tip:
measure is 2x faster as separate voltage and current.
"""
# measure=self.ch[self.channel].fetch_multiple(1) # in sequence mode
if not self.onoff_cache:
logger.error("{self.instName}.measure not possible if instrument = off")
return 0, 0
self.checkstate("running")
elements = self.ch[self.channel].measure_multiple()[0]
# if len(elements == 1):
if self.compliance:
logger.measure("{!r}.voltage == {}, current = {}, Cmpl={}".format(self.instName, elements[0], elements[1], self.cmpl))
else:
logger.measure("{!r}.voltage == {}, current = {}".format(self.instName, elements[0], elements[1]))
return elements[0], elements[1]
@measure.setter
def measure(self, typ="vir"):
self._measure = typ
@property
def cmpl(self):
"""
Check compliance from Device.
Returns
-------
value : bool
* False no compliance
* True compliance, limit accomplished
"""
self.checkstate("running")
value = self.ch[self.channel].query_in_compliance()
if value:
logger.warning("{!r} Compliance == {}".format(self.instName, value))
return value
@property
def sweepto(self):
"""
This is the end value if you run a sweep.
Returns
-------
None.
"""
return self._sweepto
@sweepto.setter
def sweepto(self, value):
self._sweepto = value
@property
def voltage(self):
"""
Get or set output voltage.
If the voltage is set, the output is switched on immediately (if self.onAfterset == True).
Args:
value (float): set to voltage (in V).
Returns:
value (float): output voltage (in V).
"""
self.checkstate("running")
value = self.ch[self.channel].measure(self.nidcpower.MeasurementTypes["VOLTAGE"])
if self.compliance:
logger.measure("{!r}.voltage == {}, Cmpl={}".format(self.instName, float(value), self.cmpl))
else:
logger.measure("{!r}.voltage == {}".format(self.instName, float(value)))
return value
@voltage.setter
def voltage(self, value):
if self.output_function != self.backend.OutputFunction.DC_VOLTAGE:
self.output_function = self.backend.OutputFunction.DC_VOLTAGE
if isinstance(value, (int, float)): # set static voltage
self.ch[self.channel].voltage_level = float(value)
logger.measure("{!r}.voltage := {}".format(self.instName, float(value)))
if self.onAfterset:
self.onoff = True
elif isinstance(value, (tuple, list)): # set voltage ramp
# self.stair_sweep (value[0], value[1], dstep=None, stime=0.01, typ='V', stair='Lin')
if len(self.stair_step_set) > 2:
del self.stair_step_set[2] # remove old values
stair_set_args, stair_set_kwargs = self.stair_step_set[:2]
stair_set_args[0] = value[0]
stair_set_args[1] = value[1]
if len(value) > 2:
stair_set_args[2] = value[2]
stair_set_kwargs["aperture_time"] = None
else:
stair_set_args[2] = None
stair_set_kwargs["aperture_time"] = 0.0
stair_set_kwargs["wait"] = True
stair_set_kwargs["typ"] = stair_set_kwargs["typ"].replace("-", "")
self.stair_step_get = self.stair_sweep(*stair_set_args, **stair_set_kwargs)
else:
logger.error("{!r}.voltage={} not possible, unknown format {}".format(self.instName, value, type(value)))
@property
def Voltage(self):
"""
Get driver Voltage.
Returns:
value(float) : driver Voltage (in V).
"""
value = self.ch[self.channel].voltage_level
return value
@property
def current(self):
"""
Get/set output current.
If the current is set, the output is switched on immediately (if self.onAfterset == True).
Args:
value (float): set to current (in A).
Returns:
value (float): current (in A).
"""
self.checkstate("running")
value = self.ch[self.channel].measure(self.nidcpower.MeasurementTypes["CURRENT"])
if self.compliance:
logger.measure("{!r}.current == {}, Cmpl={}".format(self.instName, float(value), self.cmpl))
else:
logger.measure("{!r}.current == {}".format(self.instName, float(value)))
return value
@current.setter
def current(self, value):
if self.output_function != self.backend.OutputFunction.DC_CURRENT:
self.output_function = self.backend.OutputFunction.DC_CURRENT
self.ch[self.channel].current_level = value
logger.measure("{!r}.current := {}".format(self.instName, float(value)))
if self.onAfterset:
self.onoff = 1
@property
def Current(self):
"""
Get driver current.
Returns:
value (float): driver current (in A).
"""
value = self.ch[self.channel].current_level
return value
# i_clamp and i_range:
# inst.current_lmit: max current in DC_voltage mode
# max values defined in inst.current_limit_range
#
# inst.current_level_autorange ON/OFF
# inst.current_level_range only in DC_CURRENT mode
@property
def i_clamp(self):
"""
Set the current clamping (A), will adjust current measurement range.
Current Limit Range and Current Limit are :
* i_clamp 0.000001 A (int) / 1 µA = Limit +0.00000001 A to +0.000001 A (int) / +10 nA to +1 µA
* i_clamp 0.00001 A (int) / 10 µA = Limit +0.0000001 A to +0.00001 A (int) / +100 nA to +10 µA
* i_clamp 0.0001A (int) / 100 µA = Limit +0.000001 A to +0.0001 A (int) / +1 µA to +100 µA
* i_clamp 0.001 A (int) / 1 mA = Limit +0.00001 A to +0.001 A (int) / +10 µA to +1 mA
* i_clamp 0.01 A (int) / 10 mA = Limit +0.0001 A to +0.01 A (int) / +100 µA to +10 mA
* i_clamp 0.1 A (int) / 100 mA = Limit +0.001 A to +0.1 A (int) / +1 mA to +100 mA
* i_clamp 1 A (int) / 1 A = Limit +0.01 A to +1 A (int) / +10 mA to +1 A
* i_clamp 3 A (int) / 3 A = Limit +0.1 A to +3 A (int) / +100 mA to +3 A
* i_clamp 10 A (int) / 10 A = Limit +0.1 A to +10 A pulsing only (int) / +100 mA to +10 A
"""
# self.ch[self.channel].compliance_limit_symmetry=self.backend.ComplianceLimitSymmetry.SYMMETRIC
limit = self.ch[self.channel].current_limit
logger.measure("{!r}.i_clamp == {}A".format(self.instName, limit))
return limit
@i_clamp.setter
def i_clamp(self, imax): # imax in A
if imax < 10.0e-9:
imax = 10.0e-9
# setting the current range, needs to enable the voltage source modus
if self.output_function != self.backend.OutputFunction.DC_VOLTAGE:
self.onoff = False
self.output_function = self.backend.OutputFunction.DC_VOLTAGE
oldstate = self._state
if self.ch[self.channel].current_limit_autorange == 1:
current_limit_range_old = self.ch[self.channel].current_limit_range
self.checkstate("uncommitted")
self.ch[self.channel].current_limit = imax
if oldstate == self.State.running and self._runningmode == self.Runningmode.auto:
self.initiate()
if self.ch[self.channel].current_limit_range != current_limit_range_old:
logger.info(
"{!r}.i_clamp {}A, i_range accomodating {}A from {}A".format(
self.instName, imax, self.ch[self.channel].current_limit_range, current_limit_range_old
)
)
else:
self._i_clamp = imax
i_range = self.i_range
new_i_range = i_range
if i_range > 100 * imax:
# cannot have compliance (i_clamp) to less than 1% of range
new_i_range = 10 ** (int(math.log10(imax)) + 1)
elif i_range < imax:
# cannot set compliance (i_clamp) to more than range
new_i_range = imax
if i_range != new_i_range:
self._i_range = new_i_range
self.checkstate("uncommitted")
self.ch[self.channel].current_limit_range = new_i_range
self.ch[self.channel].current_limit = imax
if oldstate == self.State.running and self._runningmode == self.Runningmode.auto:
self.initiate()
new_i_range = self.ch[self.channel].current_limit_range # read back, instrument set limit_range to the next range value
logger.info("{!r}.i_clamp {}A, i_range accomodating {}A from {}A".format(self.instName, imax, new_i_range, i_range))
self.ch[self.channel].current_limit = imax
logger.measure("{!r}.i_clamp := {}A".format(self.instName, imax))
@property
def i_range(self):
"""
Set/Get the current range (A), will adjust current clamping.
Current Value Range and Current Value are :
* i_range 0.000001 A (int) / 1 µA = Value ±0.000001 A (int) / ±1 µA
* i_range 0.00001 A (int) / 10 µA = Value ±0.00001 A (int) / ±10 µA
* i_range 0.0001 A (int) / 100 µA = Value ±0.0001 A (int) / ±100 µA
* i_range 0.001 A (int) / 1 mA = Value ±0.001 A (int) / ±1 mA
* i_range 0.01 A (int) / 10 mA = Value ±0.01 A (int) / ±10 mA
* i_range 0.1 A (int) / 100 mA = Value ±0.1 A (int) / ±100 mA
* i_range 3 A (int) / 3 A = Value ±3 A (int) / ±3 A
* i_range 10 A (int) / 10 A = Value ±10 A pulsing only (int) / ±10 A
"""
val = self.ch[self.channel].current_level_range
logger.measure("{!r}.i_range == {}A".format(self.instName, val))
return val
@i_range.setter
def i_range(self, imax):
"""
Set measure current range.
parameter: imax (in A)
"""
current_level_old = self.ch[self.channel].current_level
if current_level_old > imax:
self.ch[self.channel].current_level = imax
logger.info("{!r}.i_range {}A, current accomodating {}A from {}A".format(self.instName, imax, imax, current_level_old))
self.ch[self.channel].current_level_range = imax
logger.measure("{!r}.i_range := {}A".format(self.instName, imax))
@property
def I_range(self):
"""
Set/get current drive range.
Returns
-------
val (float): current drive range (in A).
"""
val = self.ch[self.channel].current_limit_range
logger.measure("{!r}.I_range == {}A".format(self.instName, val))
return val
@I_range.setter
def I_range(self, imax): # imax in A
self.ch[self.channel].current_limit_range = imax
logger.measure("{!r}.I_range := {}A".format(self.instName, imax))
# V_clamp and v_range:
# inst.voltage_lmit: max voltage in DC_currente mode
# max values defined in inst.voltage_limit_range
#
# inst.voltage_level_autorange ON/OFF
# inst.voltage_level_range only in DC_VOLTAGE mode
@property
def v_clamp(self):
"""
Set the voltage clamping (V), will adjust voltage measurement range.
Voltage Limit Range and Voltage Limit
* v_clamp 0.6 V (int) / 600 mV = Limit +0.006 V to +0.6 V (int) / +6 mV to +600 mV
* v_clamp 6 V (int) / 6 V = Limit +0.06 V to +6 V (int) / +60 mV to +6 V
* v_clamp 60 V (int) / 60 V = Limit +0.6 V to +60 V (int) / +600 mV to +60 V
"""
limit = self.ch[self.channel].voltage_limit
logger.measure("{!r}.v_clamp == {}V".format(self.instName, limit))
return limit
@v_clamp.setter
def v_clamp(self, vmax):
if self.output_function != self.backend.OutputFunction.DC_CURRENT:
self.onoff = False
self.output_function = self.backend.OutputFunction.DC_CURRENT
oldstate = self._state
if self.ch[self.channel].voltage_limit_autorange == 1:
voltage_limit_range_old = self.ch[self.channel].voltage_limit_range
self.checkstate("uncommitted")
self.ch[self.channel].voltage_limit = vmax
if self.ch[self.channel].voltage_limit_range != voltage_limit_range_old:
logger.info(
"{!r}.v_clamp {}V, v_range accomodating {}V from {}V".format(
self.instName, vmax, self.ch[self.channel].voltage_limit_range, voltage_limit_range_old
)
)
else:
self._v_clamp = vmax
v_range = self.v_range
new_v_range = v_range
if v_range > 100 * vmax:
# cannot have compliance (v_clamp) to less than 1% of range
new_v_range = 10 ** (int(math.log10(vmax)) + 1)
elif v_range < vmax:
# cannot set compliance (v_clamp) to more than range
new_v_range = vmax
if v_range != new_v_range:
self._v_range = new_v_range
self.checkstate("uncommitted")
self.ch[self.channel].voltage_limit_range = new_v_range
self.ch[self.channel].voltage_limit = vmax
if oldstate == self.State.running and self._runningmode == self.Runningmode.auto:
self.initiate()
new_v_range = self.ch[self.channel].voltage_limit_range # read back, instrument set limit_range to the next range value
logger.info("{!r}.v_clamp {}V, v_range accomodating {}V from {}V".format(self.instName, vmax, new_v_range, v_range))
self.ch[self.channel].voltage_limit = vmax
logger.measure("{!r}.v_clamp := {}V".format(self.instName, vmax))
@property
def v_range(self):
"""
Set the voltage range, will adjust voltage clamping.
Voltage Value Range and Voltage Value are:
* PXIe4138:
* v_range 0.6 V (int) / Value ±0.6 V
* v_range 6 V (int) / Value ±6 V
* v_range 60 V (int) / Value ±60 V
* PXIe4141:
* v_range 10.0 V (int) / Value ±10 V
"""
val = self.ch[self.channel].voltage_level_range
logger.measure("{!r}.v_range == {}V".format(self.instName, val))
return val
@v_range.setter
def v_range(self, vmax):
"""
Set voltage measure range.
vmax in V
"""
self.ch[self.channel].voltage_level_range = vmax
logger.measure("{!r}.v_range := {}V".format(self.instName, vmax))
@property
def V_range(self):
"""Get/set voltage drive range."""
val = self.ch[self.channel].voltage_limit_range
logger.measure("{!r}.V_range == {}V".format(self.instName, val))
return val
@V_range.setter
def V_range(self, vmax): # vmax in V
self.ch[self.channel].voltage_limit_range = vmax
logger.measure("{!r}.V_range := {}V".format(self.instName, vmax))
[docs]
def stair_sweep(self, start, stop, dstep=None, stime=0, typ="V", stair="Lin", aperture_time=None, wait=True):
"""
Make a stait sweep beetween start and stop.
Parameters
----------
start : float
Start value in volts or amps.
stop : TYPE
Stop value in volts or amps.
dstep : TYPE, optional
Delta amplitude for Lin, Points to interpolate for Log, if "None" than take minimum dstep and stime= time for the whole sweep(=rising,falling time). The default is None.
stime : TYPE, optional
Delay between steps, if dstep='None' than stime is the whole time for the sweep (=rising,falling time). The default is 0.
typ : TYPE, optional
'V-', 'I-' = Voltage / Current sourced, '-' changes direction,
volts, amps, timestamp are sensed. The default is 'V'.
stair : TYPE, optional
('Lin','Log) = linear or log source. The default is 'Lin'.
aperture_time : TYPE, optional
Specifies the measurement aperture time for the channel configuration.
Aperture time is specified in the units set by aperture_time_units (default is seconds).
more help: http://zone.ni.com/reference/en-XX/help/370736U-01/nidcpowercref/nidcpower_attr_aperture_time/. The default is None.
wait : TYPE, optional
not implemented yet. The default is True.
Returns
-------
TYPE
original data from the instrument (Voltage, Current, Compliance).
"""
self.sweepto = stop
if start is not None and typ == "V":
self.voltage = start
elif start is not None and typ == "I":
self.current = start
args = [None, stop, dstep]
kwargs = {"stime": stime, "typ": typ, "stair": stair, "aperture_time": aperture_time}
self.stair_step_set = [args, kwargs]
if start is None or str(start) == "?":
start = self.ch[self.channel].voltage_level
if start > stop and isinstance(dstep, (int, float)) and dstep > 0:
dstep = -1.0 * dstep
direction = "UP"
steptime = stime
if "-" in typ:
direction = "DOWN"
# start, stop = stop, start
# dstep *= -1.0
if stair.upper() == "LIN" and isinstance(dstep, (int, float)):
if stop <= start and dstep > 0 or stop >= start and dstep < 0 or dstep == 0:
return
if (stop - start) / dstep <= 1.0:
dstep = (stop - start) / 1.0
logger.debug("{!r} dstep adjusted to {} on this sweep".format(self.instName, dstep))
pts = int((stop - start) / dstep + 1)
elif isinstance(dstep, (int, float)):
pts = dstep + 1
logger.error("{!r} stair_sweep: stair=log not yet implemented, please ask for implementation".format(self.instName))
return
else: # dstep=None -> calculate points from stime
pts = round(stime / self._stair_delay * 1e6)
if pts > self.ch[self.channel].measure_buffer_size: # pts should be < maximum points
pts = self.ch[self.channel].measure_buffer_size
steptime = 0
pts += 1
if pts < 2:
pts = 2
# calculate points:
value_pts = []
delay_pts = []
if direction == "UP":
delta = (stop - start) / (pts - 1)
first = start
last = stop
else:
delta = (start - stop) / (pts - 1)
first = stop
last = start
for i in range(pts): # create ramp with voltage and time
value_pts.append(first + i * delta)
delay_pts.append(float(steptime))
if len(self.stair_step_set) != 3:
self.stair_step_set.append({"value": value_pts, "delay": delay_pts})
else:
self.stair_step_set[2] = [{"value": value_pts, "delay": delay_pts}]
if self._state == self.State.running:
self.abort()
if (
self.inst.channel_count > 1
): # workaround for PXIe4141 lmitation: Source Mode must be configured to Single Point when multiple channels are present in the same session.
setup_attr = []
for attr in self._properties: # create list for all values which are rescuing
setup_attr.append(self._properties[attr][0])
setup_attr += ["power_line_frequency", "dc_noise_rejection"]
lastsetup = {}
for i in self.channels:
lastsetup.update({i: {}})
for attr in setup_attr:
value = getattr(self.inst.channels[i], attr)
lastsetup[i].update({attr: value})
self.close_session()
self.reopen_session(self.channel) # create session only for current channel
for attr in lastsetup[self.channel]:
setattr(self.inst, attr, lastsetup[self.channel][attr]) # set all attributes for current channel again
resume = True
else:
resume = False
if "V" in typ:
self.output_function = self.backend.OutputFunction.DC_VOLTAGE
elif "I" in typ:
self.output_function = self.backend.OutputFunction.DC_CURRENT
if dstep is None:
self.ch[self.channel].aperture_time = 0
self.ch[self.channel].source_delay = 0
else:
if aperture_time is not None:
self.ch[self.channel].aperture_time = aperture_time
if self.ch[self.channel].aperture_time_units == self.nidcpower.ApertureTimeUnits.POWER_LINE_CYCLES:
timeout = self.ch[self.channel].aperture_time * 0.02 * pts
else:
timeout = self.ch[self.channel].aperture_time * pts
if steptime == 0:
timeout += self._stair_delay * 1e-6 * pts
else:
timeout += steptime * pts
self.inst.source_mode = self.nidcpower.SourceMode["SEQUENCE"]
self.inst.set_sequence(value_pts, delay_pts)
self.initiate()
if not wait:
# self.ch[self.channel].measure_when = nidcpower.MeasureWhen.AUTOMATICALLY_AFTER_SOURCE_COMPLETE
logger.error("{!r} stair_sweep: wait=false not yet implemented, please ask for implementation".format(self.instName))
if True:
self.stair_measure = self.inst.fetch_multiple(len(value_pts), timeout=timeout)
self.abort()
if resume:
self.close_session()
self.reopen_session(self.channels)
for i in self.channels: # write back value which are rescue
for attr in lastsetup[self.channel]:
setattr(self.inst.channels[i], attr, lastsetup[self.channel][attr]) # set all attributes for all channel again
else:
self.inst.source_mode = self.nidcpower.SourceMode["SINGLE_POINT"]
if "V" in typ:
self.ch[self.channel].voltage_level = float(last)
else:
self.ch[self.channel].current_level = float(last)
self.initiate()
return self.stair_measure
[docs]
def get_values(self, typ=""):
"""
Get response of previous stair_sweep(), choosing result rows from previous request typ.
get info about calculated divergence to target sweep values, for checking ramping time is ok
Transfer last requested measurement sweep results
Parameters
----------
typ : TYPE, could be 'VITS'
V = Voltage
I = current
T = time
S = status(compliance)
The default is ''.
Returns
-------
TYPE
numpy array from the values.
"""
measure = np.array(self.stair_measure)
cnt_cmpl = measure[0:, 2].sum()
if cnt_cmpl > 0.0:
logger.warning("{!r}.get_values: found Compliance ({}x) in measurement".format(self.instName, cnt_cmpl))
divergence_array = measure[0:, 0] - np.array(self.stair_step_set[2]["value"])
dmax = divergence_array.max()
dmin = divergence_array.min()
divergence = max([dmax, dmin], key=abs)
if divergence == dmax:
index = divergence_array.argmax()
else:
index = divergence_array.argmin()
logger.info(
"{} stair_sweep divergence = {}V, @ set Voltage = {}, measure = {} V, {} A, cmp = {}".format(
self.instName, divergence, self.stair_step_set[2]["value"][index], measure[index][0], measure[index][1], measure[index][2]
)
)
index = []
if "V" in typ.upper():
index += [0]
if "I" in typ.upper():
index += [1]
if "T" in typ.upper():
logger.error("{!r} get_value: typ 't' not yet implemented, please ask for implementation".format(self.instName))
if "S" in typ.upper():
index += [2]
return measure[0:, index]
@property
def stair_step(self):
"""stair_step to target or target, dstep or target, dstep,stime, using previous stair_sweep() parameters."""
return self.stair_step_get
@stair_step.setter
def stair_step(self, stair_set):
if not type(stair_set) in [type(tuple()), type(list())]:
stair_set_args, stair_set_kwargs = self.stair_step_set[:2]
stair_set_args[0] = None
stair_set_args[1] = stair_set
stair_set_kwargs["wait"] = False
stair_set_kwargs["typ"] = stair_set_kwargs["typ"].replace("-", "")
self.stair_step_get = self.stair_sweep(*stair_set_args, **stair_set_kwargs)
else:
if len(stair_set) == 2:
stair_set_args, stair_set_kwargs = self.stair_step_set[:2]
stair_set_args[0] = None
stair_set_args[1] = stair_set[0]
stair_set_args[2] = stair_set[1]
stair_set_kwargs["wait"] = False
stair_set_kwargs["typ"] = stair_set_kwargs["typ"].replace("-", "")
self.stair_step_get = self.stair_sweep(*stair_set_args, **stair_set_kwargs)
if len(stair_set) == 3:
stair_set_args, stair_set_kwargs = self.stair_step_set[:2]
stair_set_args[0] = None
stair_set_args[1] = stair_set[0]
stair_set_args[2] = stair_set[1]
stair_set_kwargs["wait"] = False
stair_set_kwargs["stime"] = stair_set[2]
stair_set_kwargs["typ"] = stair_set_kwargs["typ"].replace("-", "")
self.stair_step_get = self.stair_sweep(*stair_set_args, **stair_set_kwargs)
@property
def stair_slope(self):
"""stair_slope to target or target,slope_time using previous stair_sweep() parameters."""
return self.stair_step_get
@stair_slope.setter
def stair_slope(self, stair_set):
if not type(stair_set) in [type(tuple()), type(list())]:
stair_set_args, stair_set_kwargs = self.stair_step_set[:2]
stair_set_args[0] = None
stair_set_args[1] = stair_set
stair_set_args[2] = None
stair_set_kwargs["wait"] = False
stair_set_kwargs["typ"] = stair_set_kwargs["typ"].replace("-", "")
self.stair_step_get = self.stair_sweep(*stair_set_args, **stair_set_kwargs)
else:
stair_set_args, stair_set_kwargs = self.stair_step_set[:2]
stair_set_args[0] = None
stair_set_args[1] = stair_set[0]
stair_set_args[2] = None
stair_set_kwargs["stime"] = stair_set[1]
stair_set_kwargs["wait"] = False
stair_set_kwargs["typ"] = stair_set_kwargs["typ"].replace("-", "")
self.stair_step_get = self.stair_sweep(*stair_set_args, **stair_set_kwargs)
@property
def stime(self):
"""
Set/get delay between steps.
if dstep='None' than stime is the whole time for the sweep (=rising or falling time).
"""
stair_set_args, stair_set_kwargs = self.stair_step_set[:2]
if "stime" not in stair_set_kwargs:
return 0
return stair_set_kwargs["stime"]
@stime.setter
def stime(self, value):
stair_set_args, stair_set_kwargs = self.stair_step_set[:2]
stair_set_kwargs["stime"] = value
self.stair_step_set = [stair_set_args, stair_set_kwargs]
[docs]
def list_PXIdevices(self):
"""
List all PXIe devices.
but it's not running correcly....
how can we make a list from all devices on PXIe ?
"""
import pyvisa
rm = pyvisa.ResourceManager()
rm.list_resources()
# ('PXI11::13::INSTR', 'ASRL1::INSTR', 'ASRL3::INSTR', 'ASRL5::INSTR', 'ASRL7::INSTR', 'ASRL8::INSTR')
# PCI-Buss 11, PCI-Geraet 13
# -> list only PXI11::13::INSTR PXISLOT6 ??
# {'PXI11::13::INSTR': ResourceInfo(interface_type=<InterfaceType.pxi: 5>, interface_board_number=11, resource_class='INSTR',
# resource_name='PXI11::13::0::INSTR', alias='PXI1Slot6'),
# in NIMAX:
# PXI0::1::BACKPLANE"
# and more....
for resource in rm.list_resources():
print(resource)
# rm.opten_resource(resource)
# print (resource.model_name
rm.close()
# blind-scan.... but found only dcpower devices :-(
for i in range(0, 10):
try:
resource = self.nidcpower.Session(resource_name="PXI1Slot" + str(i))
except Exception:
continue
print("PXI1Slot{} {} {}".format(i, resource.instrument_model, resource.instrument_manufacturer))
resource.close()
def smu_fetch_settings(self, record_length=5_000_000, aperture_time=0.001):
self.inst.measure_when = self.nidcpower.enums.MeasureWhen.ON_MEASURE_TRIGGER
self.inst.measure_trigger_type = self.nidcpower.enums.TriggerType.SOFTWARE_EDGE
self.inst.measure_buffer_size = record_length
self.inst.measure_record_length = record_length
self.inst.aperture_time = aperture_time
def fetch_data(self, npoints, measure="voltage"):
start_time = time.time()
self.inst.send_software_edge_trigger(self.nidcpower.enums.SendSoftwareEdgeTriggerType.MEASURE)
temp = self.inst.fetch_multiple(npoints, timeout=hightime.timedelta(seconds=20))
total_time = time.time() - start_time
time_array = np.linspace(0, total_time, npoints)
time_pts = time_array.tolist()
voltage = []
current = []
if measure == "voltage":
for i in range(0, npoints):
voltage.append(temp[i].voltage)
return time_pts, voltage
elif measure == "current":
for i in range(0, npoints):
current.append(abs(temp[i].current * 1000))
return time_pts, current
elif measure == "both":
for i in range(0, npoints):
voltage.append(temp[i].voltage)
current.append(abs(temp[i].current * 1000))
return time_pts, voltage, current
def plot_smu_data(self, time, voltage=None, current=None):
if (voltage is not None) and (current is not None):
plt.figure(figsize=(6.4, 4.8), dpi=300)
plt.title("SMU Measurements")
plt.subplot(2, 1, 1)
plt.plot(time, voltage, "ro", markersize=0.1, alpha=0.1)
plt.ylabel("Voltage (V)")
plt.grid(linestyle="--")
plt.subplot(2, 1, 2)
plt.plot(time, current, "go", markersize=0.1, alpha=0.1)
plt.xlabel("Time (s)")
plt.ylabel("Current (mA)")
plt.grid(linestyle="--")
else:
plt.figure(figsize=(6.4, 4.8), dpi=300)
if voltage != None:
plt.plot(time, voltage, "ro", markersize=0.1, alpha=0.1)
plt.ylabel("Voltage (V)")
elif current != None:
plt.plot(time, current, "go", markersize=0.1, alpha=0.1)
plt.ylabel("Current (mA)")
plt.xlabel("Time (s)")
plt.title("SMU Measurements")
plt.grid(linestyle="--")
plt.show()
# ---------------------------
if __name__ == "__main__":
from pylab_ml.base_instrument import logsetup
logsetup()
vdd = PXIe41xx(addr="PXI1Slot2", instName="vdd")
# vdd.list_PXIdevices()
# vdd.init()
# vdd.smu_fetch_settings(record_length=30_000, aperture_time=0.001)
#vdd.initiate()
#time_pts, voltage, current = vdd.fetch_data(30_000, "both")
#vdd.plot_smu_data(time_pts, voltage, current)
#vdd.abort()
# # print(vdd.selftest)
# vdd.auto_zero
# vdd.aperture_time
# vdd.auto_zero = "OFF"
# vdd.on()
# logger.info(vdd.state)
# vdd.voltage = 0.5
# vdd.v_range = 0.6
# current = vdd.current
# logger.info("current= {} mA".format(current * 1000))
# logger.info(vdd.state)
# vdd.onoff = 0
# vdd.onoff = 1
# vdd.v_range = 6
# vdd.voltage = 2
# vdd.current = 0.000001
# vdd.i_range = 0.0001
# vdd.v_clamp = 10
# logger.info(vdd.current)
# logger.info(vdd.voltage)
vdd.stair_sweep(0, 5, dstep=None, stime=1.00)
# vdd.stair_sweep(0, 5, dstep=None, stime=1, typ="V-", stair="Lin")
# vdd.stair_sweep(0, 5, dstep=None, stime=0.1, typ="V", stair="Lin", aperture_time=0.1, wait=False)
# vdd.i_clamp = 0.005
# vdd.i_range = 0.1
# logger.info("sense={}".format(vdd.sense))
# vdd.sense = "REMOTE"
# logger.info("sense={}".format(vdd.sense))
# vdd.sense = "LOCAL"
# logger.info(vdd.dc_noise_rejection)
# logger.info(vdd.aperture_time_units)
# logger.info(vdd.auto_zero)
# vdd.voltage = 0, 4 # sweep from 0 to 4V with min rise time
# vdd_voltage = vdd.get_values("v")
# vdd_current = vdd.get_values("v")
# vdd_sense = vdd.get_values("s")
# vdd.time = 0.0005 # set rise/fall time to 500us
# vdd.voltage = 0
# vdd.voltage = 0, 4.2 # sweep from 0 to 4.2V with 500us rise time
# vdd.time = 0.01 # set rise/fall time to 10ms
# vdd.voltage = 0
# vdd.voltage = 0, 4.8 # sweep from 0 to 4.8V with 10ms rise time
# vdd.time = 0.001
# vdd.voltage = 0
# # vdd.voltage=0, 5, 3 # set some nodes, min rise/fall time, delay=500us
# # vdd.time = [0.0005, 0.001, 0.003, 0.001, 0.005, 0.001] #define delay for each node
# # vdd.voltage=0, 5, 4.3, 2.1, 5, 0
# vdd.voltage = 3
# vdd.off()
vdd.close()