Skip to content

Repository files navigation

YPL Lab Control GUI

This project is part of my work during my Summer 2026 internship at the Youngblood Photonics Lab (YPL), University of Pittsburgh.

A Windows PyQt6 application for controlling the lab's optical/DAQ hardware from one window: UEI PowerDNA analog output cards, a Moku:Go, a CoreDAQ USB optical power meter, a Santec TSL-550 tunable laser, a Newport CONEX-CC motor stage, an Emcore ITLA laser controller, and an HP/Agilent 8168F tunable laser — each on its own tab, each degrading gracefully (tab stays visible with a message instead of crashing the app) if its hardware library isn't installed or its instrument isn't connected.

Alongside the per-instrument tabs there are two cross-instrument measurement tabs: Dot Product, which drives the Moku and a UEI phase shifter together to compute an optical multiply-accumulate, and 2D Sweep, which maps one detector over a drive axis and a laser axis at once and draws the result as a heatmap.

New to the GUI? This README covers installation and configuration. For how to use it — per-tab procedures, laser/DAQ safety, common lab workflows, and data formats — read the User Guide.

Features

DAQ Control (UEI PowerDNA analog output cards)

  • Per-card analog output control (Dev0/Dev1/Dev2 — current or voltage mode), up to 32 channels
  • Per-pin live control: value spinbox + slider, custom nicknames, ramped (slew-rate-limited) or instant "Set", plus "Write All" / "Zero All" / "Set All To" (write every active pin to one value at once)
  • Automatic pin sweep (by step count or step size, adjustable dwell time) — pick any subset of pins to sweep the same Start→Stop range together in lockstep, with optional CoreDAQ optical-power logging at each step, a matplotlib results plot, and CSV export
  • Software waveform generator — sine/cosine on any subset of pins at once (all playing the same waveform together), with adjustable frequency, amplitude, offset, and update rate
  • Data recording (start/stop) with live commanded-vs-measured and V-I plots (measured via the Moku:Go oscilloscope), saved to CSV
  • Embedded Moku:Go oscilloscope panel
  • Combined/global recording across multiple sources into one CSV
  • Physical pin remap (PIN_REMAP in gui.py) for AO-333 cards whose connector/cabling doesn't wire in straight sequential order — every write path (Set, Write All, Set All To, ramp, sweep, wave) goes through the same correction. See pin_identify_test.py (below) to work out a card's actual mapping.

Moku (Moku:Go, via MultiInstrument mode — separate from the read-only oscilloscope panel embedded in the DAQ Control tab; only connect one of the two at a time against the same physical device)

  • Waveform Generator (Slot 2) + Oscilloscope (Slot 1) running at once, so a generated waveform can be watched live on the same plot via a loopback cable from an output back to an input
  • Per-output waveform controls: Sine/Square/Ramp/Pulse/DC/Off, frequency, amplitude, offset, phase, plus duty cycle (Square/Pulse) or symmetry (Ramp) — all persisted across sessions
  • Two plot views, switchable:
    • Scope trace (default) — plots the captured frame itself, so you see the actual waveform shape and amplitude. Selectable capture window (100 µs – 1 s); shorter windows acquire faster and are needed to resolve higher frequencies.
    • Rolling mean — one averaged point per frame over the last 10 s. Good for slow DC drift, but note a periodic waveform averages out to roughly its DC offset (a 1 kHz sine reads as a flat line).
  • ~30 Hz redraw decoupled from the network poll rate; repaints are skipped when no new frame has arrived, so the redraw timer costs nothing between round trips (measured ~0.4 ms per redraw). A "frames/s from device" readout shows the achieved acquisition rate, which is bounded by the Moku's network round trip, not by the GUI.
  • Note: on this Moku:Go, MultiInstrument slot 2 exposes only one output (Slot2OutA → Output1); Slot2OutB is rejected by the device with "Source port is not valid in the given configuration". The tab probes for the second output on connect and, if the device refuses it, disables the Output 2 controls with an explanation rather than failing to connect. If a firmware revision ever exposes it, the second column enables itself with no code change.

