PyVISA
PyVISADocs

Instrument Control Guide

Python instrument control with PyVISA. Oscilloscopes, multimeters, signal generators, power supplies, and network analyzers with examples and best practices.

Control measurement instruments with Python. From voltage readings to automated test sequences.

Why Python for Instrument Control?

Python has become the de facto standard for instrument control and test automation because of:

Python Advantages

  • Easy to learn: Readable syntax, extensive documentation
  • Rich ecosystem: NumPy, Matplotlib, Pandas for data analysis
  • Cross-platform: Works on Windows, Linux, macOS
  • Open source: No licensing costs
  • Huge community: Extensive support and examples

PyVISA Benefits

  • Universal compatibility: Works with any VISA-compatible instrument
  • Multiple interfaces: USB, Ethernet, Serial, GPIB
  • Industry standard: Based on IVI VISA specification
  • Vendor agnostic: Control instruments from any manufacturer

Quick Start: Your First Instrument Control

import pyvisa

# Windows: Use NI-VISA backend
rm = pyvisa.ResourceManager()
# Or specify DLL path explicitly
# rm = pyvisa.ResourceManager("C:/Windows/System32/visa32.dll")

instruments = rm.list_resources()
print(f"Found instruments: {instruments}")

# Open connection to first instrument
if instruments:
    inst = rm.open_resource(instruments[0])
    device_info = inst.query("*IDN?")
    print(f"Connected to: {device_info}")
    inst.close()
rm.close()
import pyvisa

# Linux: May need to specify library path
try:
    rm = pyvisa.ResourceManager()
except:
    # Try with explicit path
    rm = pyvisa.ResourceManager('@py')
    # Or specify library path
    # rm = pyvisa.ResourceManager('/usr/lib/x86_64-linux-gnu/libvisa.so')

instruments = rm.list_resources()
print(f"Found instruments: {instruments}")

# Open connection to first instrument
if instruments:
    inst = rm.open_resource(instruments[0])
    device_info = inst.query("*IDN?")
    print(f"Connected to: {device_info}")
    inst.close()
rm.close()
import pyvisa

# macOS: Usually works with PyVISA-py backend
rm = pyvisa.ResourceManager('@py')
# Or use NI-VISA if installed
# rm = pyvisa.ResourceManager()

instruments = rm.list_resources()
print(f"Found instruments: {instruments}")

# Open connection to first instrument
if instruments:
    inst = rm.open_resource(instruments[0])
    device_info = inst.query("*IDN?")
    print(f"Connected to: {device_info}")
    inst.close()
rm.close()

Quick Success

That's it! You're now controlling instruments with Python.

Instrument Categories and Applications

Measurement Instruments

Digital Multimeters (DMM)

Perfect for voltage, current, and resistance measurements.

import pyvisa
import time

# Connect to DMM
rm = pyvisa.ResourceManager()
dmm = rm.open_resource("USB0::0x2A8D::0x0101::MY53220001::INSTR")

# High-precision voltage measurement
dmm.write("CONF:VOLT:DC")
dmm.write("VOLT:DC:NPLC 10")  # High accuracy mode
voltage = float(dmm.query("READ?"))

print(f"Measured voltage: {voltage:.6f} V")
dmm.close()

Applications:

  • Battery testing and monitoring
  • Component characterization
  • Quality control measurements
  • Environmental monitoring

Oscilloscopes

Essential for waveform analysis and signal debugging.

# Connect to oscilloscope
scope = rm.open_resource("USB0::0x0699::0x0363::C065089::INSTR")

# Configure for signal capture
scope.write("CH1:SCALE 1.0")      # 1V/div
scope.write("HOR:SCALE 1E-3")     # 1ms/div
scope.write("TRIG:MAIN:LEV 0.5")  # 500mV trigger

# Capture waveform
scope.write("SING")  # Single acquisition
scope.query("*OPC?")  # Wait for completion

# Get measurement
frequency = float(scope.query("MEASU:FREQ?"))
amplitude = float(scope.query("MEASU:PK2PK?"))

print(f"Signal: {frequency:.0f} Hz, {amplitude:.3f} Vpp")

Applications:

  • Circuit debugging
  • Signal integrity analysis
  • Protocol decoding
  • EMI/EMC testing

Source Instruments

Power Supplies

Control voltage and current for device testing.

