# keithley2400

Simple, clean Python library for controlling the **Keithley 2400 Source Measure Unit (SMU)** over RS-232 / USB-serial.

## Features

- Full V/I/R sourcing and measurement control via SCPI
- Pure-Python — no National Instruments VISA required
- Works natively on **macOS (including Apple Silicon M1/M2/M3/M4)**, Linux, and Windows
- Statistical sampling with mean and standard deviation
- Generator-based voltage sweep (`sweep_iv`) for IV curves
- One-liner convenience function: `quick_iv_sweep()`
- Context manager support for safe resource cleanup
- Serial port discovery helper: `find_serial_port()`
- Automatic Keithley 2400 port discovery via `*IDN?` probing when `resource=None` or when a stale configured port fails

## Installation

```bash
pip install keithley2400
```

## Requirements

- Python 3.6+
- `pyvisa >= 1.11.0`
- `pyvisa-py >= 0.5.2`
- `pyserial >= 3.4`

## Hardware Setup

The Keithley 2400 communicates over **RS-232 using a USB-serial adapter**.

1. Connect the USB-serial cable between the instrument's RS-232 port and your computer.
2. Ensure the instrument's baud rate matches (factory default: **9600 baud, 8N1**).
3. Find your port:

```python
from keithley2400 import find_serial_port
ports = find_serial_port()
# Example output: ['ASRL/dev/cu.usbserial-FTRTKC3X::INSTR']
```

## Quick Start

### 1. Connect automatically (recommended)

```python
from keithley2400 import Keithley2400

with Keithley2400(resource=None) as k:   # auto-discovers by probing *IDN?
    print(k.idn())
```

### 2. Find your serial port manually

```python
from keithley2400 import find_serial_port
print(find_serial_port())
```

### 3. Run an IV Sweep (one-liner)

```python
from keithley2400 import quick_iv_sweep

voltages = [v * 0.5 for v in range(0, 21)]  # 0 to 10 V in 0.5 V steps

csv_file = quick_iv_sweep(
    voltages=voltages,
    out_csv="iv_curve.csv",
    compliance_amps=0.01,    # 10 mA current limit
    stabilize_s=0.5,         # wait 500 ms after each voltage step
    n_samples=50,            # average 50 readings per point
)
print(f"Saved: {csv_file}")
```

### 4. Full Manual Control

```python
from keithley2400 import Keithley2400

# Using context manager (automatically turns output off and closes on exit)
with Keithley2400(resource="ASRL/dev/cu.usbserial-FTRTKC3X::INSTR") as k:
    print(k.idn())

    k.set_source_v(5.0)
    k.set_current_compliance(0.01)  # 10 mA limit
    k.configure_measure_vi()
    k.output_on()

    v, i = k.read_v_i()
    print(f"V={v:.4f} V  I={i:.6e} A")

    # Statistical sampling
    stats = k.sample_stats(param="I", n=100, delay_s=0.01, stabilize_s=0.5)
    print(f"I mean={stats['mean']:.6e} A  stdev={stats['stdev']:.2e} A")
```

### 5. Resistance Measurement

```python
from keithley2400 import Keithley2400

with Keithley2400() as k:
    # 2-wire resistance
    k.configure_measure_ohms(method="2W")
    k.set_ohms_source_current(1e-3)  # 1 mA test current
    k.output_on()
    r = k.read_r(mode="ohms")
    print(f"R = {r:.2f} Ohm")
```

### 6. Custom IV Sweep with Generator

```python
from keithley2400 import Keithley2400
import csv

voltages = [round(v * 0.1, 2) for v in range(0, 101)]  # 0 to 10 V

with Keithley2400() as k:
    k.output_on()
    with open("iv_data.csv", "w", newline="") as f:
        writer = csv.writer(f)
        writer.writerow(["V_set", "I_mean_A", "I_stdev_A"])
        for v, i_mean, i_stdev in k.sweep_iv(voltages, compliance_amps=0.05):
            writer.writerow([v, i_mean, i_stdev])
            print(f"V={v:.2f}  I={i_mean:.4e} A")
```

## API Reference