Dot Product (optical multiply-accumulate: Moku bit stream × UEI phase weights)

  • Computes a complex dot product on the bench: each element multiplies a random bit b (±1, played as a DC level on a Moku generator output) by a weight phase φ (held on one UEI AOut phase shifter), with the Moku scope input reading the detector
  • No ramping — the AOut is stepped by direct writes, since slew-rate limiting each element would dominate the step time and smear one element's phase into the next
  • Real and imaginary parts recovered by quadrature: every element is measured twice, at φ and at φ + 90°, because a detector reads power — one real number — so a complex product can't come from a single reading
  • Adjustable multiplication speed (step period per measurement, with the derived element rate shown; the achieved rate is reported after each run, since the floor is the Moku's network round trip)
  • Weight phases: random over [0, 2π), random ±1 (0 or π), or a manual list of degrees; fixable seed replays the exact same bit stream and weights
  • Thermal phase-shifter map (drive = V_π·√(φ/π), i.e. φ ∝ drive²) with a φ₀ bias for nulling the interferometer's own phase, and a warning when the chosen V_π would need more drive than the card can deliver
  • Live plot of the running dot product — measured real/imaginary against the expected curves, which are known up front and drawn immediately
  • Baseline (zero-product) reference taken before each run, plus a least-squares gain fit so the detector's volts and the dimensionless expected products are directly comparable; reports normalized error, correlation, drive clamps and stale frames
  • Four-panel matplotlib results plot (cumulative real, cumulative imaginary, per-element measured-vs-expected scatter, raw detector readings) and CSV export with the PNG saved alongside
  • Borrows the already-connected Moku (from the Moku tab) and DAQ card (from DAQ Control) rather than opening second sessions — connect both there first

2D Sweep (drive axis × laser axis heatmap)

  • Maps one detector over two swept axes at once: an X (fast) axis driving either a UEI AOut (ramped at the card's slew rate, same as the DAQ tab's 1D sweep) or a DC level on a Moku generator output, and a Y (slow) axis stepping laser wavelength or power on the Santec, HP-8168F, or ITLA
  • The laser is deliberately the outer axis — retuning (and, on the ITLA, relocking) costs orders of magnitude more than moving a DAC, so it steps once per row rather than once per point
  • Detector is any CoreDAQ head (all four are recorded to CSV regardless of which one is mapped) or either Moku scope input, with an adjustable number of readings averaged into each grid point
  • Optional serpentine (zig-zag) scan — alternate rows run backwards, so each row starts where the last one ended instead of jumping the drive back to Start
  • Live heatmap fills in as the sweep runs, with a per-point readout and a running time-remaining estimate
  • Matplotlib results heatmap with live-adjustable interpolation (nearest → bilinear/bicubic/gaussian/lanczos/…), colormap, and a log colour scale for detectors spanning decades. Interpolation is a control rather than a plot-time parameter on purpose: how much smoothing between measured points is honest is a judgement you can only make while looking at the data, so nearest (one flat cell per measured point, nothing invented) is the default
  • Stopping mid-run keeps the partial map — unmeasured points stay visibly blank instead of being coloured as if they held a real value
  • CSV export in both long form (one row per point, plus all four CoreDAQ heads) and matrix form (…_matrix.csv: laser rows × drive columns, for dropping straight into Excel/Origin as a surface), with the heatmap PNG saved alongside
  • Borrows every instrument from its own tab rather than reopening it — connect the drive source, the laser, and the detector in their own tabs first

CoreDAQ Power Meter

  • Connects over USB-serial (auto-detect or manual COM port)
  • Per-head (1–4) gain control for LINEAR frontends
  • Wavelength-corrected power readout per head
  • Live combined plot of all 4 heads over the last 30s, with a legend you can click to show/hide individual heads — opens automatically on connect

Santec Laser (TSL-550)

  • Connects via Prologix GPIB-to-USB
  • Manual wavelength/power set; "Output ON" also applies whatever wavelength/power is currently dialed in
  • Power sweep (Cal 2-DC) with CoreDAQ logging, a matplotlib results plot, and CSV export
  • Hardware-triggered Fast Sweep (continuous wavelength sweep, captured by CoreDAQ in free-run) with a clean white-background matplotlib results plot, saved as a PNG alongside its CSV export

CONEX Motor (Newport CONEX-CC / TRA12CC)

  • Independent X/Y axis control, each on its own COM port
  • Home, move absolute (with left/right nudge-by-that-distance buttons), and move relative
  • Hold-to-jog — press and hold to move continuously, release to stop
  • Velocity control and emergency stop
  • Diagnostics: state/position/velocity queries, travel limits, device identity, VISA resource listing

ITLA Laser (Emcore TTX)

  • Connects via Prologix GPIB
  • ITU-grid channel tuning or direct wavelength entry (with optional FTF sub-grid detuning)
  • Live wavelength/power retuning without an off/on power cycle
  • Wavelength sweep (grid-snapped) and power sweep, with CoreDAQ logging, a matplotlib results plot, and CSV export
  • Dither mode, plus diagnostics readback (temperature, fatal status, etc.)

HP-8168F Laser

  • Connects via Prologix GPIB
  • Manual wavelength/power set, output on/off
  • Wavelength sweep and power sweep with CoreDAQ logging, a matplotlib results plot, and CSV export

Across every tab

  • Drag a tab left/right to reorder it (order persists between sessions), or drag it vertically out of the bar to pop it into its own window
  • COM ports, GPIB addresses, and last-used wavelength/power are remembered automatically between runs
  • Every export enables a "📂 Open" button for that box's most recent file (files are not auto-launched on save)

Repository Layout

  • docs/USER_GUIDE.md — the day-to-day operating manual: per-tab procedures, lab safety, common workflows, data formats, troubleshooting.
  • code/UeiDaq_gui/
    • gui.py — current entry point. The unified, multi-tab GUI described above.
    • gui_no_remap.py — fallback mirror of gui.py with PIN_REMAP forced empty (pure identity mapping). Run this instead of gui.py if the Dev2 (AO-333) remap in progress needs to be set aside; otherwise identical.
    • pin_identify_test.py — standalone script (not launched by the GUI) that walks an AO-333 card's pins one at a time so you can work out its real PIN_REMAP. Optionally connects directly to the Guardian ADC (needs the 32-bit environment, see Optional: AO-333 Guardian ADC check) to auto-detect which physical channel each logical pin lands on (all 32 channels as of 2026-07-21); the multimeter is a fallback if that DLL is unreachable. Close the main GUI (or disconnect that card) before running it — two processes on the same channels will fight each other. (The GUI itself no longer has any live Guardian ADC readback — removed 2026-07-22 — this script's own connection is independent of that.)
    • hardware/ — one module per instrument (coredaq.py, itla.py, laser_hp_8168F.py, laser_tsl_550.py, plus a shared visa_module/ for GPIB-over-Prologix support).
    • connection_settings.json, ao_channel_names.json — auto-generated by the GUI itself (last-used COM ports/GPIB addresses/wavelengths, pin nicknames). Don't hand-edit; delete either to reset to defaults.
    • GUIcontroller.py, GUIControllerNew.py, GUIControllerOriginal.py, textController.py — earlier/reference versions, not actively maintained. Use gui.py.
  • code/UeiDaq_library/ — UEI's official wheels/examples/docs, bundled as a fallback (see Prerequisites — the primary source is the UEI Framework installer, not this folder).
  • data/ — where every sweep/recording CSV gets saved (auto-created, auto-opens the file after saving).
  • pyproject.toml / uv.lock — dependency manifest for the uv package manager (see Setup).

