Source code for pylab_ml.smu.natinst.pxie41xx

"""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 general 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 the instrument. 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 automatically or manually. 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. Parameters ---------- imax (float): current range (in A). Returns ------- None. """ 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. Parameters ---------- vmax (float): voltage measure range (in V). Returns ------- None. """ 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 : float Stop value in volts or amps. dstep : float, 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 : float, optional Delay between steps, if dstep='None' than stime is the whole time for the sweep (=rising,falling time). The default is 0. typ : str, optional 'V-', 'I-' = Voltage / Current sourced, '-' changes direction, volts, amps, timestamp are sensed. The default is 'V'. stair : str, optional ('Lin','Log) = linear or log source. The default is 'Lin'. aperture_time : float, 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 : bool, optional not implemented yet. The default is True. Returns ------- stair_measure : list of dict list of dict with keys 'value' and 'delay' for each step, value is the voltage or current value for this step, delay is the time to wait after setting this step before the next step is set. """ 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 : str, could be 'VITS' V = Voltage I = current T = time S = status(compliance) The default is ''. Returns ------- numpy array 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 correctly.... 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()
[docs] def smu_fetch_settings(self, record_length=5_000_000, aperture_time=0.001): """ Set settings for smu_fetch(). Parameters ---------- record_length : int, optional Number of points to fetch. The default is 5_000_000. aperture_time : float, optional Aperture time for the measurements. The default is 0.001. Returns ------- None. """ 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
[docs] def fetch_data(self, npoints, measure="voltage"): """ Fetch data from the instrument after a software edge trigger. Parameters ---------- npoints : int Number of points to fetch. measure : str, optional Type of measurement to fetch: "voltage", "current", or "both". The default is "voltage". Returns ------- time_pts : list List of time points corresponding to the measurements. voltage : list, optional List of voltage measurements (in V), returned if measure is "voltage" or "both". current : list, optional List of current measurements (in mA), returned if measure is "current" or "both". """ 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
[docs] def plot_smu_data(self, time, voltage=None, current=None): """ Plot the fetched SMU data. Parameters ---------- time : list List of time points corresponding to the measurements. voltage : list, optional List of voltage measurements (in V). The default is None. current : list, optional List of current measurements (in mA). The default is None. Returns ------- None. Displays a plot of the voltage and/or current measurements over time. """ 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()