### `Keithley2400` Class

#### `__init__(resource, backend="@py", settings=None)`
Open connection to the instrument.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `resource` | str or None | `None` | PyVISA resource string. `None` or `"auto"` triggers auto-discovery by probing serial ports with `*IDN?`. |
| `backend` | str | `"@py"` | PyVISA backend (`"@py"` = pyvisa-py, no NI-VISA needed) |
| `settings` | SerialSettings | None | RS-232 parameters (default: 9600 8N1) |

#### Output Control
| Method | Description |
|--------|-------------|
| `output_on()` | Enable source output |
| `output_off()` | Disable source output |

#### Source Configuration
| Method | Description |
|--------|-------------|
| `set_source_v(volts)` | Source a voltage |
| `set_source_i(amps)` | Source a current |
| `set_current_compliance(amps)` | Set current limit (when sourcing V) |
| `set_voltage_compliance(volts)` | Set voltage limit (when sourcing I) |

#### Measurement Configuration
| Method | Description |
|--------|-------------|
| `configure_measure_vi(auto_range=True)` | Measure V and I simultaneously |
| `configure_measure_ohms(method="2W")` | Resistance measurement (2W or 4W) |
| `set_current_range(amps)` | Manual current measurement range |

#### Readings
| Method | Returns | Description |
|--------|---------|-------------|
| `read_v_i()` | `(float, float)` | Voltage and current |
| `read_v()` | `float` | Voltage only |
| `read_i()` | `float` | Current only |
| `read_r(mode="calc")` | `float` | Resistance (R=V/I or instrument ohms) |

#### Statistical Sampling
| Method | Returns | Description |
|--------|---------|-------------|
| `sample_stats(param, n=50, delay_s=0.01, stabilize_s=0.0)` | `dict` | Mean and stdev over N readings |

Returns: `{"mean": float, "stdev": float, "n": float}`

#### IV Sweep
| Method | Returns | Description |
|--------|---------|-------------|
| `sweep_iv(voltages, ...)` | `Iterator[(V, I_mean, I_stdev)]` | Voltage sweep generator |

#### Utilities
| Method | Returns | Description |
|--------|---------|-------------|
| `idn()` | `str` | Instrument identification string |
| `get_error()` | `str` | Error queue query |
| `close()` | — | Close VISA session |

### `SerialSettings` Dataclass

| Field | Default | Description |
|-------|---------|-------------|
| `baud_rate` | `9600` | Communication speed |
| `data_bits` | `8` | Data bits per character |
| `parity` | `none` | Parity check |
| `stop_bits` | `one` | Stop bits |
| `terminator` | `"\r"` | Line terminator (CR confirmed working) |
| `timeout_ms` | `5000` | Read timeout in milliseconds |

### Convenience Functions

#### `find_serial_port(backend="@py") -> list`
List all detected USB-serial ports.

#### `find_keithley_resource(backend="@py", settings=None, verbose=True) -> Optional[str]`
Probe detected serial ports with `*IDN?` and return the first one that identifies as a Keithley 2400.

#### `quick_iv_sweep(voltages, out_csv=None, resource=..., ...) -> Optional[str]`
One-liner: connect → sweep → save CSV → disconnect. Returns the CSV path.

## CSV Output Format

```csv
voltage_V,current_mean_A,current_stdev_A,n_samples
0.0,+1.234567e-09,2.345e-11,50
0.5,+5.678901e-09,3.456e-11,50
1.0,+1.234567e-08,4.567e-11,50
...
```

## Notes

- Always call `output_off()` before `close()` (the context manager does this automatically)
- `FORM:ELEM VOLT,CURR` is set in `initialize()` — this makes `read_v_i()` parsing deterministic
- If you switch to `configure_measure_ohms()`, FORM:ELEM changes to RES; call `configure_measure_vi()` again to switch back
- If a configured serial port goes stale, `Keithley2400(..., auto_discover=True)` will scan candidate serial ports and accept only one whose `*IDN?` response looks like a Keithley 2400
- The `@py` backend works without a NI-VISA installation — fully compatible with Apple Silicon

## License

MIT License
