October filled the Hub from the bottom of the pipeline up. Four new instrument modules arrived: a Swabian Time Tagger, a Keithley 2410 SourceMeter, a three-axis Thorlabs KST201 stage and a PicoQuant Taiko picosecond laser driver. Experiments that drive them arrived at the same time: a search for single-photon emitters that runs on three of the four, and a quantum-dot run that navigates to each dot and takes its power and polarisation series. Underneath, plesty-lib 0.6.0 reaches Thorlabs controllers that Windows bound to FTDI's D2XX driver. It also lets one experiment start only the instruments a given configuration uses.
plesty-fss learns a quarter-wave-plate rotatorPart one · Devices
What's new on the Hub
plesty-swabian-tt v0.1.1Count rates, counter traces, cross- and auto-correlation (g2), start-stop histograms, coincidences and time-tag recording, with settings per channel. A seeded simulator runs where Swabian's library has no wheel, macOS included.
plesty-k2410-source-meter v0.1.0Sources a voltage or a current and measures both, single points or sweeps, over VISA on GPIB or RS-232. The source goes up to 1100 V, so the module refuses an unsafe request before it reaches the instrument and shuts down safely on exit.
plesty-kst201 v0.1.0Three single-axis K-Cube controllers form one device on one TCP port. The axes are called by name (x, y, z), positions are in millimetres, and the scaling matches Kinesis.
plesty-taiko-pdl-m1 v0.1.1Repetition rate, power, pulsed or cw mode, lock state, calibrated average power and temperatures. Power always goes to 0 before a lock, and the driver locks on disconnect, so the laser never stays on by accident.
The first days of the window also brought first releases of the Attocube AMC300 piezo controller (plesty-amc300-piezo-controller), a Raspberry Pi Pico W mirror and LED controller (plesty-pi-mirror-controller) and The Imaging Source DMK 37AUX290 camera (cam_dmk_37aux290). On 27 October the Attocube ANC300 piezo controller (plesty-anc300-piezo-controller v0.1.0) followed, and three of these devices reached a new line:
- AMC300 v0.2.0 reads the attoDRY800 cryostat's sample temperature and pressure, and adds open-loop stepping and fine positioning by DC level.
- DMK camera v0.2.0 measures a frame's dots, marker lines and laser spot, and returns only numbers.
- K10CR1 v0.4.0 picks its stage by serial number, so one PC can serve two wave plates.
The navigation experiment below uses all of them.
For users: install a device and talk to it
Every module installs and runs the same way. Clone it, sync it, and start it as a device server on the PC the instrument is plugged into. Each module ships a mock address, so you can try the whole API without the hardware:
git clone https://gitlab.com/plesty/hub/devices/swabian/plesty-swabian-tt.git
cd plesty-swabian-tt
uv sync
cp .env.example .env # TIME_TAGGER_ADDRESS: empty, a serial, net:host[:port] or mock
uv run python -m plesty.swabian_tt --tcp-port 5555
From any other machine, a client from plesty-lib connects to that server and offers the same parameters and operations as the local device:
from plesty.lib.service import build_client
with build_client("tcp://<bench-host>:5555") as tt:
print(tt.describe()) # every parameter and operation
rates = tt.measure_count_rate(channels=[1, 2], duration=1.0)
g2 = tt.measure_correlation(channel_1=1, channel_2=2, duration=10.0)
The other three follow the same pattern. In-process on the simulator, it looks like this:
from plesty.k2410_source_meter import Device
dev = Device(address="mock"); dev.init(); dev.connect()
dev.source_and_measure(source_function="VOLT", level=5.0, compliance=1e-3, source_range=20.0)
from plesty.taiko_pdl_m1 import Device
with Device(address="mock") as laser:
laser.write("repetition_rate", 10_000_000) # Hz
laser.emission_on() # releases the lock, power stays 0
laser.write("power_permille", 100) # 10 % of the maximum at this rate
laser.emission_off() # power 0, then lock
# KST201: one server for the whole stage
# uv run python -m plesty.kst201 --tcp-port 5555 --mock
stage.move_axis("x", 12.5); stage.get_positions() # {'x': 12.5, 'y': ..., 'z': ...}
PDLM_Lib.dll, are never shipped with a module. You install the vendor's own package, and the module finds the library there or at the path you set in .env. The module's preflight check says what is missing before anything opens the instrument.The change underneath: APT over FTDI D2XX
Thorlabs motion controllers (K-Cubes, the K10CR1 rotation stage, the KST201) speak the APT serial protocol over an FTDI USB chip. Without Thorlabs Kinesis, the chip shows up as a COM port and pyserial reaches it. Once Kinesis is installed, Windows hands the controller to FTDI's D2XX driver. There is then no COM port, serial.tools.list_ports never lists the controller, and every APT module stopped with "no port".
BaseAptDevice, sets its scale factors and SERIAL_PREFIXd2xx:<serial>In plesty-lib 0.6.0, plesty.lib.traffic.apt_d2xx.D2xxPort is a serial-like adapter over D2XX. It offers exactly the calls AptTrafficManager makes on a port, so nothing above it changes:
| Address | What opens |
|---|---|
COM7 | That port, through pyserial, as before |
| a serial number | The COM port that carries it. If no COM port does, the D2XX device with that serial |
d2xx:<serial> | Forces D2XX |
| empty | Any APT COM port. If there is none, the single D2XX device whose serial starts with the class's SERIAL_PREFIX |
Preflight lists D2XX devices next to the COM ports. It only enumerates and never opens a controller, so a wrong serial is reported with every serial it did see. Opening a controller still needs Kinesis closed, because Kinesis holds the device while it runs.
No new dependency
The first version used the ftd2xx package from PyPI. That package has had no release or commit for over two years, so lib now calls FTDI's own ftd2xx.dll through ctypes. Each function it uses is declared with its argument types from ftd2xx.h, so handles and pointers pass at the platform's width. The DLL comes with the driver: Kinesis or FTDI's driver package installs it on every PC where a controller is bound to D2XX. Nothing extra goes into Python. On a host without it, the module reports no D2XX devices instead of failing to import.
For developers: building a Thorlabs module on it
A Thorlabs controller module no longer needs any driver code of its own. Subclass the APT base device, give it your hardware's counts-per-unit scale, and set the serial prefix of your model:
from plesty.lib.device.base_apt_device import BaseAptDevice
class RotationStage(BaseAptDevice):
SERIAL_PREFIX = "55" # K10CR1 serials start with 55; empty takes any single device
# register your parameters with register_apt_param(...) and build
# operations in physical units on home(), move_absolute_counts(), ...
The address comes from the CLI or .env, as configuration and never as code. The same module then works on a PC with a COM port and on one with Kinesis, with no change. The KST201 shows the multi-controller case. It is one device for the whole stage, not one server per axis, and it chooses each axis's controller by serial number. Its 0.1.0 release carried its own D2XX adapter over the old ftd2xx package. Its next release hands that to lib's apt_d2xx and keeps only the KST201's own rule for choosing between D2XX and a COM port.
Every new device module follows the same pattern:
- Schemas first.
schema_param.jsonandschema_func.jsondeclare every parameter (type, unit, range, access) and operation. Writes are checked against them, and the generated docs pages come from them. - A preflight before connect. It tells a wrong address from a missing instrument, a vendor library that is absent or of the wrong bitness, and a driver that is not bound.
- A
mockaddress. It runs an in-memory simulator, so the tests, the docs examples and a first try need no hardware. - Safety in the module, not the user's script. Interlocks, compliance limits, power-to-zero before a lock, and a safe state on disconnect.
Part two · Experiments
What the new devices are measuring
plesty-perovskite-g2-search experimentA stage visits the sites of a raster grid and peaks the count rate at each one. A site is kept as a single-photon emitter only if its count rate stands above the background and its second-order correlation at zero delay, g2(0), is below 0.5. Each emitter found then gets a saturation curve: its count rate at each laser power of a series.
plesty-nav-pow-pol-pl v0.1.0One run goes dot after dot through a list of quantum dots taken from an earlier image scan. For each dot it navigates and aligns on the camera's view of the chip, then peaks the laser on the dot's own glow. It then takes a power series and a polarisation series and analyses both. Every move is bounded and every location checked before the stage moves. A stopped run resumes at the dot it stopped at.
plesty-pl-image-scan v0.1.0Maps a quantum-dot chip cell by cell, with one image per illumination at each position. Each cell is one journaled step, so an interrupted scan resumes at its cell. The navigation experiment reads its map straight from the run's files.
plesty-pol-pl v0.2.0A half-wave plate turns in front of a fixed polariser, with one spectrum per angle and a fine-structure analysis per dot. It is the base of the bench's welcome demo. With a mocked stage and a replayed spectrometer it runs on one PC with no lab at all.
plesty-fss v0.2.0Gives the exciton fine-structure splitting of every line from polarisation-resolved spectra. It gained a qwp rotator (a quarter-wave plate in front of a fixed polariser), which is what the navigation experiment's polarisation series measures with.
Which experiment drives which new device
| Device | g2 search | nav + pow/pol PL | PL image scan |
|---|---|---|---|
| Swabian Time Tagger | ● | – | – |
| Thorlabs KST201 stage | ● | – | – |
| PicoQuant Taiko PDL M1 | ● | – | – |
| DMK 37AUX290 camera | – | ● | ● |
| AMC300 piezo controller | – | ● | ● |
| Pico mirror / LED controller | – | ● | ● |
The Keithley 2410 has no experiment on the Hub yet. It is ready for one that sources and measures.
For users: run an experiment from the bench
An experiment module never needs its instruments started by hand. Its setup.yaml names the instruments, which bench serves each one, and the package and version to install there. PLESTY Bench does the rest:
- Install it. Experiment ▸ Add experiment… picks a release, or Hub ▸ Load from… takes a local checkout. Installing writes the module's
.env, and Configure… ▸ Device Setup edits it. - Create a run and set its configuration. It is saved to the run, never into the module's own files.
- Run. The bench starts the instruments this configuration uses on their benches, gives the run each instrument's address (
TT_ADDRESS,STAGE_ADDRESS, …), opens the live viewer, and stops the instruments when the run ends. The run's folder gets itsreport.html.
The same module runs from a terminal, and that is what the bench calls:
uv sync
cp .env.example .env # the benches, the shared disk
uv run python -m plesty.perovskite_g2_search --dry-run # the plan, nothing connected
uv run python -m plesty.perovskite_g2_search # run with config/default.yaml
uv run python -m plesty.perovskite_g2_search --resume <run_id>
uv run python -m plesty.perovskite_g2_search --manifest # the settings it accepts, as JSON
plesty-pol-pl brings up a mocked rotation stage and a replay spectrometer on this machine with plesty-server fleet up -f setup.yaml, then runs config/demo.yaml. That is the run the bench's first-run welcome makes for you.For developers: how an experiment module is shaped
setup.yaml is committed and names no lab. Every value is the variable that holds it, set in the checkout's .env. A literal address, token or path is refused:
version: 1
benches:
bench-local: # the machine the run is on
data: ${LOCAL_DATA}
bench-perovskite: # the bench serving the instruments
data: ${BENCH_PEROVSKITE_DATA}
address: ${BENCH_PEROVSKITE_HOST}
token: ${BENCH_PEROVSKITE_TOKEN}
devices: # one entry per sub-device of the rig
stage: { host_name: bench-perovskite, package: plesty-kst201, version: 0.1.0 }
tt: { host_name: bench-perovskite, package: plesty-swabian-tt, version: 0.1.1 }
laser: { host_name: bench-perovskite, package: plesty-taiko-pdl-m1, version: 0.1.1 }
pm: { host_name: bench-perovskite, package: plesty-pm100d, version: 0.1.0 }
The entry point is plesty-lib's ExperimentCLI. The module registers how to build its rig and its experiment. The CLI provides --dry-run, --resume, --manifest, the journal and the plan:
from plesty.lib.experiment.cli import ExperimentCLI
cli = ExperimentCLI(MANIFEST)
cli.optional("pm") # without a powermeter, the curve uses the laser's estimate
@cli.rig
def build_devices(args, config, skip):
return G2SearchRig({n: c for n, c in DEVICES.items() if n not in skip})
@cli.build
def build_experiment(args, config, run_root, devices):
return Experiment(devices=devices, config=config, run_root=run_root)
# new in plesty-lib 0.6.0: which devices of setup.yaml this configuration uses
@cli.needs
def needs(config):
return ["hwp", "spec"] if not config.get("dots") else None # None = all of them
@cli.needs is what 0.6.0 adds. One module can measure in configurations that use different instruments. For example, measuring in place uses the wave plates and the spectrometer, while navigating also needs the camera, the LEDs and the piezos on another bench. python -m <module> --devices --config <file> prints the answer as JSON, and the manifest says chooses_devices. The bench then starts only those instruments, and a run needs only their benches set up. The navigation experiment's "measure where the stage stands" mode no longer stops on an unset camera bench.
The rest of the contract is plain:
- A run is a fixed plan of steps, frozen into
plan.json. Each finished step commits one record torecords.jsonl, and that is what makes--resumework. Stopping a run isSIGINT, so the journal records where to resume. - Big data stays off the network. Instruments write frames and spectra to the shared disk under their bench's folder, and only paths relative to the share travel. In 0.6.0,
set_data_pathtakes a path relative to the bench's folder. - Analysis is an analyzer. A run hands its spectra to an analyzer such as
plesty-fss, and the results land in the run and its report.
To contribute one, scaffold with plesty init, write setup.yaml with variables only, register your rig and experiment on ExperimentCLI, and land it through a merge request into the module's exp branch. The quantum quality gates that plesty check enforces run on every push.