# Connect to power supply  
psu = rm.open_resource("ASRL/dev/ttyUSB0::INSTR", baud_rate=115200)

# Configure output
psu.write("VOLT 12.0")    # Set 12V
psu.write("CURR 2.0")     # Limit to 2A  
psu.write("OUTP ON")      # Enable output

# Monitor load
voltage = float(psu.query("MEAS:VOLT?"))
current = float(psu.query("MEAS:CURR?"))
power = voltage * current

print(f"Load: {voltage:.2f}V, {current:.3f}A, {power:.2f}W")

Signal Generators

Generate test signals for device validation.

# Connect to signal generator
sig_gen = rm.open_resource("USB0::0x2A8D::0x0001::MY52345678::INSTR")

# Generate sine wave
sig_gen.write("SOUR:FUNC SIN")
sig_gen.write("SOUR:FREQ 1000")     # 1 kHz
sig_gen.write("SOUR:VOLT 2.0")      # 2 Vpp
sig_gen.write("OUTP ON")            # Enable output

print("Generating 1 kHz sine wave, 2 Vpp")

Network Instruments

Network Analyzers

Measure frequency response and impedance.

# Connect to network analyzer
na = rm.open_resource("TCPIP0::192.168.1.100::5025::SOCKET")

# Configure S-parameter measurement
na.write("CALC:PAR:DEF 'S11',S11")
na.write("SENS:FREQ:START 1E6")    # 1 MHz start  
na.write("SENS:FREQ:STOP 1E9")     # 1 GHz stop
na.write("SENS:SWE:POIN 201")      # 201 points

# Perform measurement
na.write("INIT:IMM")
na.query("*OPC?")

# Get data
data = na.query("CALC:DATA:FDATA?")
s11_data = [float(x) for x in data.split(',')]

print(f"S11 measurement complete: {len(s11_data)} points")

Advanced Python Control Techniques

Data Logging and Analysis

import pandas as pd
import matplotlib.pyplot as plt
from datetime import datetime

class InstrumentLogger:
    """Professional instrument data logging"""
    
    def __init__(self, instrument, log_file="data.csv"):
        self.instrument = instrument
        self.log_file = log_file
        self.data = []
    
    def log_measurement(self, measurement_cmd="MEAS:VOLT:DC?"):
        """Log single measurement with timestamp"""
        timestamp = datetime.now()
        value = float(self.instrument.query(measurement_cmd))
        
        self.data.append({
            'timestamp': timestamp,
            'value': value
        })
        
        return timestamp, value
    
    def continuous_logging(self, duration_minutes=60, interval_seconds=10):
        """Log data continuously"""
        import time
        
        end_time = time.time() + (duration_minutes * 60)
        
        print(f"Starting {duration_minutes} minute data logging...")
        
        while time.time() < end_time:
            timestamp, value = self.log_measurement()
            print(f"{timestamp.strftime('%H:%M:%S')}: {value:.6f}")
            time.sleep(interval_seconds)
        
        self.save_data()
    
    def save_data(self):
        """Save logged data to CSV"""
        df = pd.DataFrame(self.data)
        df.to_csv(self.log_file, index=False)
        print(f"Data saved to {self.log_file}")
    
    def plot_data(self):
        """Create time series plot"""
        df = pd.DataFrame(self.data)
        plt.figure(figsize=(12, 6))
        plt.plot(df['timestamp'], df['value'])
        plt.title('Instrument Measurements Over Time')
        plt.xlabel('Time')
        plt.ylabel('Measurement')
        plt.grid(True)
        plt.xticks(rotation=45)
        plt.tight_layout()
        plt.show()

# Example usage
dmm = rm.open_resource("USB0::0x2A8D::0x0101::MY53220001::INSTR")
logger = InstrumentLogger(dmm, "battery_test.csv")

# Log for 10 minutes, one reading per 30 seconds
logger.continuous_logging(duration_minutes=10, interval_seconds=30)
logger.plot_data()

Multi-Instrument Coordination