Prerequisites

Software:

  • Windows 10/11
  • Python 3.12 (pinned in .python-version / pyproject.toml)
  • uv — this project's package/environment manager. Install with:
    winget install --id=astral-sh.uv -e
  • UEI Framework software installed to its default location (C:\Program Files (x86)\UEI\Framework\... and ...\PowerDNA\...). This is UEI's own Windows installer (comes with the DAQ hardware / from UEI support) — it provides both the UeiDaq Python bindings uv sync links against and PDNALib.dll, which pin_identify_test.py's optional Guardian ADC check needs. Not something uv/pip can install for you.
  • (Optional) NI-DAQmx runtime — only needed for the Santec TSL-550 tab to actually talk to hardware (the nidaqmx Python package itself installs fine without it; you'll only hit this if you try to connect).
  • (Optional) Moku CLI installed to C:\Program Files\Liquid Instruments\Moku CLI\ — only needed for the Moku:Go oscilloscope panel inside the DAQ Control tab and the Moku tab.

Hardware / network, as applicable to what you're using:

  • A UEI DAQ cube reachable on the network (see CUBE_IP in Configuration)
  • A Prologix GPIB-USB adapter for the ITLA, Santec, and HP-8168F laser tabs (each just needs its GPIB address + the adapter's COM port)
  • CONEX-CC/TRA12CC controller and CoreDAQ power meter connect over USB-serial directly — no adapter needed

Setup

  1. Install uv (see above) if you don't already have it.

  2. Install the UEI Framework software if this machine hasn't had it installed before — get it from UEI / whoever set up the DAQ hardware. Confirm afterward that this file exists:

    C:\Program Files (x86)\UEI\Framework\Python\UeiDaq_np1-5.2.0-cp312-abi3-win_amd64.whl
    
  3. From the repo root, sync the environment:

    cd "path\to\GUI"
    uv sync

    This creates .venv and installs everything declared in pyproject.toml/uv.lock — PyQt6, numpy, the UEI bindings, pyqtgraph, matplotlib, pyserial, pyvisa/pyvisa-py, moku, and nidaqmx. One command, nothing to install by hand.

    If this folder lives inside OneDrive (as this one does), uv sync can intermittently fail with Access is denied while OneDrive has a file in .venv locked for syncing. This is harmless — it isn't a broken environment, just OneDrive racing the installer. Just re-run uv sync (usually 1–5 tries clears it). If it keeps happening, exclude .venv and .venv32 from OneDrive sync (OneDrive Settings → Account → Choose folders) — they're build artifacts, not something you need backed up.

  4. Run it:

    uv run code\UeiDaq_gui\gui.py

    (or activate .venv yourself and run python code\UeiDaq_gui\gui.py)

That's it for the software side — every tab should open. Tabs whose hardware library failed to import show a message explaining what's missing instead of a blank/crashed tab; if you see one after uv sync succeeded, something in step 2 or 3 didn't take (see Troubleshooting).

Optional: AO-333 Guardian ADC check

pin_identify_test.py's optional Guardian ADC cross-check (auto-detects which physical channel a driven pin actually lands on, instead of needing a multimeter for every pin) needs a separate 32-bit Python environment — the UEI wheel for that particular DLL path is 32-bit only. This is optional: without it, the script still drives pins for real, you just read the result on a multimeter instead of getting it auto-confirmed.

uv venv --python cpython-3.12.13-windows-x86-none .venv32
uv pip install --python .venv32\Scripts\python.exe numpy==1.26.4 pywin32 `
  "C:\Program Files (x86)\UEI\Framework\Python\UeiDaq_np1-5.2.0-cp312-abi3-win32.whl"

Run pin_identify_test.py with .venv32\Scripts\python.exe for the Guardian check to actually load; run it with your normal environment and GUARDIAN_READBACK just logs "unavailable" and falls back to the multimeter for every pin.

(2026-07-22: the GUI's own live Guardian ADC readback — and the ao333_bridge.py helper process it used to auto-launch — was removed entirely at user request. This section now only concerns pin_identify_test.py's independent, on-demand check.)

Usage

Once setup is done, day-to-day this is the only command you need, run from the repo root:

uv run code\UeiDaq_gui\gui.py

This opens one window sized to use most of your screen, with a tab per instrument (see Features above).

📖 For how to actually operate each tab — step-by-step procedures, safety notes, common lab workflows, and data formats — see the User Guide. This README covers installation and configuration; the User Guide is the day-to-day manual.

A few things worth knowing up front:

  • Only the CoreDAQ Power Meter auto-connects on launch. Every other tab waits for you to click Connect — auto-connecting lasers and motors on startup was deliberately removed, since it moves hardware before you've looked at the rig.
  • DAQ output values are never restored on launch, also deliberately: every pin starts at 0. Restoring a saved output into a rig that's been rewired since is how you damage a device. (Sweep ranges, set-points, and nicknames are remembered — just not live outputs.)
  • Drag a tab left/right to reorder it; drag it vertically out of the bar to pop it into its own window (useful for watching two instruments side by side). A popped-out window has a "⬅ Reattach to main window" button.
  • Closing the main window disconnects and cleans up every instrument, even ones currently popped out into their own windows. Before disconnecting, it zeros every DAQ output pin and turns off Santec/HP-8168F laser emission — a manual Disconnect click does the same for whichever instrument you clicked it on. The ITLA is deliberately left emitting on exit (use its own Off button); everything else is shut down. Killing the process from Task Manager skips all of that and leaves outputs live.

Configuration

Hardware-specific constants live at the top of code/UeiDaq_gui/gui.py and need to match your actual setup:

  • CUBE_IP — IP address of the UEI DAQ cube
  • MOKU_IP — IP address of the Moku:Go, if used
  • CARDS — which device slots exist, their mode (voltage/current), and channel counts
  • PIN_REMAP — GUI/logical pin → actual physical output channel, per card dev. Only populate entries you've directly confirmed (see pin_identify_test.py); an unconfirmed entry is more dangerous than none, since it would silently send commanded voltage to a physical pin you don't think you're touching.
  • MODE_RANGES — output voltage/current limits per mode
  • RAMP_TICK_MS / SLEW_RATE_V / SLEW_RATE_MA — ramping behavior for analog output changes

Everything else — COM ports, GPIB addresses, last-used wavelength/power, per-pin nicknames — is set from within the GUI itself and persisted automatically to connection_settings.json / ao_channel_names.json.

Data Output

Sweep results and recordings save as timestamped CSV to data/ at the repo root (auto-created if missing), with sweep plot images in data/images/. Saving enables that box's "📂 Open" button, which opens the most recent file for that box — files are not auto-launched on save.

Sweep/recording CSVs start with # comment lines recording the run's parameters (range, step, dwell, set-point), followed by a normal header row — so a sweep stays reproducible later. pandas.read_csv(..., comment='#') and Excel's import both skip them cleanly. Optical power is always in watts.

See Where your data goes for filename conventions and a worked example.

Troubleshooting

  • A tab shows "X not found" instead of its controls — the corresponding optional dependency didn't install. Re-run uv sync; if a specific package is still missing, uv pip install <package> gets you unblocked immediately, but track down why uv sync didn't install it (see the OneDrive note above).
  • uv sync fails with Access is denied — see the OneDrive note in step 3 above. Re-running it (a few times, if needed) resolves it.
  • DAQ Control tab can't connect — check CUBE_IP matches your cube and that it's reachable on the network (ping <CUBE_IP>).
  • A laser/motor tab can't connect — check the COM port and (for GPIB instruments) the GPIB address match what's set in that tab; these are editable directly in the GUI and get saved for next time.
  • Commanding pin N on the AO-333 (Dev2) card outputs on the wrong physical channel — the card's connector/cabling doesn't necessarily wire straight through. Don't guess a fix by trial and error; run pin_identify_test.py to find the actual mapping for the pins you care about, then encode the confirmed pairs into PIN_REMAP (see Configuration).

About

This project is part of my work during my Summer 2026 internship at the Youngblood Photonics Lab (YPL), University of Pittsburgh.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages