Source code for cohesivm.devices.agilent.Agilent4156C

"""Implements the Agilent 4156C Precision Semiconductor Parameter Analyzer.

Requires the PyVISA package: https://pypi.org/project/PyVISA/"""
from __future__ import annotations
import pyvisa
from abc import ABC
from typing import List, Any, TypeVar
from cohesivm.devices import Device
from cohesivm.channels import Channel, SweepVoltageSMU

TChannel = TypeVar('TChannel', bound='Agilent4156CChannel')


[docs] class Agilent4156CChannel(Channel, ABC): """Abstract base class which implements the properties and methods which all Agilent 4156C channels have in common.""" @property def identifier_list(self) -> list[str]: return self.identifier.split(',') def _write(self, command: str) -> None: """Sends an ASCII command to the device. :param command: ASCII command string which is sent to the device. """ self.connection.write(command) def _query(self, command: str) -> str: """Sends an ASCII command to the device and returns the response. :param command: ASCII command string which is sent to the device. :returns: The response string, which should be in ASCII format if the device is set up correctly. """ return self.connection.query(command)
[docs] def wait_for_completion(self) -> None: """Sends an ASCII query to check if the operation on the device is finished. Waits for the response with an increased timeout.""" old_timeout = self.connection.timeout self.connection.timeout = 1000000 self.connection.query('*OPC?') self.connection.timeout = old_timeout
[docs] def set_property(self, name: str, value: Any = None) -> None: if value is None: self._write(name) else: self._write(f'{name} {value}')
[docs] def get_property(self, name: str) -> Any: return self._query(f'{name}?')
[docs] def get_key_property(self, name: str, key: str) -> Any: """Retrieves the value of a property which is further specified by the `key` argument.""" return self._query(f'{name}? {key}')
[docs] def disable(self) -> None: for identifier in self.identifier_list: self.set_property(f':PAGE:CHAN:{identifier}:DIS')
[docs] class SweepVoltageSMUChannel(Agilent4156CChannel, SweepVoltageSMU): """Two source monitor units of the Agilent 4156C configured to work as one Sweep Voltage SMU channel. For more details and specifications see the user manual of the Agilent 4156C. :param force_smu: String identifier of the SMU channel which is used as voltage source: 'SMU1', 'SMU2', 'SMU3' or 'SMU4'. :param com_smu: String identifier of the SMU channel which is used as common connection: 'SMU1', 'SMU2', 'SMU3' or 'SMU4'. :param s_compliance: Float value for the current compliance in A, i.e., the maximum allowed current. Determines the voltage output range: maximum voltage is 20, 40, 100 V for 0.1, 0.05, 0.02 A compliance, respectively. The minimum value is 0.1 pA. :param s_int_time: String value for the integration time which can be one of 'SHORT', 'MEDIUM', 'LONG'. :param s_delay: Float value for the time in s between setting a voltage step and running a current measurement. :param s_hold_time: Float value for the time in s before the sweep is started. :raises ValueError: If the identifier is not available or if a setting value is not valid. :raises TypeError: If a setting type is not supported. """ def __init__(self, force_smu: str = 'SMU1', com_smu: str = 'SMU2', s_compliance: float = 0.05, s_int_time: str = 'MEDIUM', s_delay: float = 0.0, s_hold_time: float = 0.0) -> None: if force_smu == com_smu: raise ValueError("The `force_smu` must not be the same as the `com_smu`!") if not {force_smu, com_smu} <= {'SMU1', 'SMU2', 'SMU3', 'SMU4'}: raise ValueError("Identifier of the SMU channels must be one of 'SMU1', 'SMU2', 'SMU3' or 'SMU4'!") self._force_smu = force_smu self._com_smu = com_smu self._identifier = f'{force_smu},{com_smu}' self._commands = { 'int_time': ':PAGE:MEAS:MSET:ITIM:MODE', 'compliance': ':PAGE:MEAS:SWE:VAR1:COMP', 'delay': ':PAGE:MEAS:SWE:DEL', 'hold_time': ':PAGE:MEAS:SWE:HTIM' } self._settings = { self._commands['int_time']: s_int_time, self._commands['compliance']: s_compliance, self._commands['delay']: s_delay, self._commands['hold_time']: s_hold_time } self._max_voltage = 100 Agilent4156CChannel.__init__(self, self._identifier, self._settings) def _check_settings(self) -> None: if self._settings[self._commands['int_time']] not in ['SHORT', 'MEDIUM', 'LONG']: raise ValueError("Integration time (`int_time`) must be either 'SHORT', 'MEDIUM' or 'LONG'!") try: compliance = float(self._settings[self._commands['compliance']]) except ValueError: raise TypeError('Compliance setting cannot be cast to float!') if compliance < 0.1e-12 or compliance > 0.1: raise ValueError('Current compliance must be between 0.1 pA and 0.1 A!') if compliance > 0.02: if compliance > 0.05: self._max_voltage = 20 else: self._max_voltage = 40 else: self._max_voltage = 100 try: delay = float(self._settings[self._commands['delay']]) except ValueError: raise TypeError('Delay setting cannot be cast to float!') if delay < 0 or delay > 60: raise ValueError('Delay must be between 0 and 60 s!') try: hold_time = float(self._settings[self._commands['hold_time']]) except ValueError: raise TypeError('Hold time setting cannot be cast to float!') if hold_time < 0 or hold_time > 600: raise ValueError('Hold time must be between 0 and 600 s!')
[docs] def enable(self) -> None: self.set_property(':PAGE:CHAN:MODE', 'SWE') self.set_property(f':PAGE:CHAN:{self._force_smu}:FUNC', 'VAR1') self.set_property(f':PAGE:CHAN:{self._force_smu}:MODE', 'V') self.set_property(f':PAGE:CHAN:{self._force_smu}:INAM', f"'I{self._force_smu}'") self.set_property(f':PAGE:CHAN:{self._force_smu}:VNAM', f"'V{self._force_smu}'") self.set_property(f':PAGE:CHAN:{self._com_smu}:FUNC', 'CONS') self.set_property(f':PAGE:CHAN:{self._com_smu}:MODE', 'COMM') self.set_property(f':PAGE:CHAN:{self._com_smu}:INAM', f"'I{self._com_smu}'") self.set_property(f':PAGE:CHAN:{self._com_smu}:VNAM', f"'V{self._com_smu}'") self.set_property(f':PAGE:MEAS:MSET:{self._force_smu}:RANG:MODE', 'AUTO') self.set_property(':PAGE:MEAS:SWE:VAR1:SPAC', 'LIN') self.set_property(':FORM:DATA', 'ASC')
[docs] def sweep_voltage_and_measure(self, start_voltage: float, end_voltage: float, voltage_step: float, hysteresis: bool ) -> list[tuple[float, float]]: if abs(start_voltage) > self._max_voltage or abs(end_voltage) > self._max_voltage: raise ValueError(f'Voltage must not exceed maximum value of {self._max_voltage} V!') self.set_property(f':PAGE:MEAS:SWE:VAR1:MODE', 'DOUBLE' if hysteresis else 'SINGLE') self.set_property(':PAGE:MEAS:SWE:VAR1:STAR', f'{start_voltage:.6f}') self.set_property(':PAGE:MEAS:SWE:VAR1:STOP', f'{end_voltage:.6f}') self.set_property(':PAGE:MEAS:SWE:VAR1:STEP', f'{voltage_step:.6f}') self.set_property(':PAGE:SCON:SING') self.wait_for_completion() self.set_property(':DISP', f'ON') self.set_property(':PAGE:GLIS:GRAP') self.set_property(':PAGE:GLIS:SCAL:AUTO', 'ONCE') self.set_property(':DISP', f'OFF') voltage_list = self.get_key_property(':DATA', f"'V{self._force_smu}'").split(',') current_list = self.get_key_property(':DATA', f"'I{self._force_smu}'").split(',') return [(float(voltage), float(current)) for voltage, current in zip(voltage_list, current_list)]
[docs] class Agilent4156C(Device): """Implements the Agilent 4156C Precision Semiconductor Parameter Analyzer as a Device class which is a container for the channels and the device connection. For more details and specifications see the user manual of the Agilent 4156C. :param channels: List of channels which are subclasses of the :class:`Agilent4156CChannel`. The device consists of 4xSMU, 2xVSU, and 2xVMU channels. :param resource_name: The VISA string identifier of the device. :raises TypeError: If a channel is not a subclass of the Agilent4156CChannel class. :raises ValueError: If duplicate channels are provided. """ def __init__(self, channels: List[TChannel] = None, resource_name: str = '') -> None: if channels is None: channels = [SweepVoltageSMUChannel()] channel_identifiers = [] for channel in channels: if not isinstance(channel, Agilent4156CChannel): raise TypeError(f'Channel {channel} is not an Agilent4156CChannel!') channel_identifiers += channel.identifier_list if len(channel_identifiers) != len(set(channel_identifiers)): raise ValueError('Duplicate channels are not allowed!') self._resource_name = resource_name super().__init__(channels) @property def channels(self) -> List[TChannel]: return self._channels
[docs] def _establish_connection(self) -> pyvisa.resources.GPIBInstrument: rm = pyvisa.ResourceManager() res: pyvisa.resources.GPIBInstrument = rm.open_resource(self._resource_name) res.write('*RST;*CLS') res.write(':DISP OFF') res.write(':PAGE:CHAN:ALL:DIS') return res