class TestSequenceController:
    """Coordinate multiple instruments for complex tests"""
    
    def __init__(self):
        self.instruments = {}
        self.rm = pyvisa.ResourceManager()
    
    def add_instrument(self, name, resource_string, **config):
        """Add instrument to test setup"""
        inst = self.rm.open_resource(resource_string)
        
        # Apply configuration
        if 'timeout' in config:
            inst.timeout = config['timeout']
        if 'termination' in config:
            inst.write_termination = config['termination']
            inst.read_termination = config['termination']
        
        self.instruments[name] = inst
        print(f"Added {name}: {inst.query('*IDN?').strip()}")
    
    def device_under_test_sequence(self):
        """Example: Device characterization sequence"""
        
        # Step 1: Apply power
        psu = self.instruments['power_supply']
        psu.write("VOLT 5.0")
        psu.write("CURR 1.0") 
        psu.write("OUTP ON")
        time.sleep(1)  # Settling time
        
        # Step 2: Generate test signal
        sig_gen = self.instruments['signal_generator']
        sig_gen.write("SOUR:FUNC SIN")
        sig_gen.write("SOUR:FREQ 1000")
        sig_gen.write("SOUR:VOLT 1.0")
        sig_gen.write("OUTP ON")
        time.sleep(0.5)
        
        # Step 3: Measure response
        scope = self.instruments['oscilloscope']
        scope.write("SING")
        scope.query("*OPC?")
        
        frequency = float(scope.query("MEASU:FREQ?"))
        amplitude = float(scope.query("MEASU:PK2PK?"))
        
        # Step 4: Measure power consumption
        supply_voltage = float(psu.query("MEAS:VOLT?"))
        supply_current = float(psu.query("MEAS:CURR?"))
        power_consumption = supply_voltage * supply_current
        
        # Step 5: Cleanup
        sig_gen.write("OUTP OFF")
        psu.write("OUTP OFF")
        
        results = {
            'input_frequency': 1000,
            'output_frequency': frequency,
            'output_amplitude': amplitude,
            'power_consumption': power_consumption,
            'test_passed': abs(frequency - 1000) < 10  # 10 Hz tolerance
        }
        
        return results
    
    def close_all(self):
        """Clean up all instruments"""
        for name, inst in self.instruments.items():
            try:
                inst.close()
            except:
                pass
        self.rm.close()

# Example usage
controller = TestSequenceController()
controller.add_instrument('power_supply', 'ASRL/dev/ttyUSB0::INSTR', 
                         timeout=5000, termination='\n')
controller.add_instrument('signal_generator', 'USB0::0x2A8D::0x0001::MY52345678::INSTR')
controller.add_instrument('oscilloscope', 'USB0::0x0699::0x0363::C065089::INSTR')

# Run test sequence
test_results = controller.device_under_test_sequence()
print(f"Test results: {test_results}")

controller.close_all()

Integration with Scientific Python Stack

Data Analysis with NumPy and Pandas

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy import signal

def analyze_waveform_data(scope_resource):
    """Complete waveform analysis example"""
    
    scope = rm.open_resource(scope_resource)
    
    # Configure scope for data acquisition
    scope.write("DATA:ENCDG RIBINARY")  # Binary for speed
    scope.write("DATA:WIDTH 2")         # 16-bit data
    scope.write("WFMO:YMULT?")
    y_mult = float(scope.read())
    
    # Acquire waveform
    scope.write("SING")
    scope.query("*OPC?")
    scope.write("CURVE?")
    
    # Get binary data
    raw_data = scope.read_raw()
    
    # Convert to numpy array (skip IEEE header)
    waveform = np.frombuffer(raw_data[2:], dtype=np.int16) * y_mult
    
    # Create time array
    sample_rate = 1e6  # 1 MSa/s (get from instrument)
    time_array = np.arange(len(waveform)) / sample_rate
    
    # Analyze waveform
    analysis = {
        'mean': np.mean(waveform),
        'std': np.std(waveform),
        'pk_pk': np.max(waveform) - np.min(waveform),
        'rms': np.sqrt(np.mean(waveform**2))
    }
    
    # Frequency domain analysis
    freqs, power_spectrum = signal.welch(waveform, sample_rate)
    fundamental_freq = freqs[np.argmax(power_spectrum[1:])]  # Skip DC
    
    analysis['fundamental_frequency'] = fundamental_freq
    
    # Create comprehensive plot
    fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8))
    
    # Time domain
    ax1.plot(time_array * 1000, waveform)  # Convert to ms
    ax1.set_xlabel('Time (ms)')
    ax1.set_ylabel('Voltage (V)')
    ax1.set_title('Waveform - Time Domain')
    ax1.grid(True)
    
    # Frequency domain
    ax2.loglog(freqs, power_spectrum)
    ax2.set_xlabel('Frequency (Hz)')
    ax2.set_ylabel('Power Spectral Density')
    ax2.set_title('Waveform - Frequency Domain')
    ax2.grid(True)
    
    plt.tight_layout()
    plt.show()
    
    scope.close()
    return analysis, waveform, time_array

