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.
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_REMAPingui.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. Seepin_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);Slot2OutBis 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 chosenV_π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, sonearest(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)
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 ofgui.pywithPIN_REMAPforced empty (pure identity mapping). Run this instead ofgui.pyif 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 realPIN_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 sharedvisa_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. Usegui.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 theuvpackage manager (see Setup).
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 theUeiDaqPython bindingsuv synclinks against andPDNALib.dll, whichpin_identify_test.py's optional Guardian ADC check needs. Not somethinguv/pip can install for you. - (Optional) NI-DAQmx runtime —
only needed for the Santec TSL-550 tab to actually talk to hardware (the
nidaqmxPython 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_IPin 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
-
Install
uv(see above) if you don't already have it. -
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 -
From the repo root, sync the environment:
cd "path\to\GUI" uv syncThis creates
.venvand installs everything declared inpyproject.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 synccan intermittently fail withAccess is deniedwhile OneDrive has a file in.venvlocked for syncing. This is harmless — it isn't a broken environment, just OneDrive racing the installer. Just re-runuv sync(usually 1–5 tries clears it). If it keeps happening, exclude.venvand.venv32from OneDrive sync (OneDrive Settings → Account → Choose folders) — they're build artifacts, not something you need backed up. -
Run it:
uv run code\UeiDaq_gui\gui.py
(or activate
.venvyourself and runpython 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).
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.)
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.pyThis 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.
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 cubeMOKU_IP— IP address of the Moku:Go, if usedCARDS— which device slots exist, their mode (voltage/current), and channel countsPIN_REMAP— GUI/logical pin → actual physical output channel, per carddev. Only populate entries you've directly confirmed (seepin_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 modeRAMP_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.
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.
- 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 whyuv syncdidn't install it (see the OneDrive note above). uv syncfails withAccess 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_IPmatches 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.pyto find the actual mapping for the pins you care about, then encode the confirmed pairs intoPIN_REMAP(see Configuration).