# Example usage
# analysis = analyze_waveform_data("USB0::0x0699::0x0363::C065089::INSTR")
# print(f"Waveform analysis: {analysis}")

Machine Learning Integration

from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
import joblib

class InstrumentCalibration:
    """ML-powered instrument calibration"""
    
    def __init__(self, reference_instrument, dut_instrument):
        self.reference = reference_instrument
        self.dut = dut_instrument
        self.calibration_model = None
    
    def collect_calibration_data(self, test_points=50):
        """Collect calibration data points"""
        
        reference_readings = []
        dut_readings = []
        
        # Generate test voltages (example for DMM calibration)
        test_voltages = np.linspace(0, 10, test_points)
        
        for voltage in test_voltages:
            # Set reference voltage (assuming programmable reference)
            # In practice, you'd use a precision calibrator
            
            # Read both instruments
            ref_reading = float(self.reference.query("MEAS:VOLT:DC?"))
            dut_reading = float(self.dut.query("MEAS:VOLT:DC?"))
            
            reference_readings.append(ref_reading)
            dut_readings.append(dut_reading)
            
            time.sleep(0.1)  # Settling time
        
        return np.array(reference_readings), np.array(dut_readings)
    
    def train_calibration_model(self, degree=2):
        """Train polynomial calibration model"""
        
        ref_data, dut_data = self.collect_calibration_data()
        
        # Use polynomial features for non-linear calibration
        poly_features = PolynomialFeatures(degree=degree)
        dut_poly = poly_features.fit_transform(dut_data.reshape(-1, 1))
        
        # Train linear regression on polynomial features
        model = LinearRegression()
        model.fit(dut_poly, ref_data)
        
        # Store model and preprocessing
        self.calibration_model = {
            'model': model,
            'poly_features': poly_features,
            'degree': degree
        }
        
        # Calculate calibration accuracy
        predictions = self.apply_calibration(dut_data)
        accuracy = np.mean(np.abs(predictions - ref_data))
        
        print(f"Calibration model trained (degree {degree})")
        print(f"Mean absolute error: {accuracy:.6f}")
        
        return accuracy
    
    def apply_calibration(self, raw_readings):
        """Apply calibration to raw readings"""
        
        if self.calibration_model is None:
            raise Exception("Calibration model not trained")
        
        model = self.calibration_model['model']
        poly_features = self.calibration_model['poly_features']
        
        # Ensure raw_readings is numpy array
        if not isinstance(raw_readings, np.ndarray):
            raw_readings = np.array([raw_readings])
        
        # Apply polynomial transformation
        raw_poly = poly_features.transform(raw_readings.reshape(-1, 1))
        
        # Get calibrated values
        calibrated = model.predict(raw_poly)
        
        return calibrated
    
    def save_calibration(self, filename):
        """Save calibration model"""
        joblib.dump(self.calibration_model, filename)
        print(f"Calibration saved to {filename}")
    
    def load_calibration(self, filename):
        """Load calibration model"""
        self.calibration_model = joblib.load(filename)
        print(f"Calibration loaded from {filename}")

# Example usage
# reference_dmm = rm.open_resource("USB0::0x2A8D::0x0101::REF123456::INSTR")
# test_dmm = rm.open_resource("USB0::0x2A8D::0x0101::DUT789012::INSTR")
# 
# calibrator = InstrumentCalibration(reference_dmm, test_dmm)
# calibrator.train_calibration_model(degree=3)
# calibrator.save_calibration("dmm_calibration.pkl")

Python vs Other Instrument Control Options

Python vs LabVIEW

AspectPython + PyVISALabVIEW
Learning CurveModerate (text-based)Steep (graphical)
CostFree$$$$ (expensive licenses)
FlexibilityHighMedium
Data AnalysisExcellent (NumPy, SciPy)Good
Version ControlExcellent (text files)Poor (binary files)
CommunityHugeSmaller
PerformanceGoodExcellent

Python vs MATLAB

AspectPython + PyVISAMATLAB
CostFree$$$ (expensive)
Learning CurveModerateEasy
LibrariesExtensiveExcellent
Industry AdoptionGrowing rapidlyEstablished
LicensingOpen sourceProprietary

Migration from LabVIEW to Python

# LabVIEW-style error handling in Python
def labview_style_function():
    """Python function with LabVIEW-style error handling"""
    error_code = 0
    error_message = ""
    result = None
    
    try:
        # Your instrument code here
        rm = pyvisa.ResourceManager()
        inst = rm.open_resource("USB0::0x2A8D::0x0101::MY53220001::INSTR")
        result = float(inst.query("MEAS:VOLT:DC?"))
        inst.close()
        
    except Exception as e:
        error_code = -1
        error_message = str(e)
    
    return result, error_code, error_message

# Usage
voltage, err_code, err_msg = labview_style_function()
if err_code == 0:
    print(f"Measurement: {voltage} V")
else:
    print(f"Error {err_code}: {err_msg}")

Best Practices for Python Instrument Control

Do This:

Best Practice

Always properly manage resources to prevent connection leaks and ensure clean shutdown.

Resource Management:

# Always use try/finally or context managers
try:
    rm = pyvisa.ResourceManager()
    inst = rm.open_resource("USB0::0x1234::0x5678::SN::INSTR")
    # Your code here
finally:
    inst.close()
    rm.close()

# Or better yet, use context managers
with pyvisa.ResourceManager() as rm:
    with rm.open_resource("USB0::0x1234::0x5678::SN::INSTR") as inst:
        # Your code here
        pass  # Resources automatically closed

Error Handling:

from pyvisa import VisaIOError

try:
    inst.write("*IDN?")
    response = inst.read()
except VisaIOError as e:
    if e.error_code == -1073807339:  # Timeout
        print("Instrument not responding")
    elif e.error_code == -1073807343:  # Invalid resource
        print("Device not found")
    else:
        print(f"VISA error: {e}")
except Exception as e:
    print(f"Unexpected error: {e}")

Avoid This:

Common Pitfalls

These mistakes can lead to performance issues, connection problems, and unreliable measurements.

  • Opening/closing connections repeatedly in loops
  • Using string formatting for instrument commands without validation
  • Ignoring timeout settings
  • Not checking instrument errors
  • Hardcoding resource strings (use configuration files)

Performance Tips:

# Optimize for speed
instrument.chunk_size = 1024 * 1024  # Large buffer for data transfers
instrument.timeout = 30000  # Appropriate timeout

# Use binary data format for large datasets
instrument.write("DATA:ENCDG RIBINARY")

# Batch operations when possible
commands = ["CONF:VOLT:DC", "SAMP:COUN 100", "READ?"]
for cmd in commands[:-1]:
    instrument.write(cmd)
data = instrument.query(commands[-1])

Learning Path and Next Steps

Beginner Path

  1. Start simple: Connect to one instrument, read basic measurements
  2. Learn SCPI: Understand Standard Commands for Programmable Instruments
  3. Error handling: Implement proper exception handling
  4. Data logging: Create simple CSV logging

Intermediate Path

  1. Multi-instrument control: Coordinate multiple devices
  2. Data analysis: Integrate NumPy, Matplotlib
  3. Performance optimization: Fast data acquisition
  4. GUI development: Create user interfaces with tkinter or PyQt

Advanced Path

  1. Test automation frameworks: pytest, unittest integration
  2. Database integration: Store results in databases
  3. Web interfaces: Flask/Django for remote control
  4. Machine learning: Automated analysis and calibration

Common Applications

Manufacturing Test

  • Automated production testing
  • Statistical process control
  • Yield analysis and optimization
  • Calibration systems

Research and Development

  • Device characterization
  • Experimental data collection
  • Prototype validation
  • Publication-quality plots

Service and Maintenance

  • Instrument calibration
  • Performance verification
  • Troubleshooting automation
  • Predictive maintenance

Next Steps

Ready to dive deeper? Explore these specialized guides:

Start your Python instrument control journey today! With PyVISA and Python, you have access to the most powerful and flexible instrument control platform available.

How is this guide?