Plesty Documentation

A Fleet

Declare, start and supervise every device server of a bench at once with fleet.yaml and plesty-server.

Lesson 2 of 6. Continues from lesson 1. The answer is on branch step-2-fleet, and git diff step-1-device..step-2-fleet is this page.

A measurement needs more than one instrument. This sweep needs three — the powermeter you already have, the rotation stage that turns the filter, and the spectrometer that records each spectrum. Each is its own module, its own program, its own port.

Declaring three of those through a form, and remembering afterwards which port each took, is exactly the bookkeeping a fleet removes: one file listing what this bench runs, and one command to bring all of it up.

Where your device is written down

The console's form did not invent anything — it wrote your declaration to a file, and that file is what a bench really is. Open it with Edit ▸ Fleet file…, or find it on disk:

cat ~/.plesty/demo-bench/fleet.yaml
version: 1

# Where venvs, logs and process state live. plesty-server reads this line
# itself, so `~` works here — and nowhere else in the file.
home: ~/.plesty/demo-bench

devices:
  pm100d:
    package: plesty-pm100d
    git: https://gitlab.com/plesty/hub/devices/thorlab/plesty-pm100d.git
    ref: demo/mock-replay
    args: [--mock]
    port: 5551
    autostart: true
Key Meaning
the key (pm100d) Instance name — it names the venv, the log and the process record
package Distribution to install
git + ref Repository and branch to install from; version: pins a PyPI release instead
args Arguments handed to the server process
port The port the bench allocated for it, and probes it on
env Environment for that server, on top of the bench's own
autostart Whether fleet up starts it — the check-box you saw in the form
home Where venvs, logs and process state live

It belongs with your project rather than in the bench home, so put it there — File ▸ Save working fleet as…, into your demo-bench directory. plesty-server looks for ./fleet.yaml before the one in its home, so from now on commands you run in the project use this copy.

Get the dataset first

The two new devices need to know where the measured sweep lives, so fetch it before you declare them. It is 148 kB, and you do not build it by hand:

mkdir -p data
curl -fL -o data/pol-pl-demo.npz \
  https://gitlab.com/plesty/demo-bench/-/raw/step-2-fleet/data/pol-pl-demo.npz

Then check you have the dataset and not a web page:

python -c "import numpy; print(numpy.load('data/pol-pl-demo.npz')['counts'].shape)"
(91, 1340)

Both of those matter more than they look. -f makes curl fail on an error response instead of writing it to the file; without it, a redirect to a login page or a bot check is saved as pol-pl-demo.npz, and curl reports success. The shape is what proves the file is the sweep: 91 angles by 1340 pixels. A mock handed anything else falls back to inventing numbers — it says so once in its server log, and everything after that looks like a working bench producing believable readings that are not a measurement.

You will be typing its path into the declarations in a moment, and it has to be absolute — pwd prints the one to use, and there is a section below on why a relative one would quietly do the wrong thing.

pwd
# /home/you/plesty-demo-bench   ← this is the "…" in the table that follows

This file is a replay of a real experiment — 91 rows of a polarization sweep, measured on 2026-08-04 on the instruments this demo names. The mocks answer from it instead of inventing numbers, which is what lets you go through the whole tutorial without touching an instrument and still get a feel for how a measurement behaves.

On a real bench none of it is needed: no dataset, no PLESTY_MOCK_*, no --mock. The numbers come from the light, and everything else you write here stays the same.

Add the other two

Back to the hub for the two you do not have. Filtering hub.plesty.net by vendor thorlab finds plesty-k10cr1, the rotation stage; the spectrometer is under princeton_instruments as plesty-lightfield-spectrometer. Install both from Catalogue ▸ Search & install…, then declare an instance of each with Edit ▸ Add device…, exactly as you did for the powermeter.

This is where the bench ends up — all three, including the powermeter you already have, which needs two variables adding to it (Edit, Ctrl+E). Two fields are the same for every one: Source is Repository (git) at ref demo/mock-replay, and Args is just --mock. What differs is the environment:

Device Module Env
pm100d plesty-pm100d PLESTY_MOCK_DATASET=…/data/pol-pl-demo.npz
PLESTY_MOCK_BENCH=…/bench
k10cr1 plesty-k10cr1 PLESTY_MOCK_BENCH=…/bench
spectrometer plesty-lightfield-spectrometer DEVICE_DATA_PATH=…/frames
PLESTY_MOCK_DATASET=…/data/pol-pl-demo.npz
PLESTY_MOCK_BENCH=…/bench

is the path pwd just printed, and every one of these has to be absolute — the section below says what goes wrong otherwise.

Choosing Repository (git) over Release (PyPI) is the one place the demo departs from a normal bench. Each demo/mock-replay branch is the released module plus the ability to answer from the dataset; a real bench pins a version: instead.

The stage and the spectrometer only differ in what they need to know. The stage publishes where it is (PLESTY_MOCK_BENCH); the spectrometer reads that, replays a spectrum from the dataset, and writes the frame somewhere (DEVICE_DATA_PATH); the powermeter reads the angle and replays a power. Why they need to agree at all is the next section but one.

No ports anywhere: the bench allocates 5552 and 5553 for the two new ones, the next free after the powermeter's.

Look at what the form wrote

Now open the fleet file again. Three devices, and the shape you filled in — with /path/to/demo-bench standing in for the path you actually typed:

version: 1
home: /home/you/.plesty/demo-bench
devices:
  pm100d:
    package: plesty-pm100d
    git: https://gitlab.com/plesty/hub/devices/thorlab/plesty-pm100d.git
    ref: demo/mock-replay
    args:
    - --mock
    port: 5551
    env:
      PLESTY_MOCK_DATASET: /path/to/demo-bench/data/pol-pl-demo.npz
      PLESTY_MOCK_BENCH: /path/to/demo-bench/bench
  k10cr1:
    package: plesty-k10cr1
    git: https://gitlab.com/plesty/hub/devices/thorlab/plesty-k10cr1.git
    ref: demo/mock-replay
    args:
    - --mock
    port: 5552
    env:
      PLESTY_MOCK_BENCH: /path/to/demo-bench/bench
  spectrometer:
    package: plesty-lightfield-spectrometer
    git: https://gitlab.com/plesty/hub/devices/princeton_instruments/lightfield_spectrometer.git
    ref: demo/mock-replay
    args:
    - --mock
    port: 5553
    env:
      DEVICE_DATA_PATH: /path/to/demo-bench/frames
      PLESTY_MOCK_DATASET: /path/to/demo-bench/data/pol-pl-demo.npz
      PLESTY_MOCK_BENCH: /path/to/demo-bench/bench

Three things changed that nobody typed, and they are worth knowing before you start editing this file by hand:

  • The comments are gone. Saving through the console rewrites the whole file from the declarations it holds, so anything it does not model — comments, blank lines, your ordering — does not survive. Keep the notes in your own copy, not in the working fleet.

  • args is now a list per line, and a port appeared beside it. You wrote --mock; the file stores each argument separately, which is how it reaches the process, and records the port the bench picked so the same device keeps it across restarts.

    This is also why nothing here pins a port. Allocation asks the host whether a port is free, so a demo bench on a machine that already runs a real one simply takes the next ports up and the two coexist — hold 5551 and the powermeter comes up on 5552 without being asked. Write port: yourself and you lose that: the device then insists on a port something else may already hold.

    The write-back is why fleet.yaml shows as modified in git status after your first fleet up. That is the bench recording what it chose, not something going wrong.

  • autostart: true disappeared — it is the default, and only the exceptions are written down. A device you un-check appears as autostart: false.

That last point is the fleet idea arriving: the file is not a script, it is a description of what this bench is. plesty-server reads it and makes the world match.

Bring it up

Back in the console, the three devices are listed and each carries a check-box in the Fleet column. Checked means part of the working fleet — the set that starts together. Then press Fleet up.

One press installs whatever is missing and starts every checked device. Watch the table: each row turns running and fills in its port, its PID and the time it started. A row that goes exited instead has a reason in its Log tab.

Fleet down stops them all again. Between the two you have the thing a fleet is for: a bench is up or it is down, rather than three programs you start in the right order and remember to stop.

If the powermeter from lesson 1 is still running, press Fleet down first. Fleet up starts what is not running; it does not restart what is — and that server was started before you added the replay variables, so it would keep answering with the old synthetic readings and nothing would say why.

Turn the stage, read the meter

Lesson 1's script opened one client and asked it for a reading. Write a second one beside it, plesty/demo_bench/sweep_power.py, that opens two — the stage and the meter — and walks the plate through a series of angles:

from plesty.lib.service import build_client

#: The addresses the fleet file gives the two servers. Check yours with
#: `plesty-server status` — the bench allocated them.
HWP_ADDRESS = "tcp://localhost:5552"
PM_ADDRESS = "tcp://localhost:5551"

#: A half-wave plate rotates polarization at twice its own angle, so 0..90 deg
#: of plate covers one full period of the power curve.
ANGLES = [0.0, 12.0, 24.0, 36.0, 48.0, 60.0, 72.0, 84.0]


def main() -> None:
    """Move the stage to each angle and print the power measured there."""
    # The stage takes its time: a move is physical, so it is given a longer
    # window than the meter.
    with build_client(HWP_ADDRESS, timeout=140_000) as hwp, build_client(PM_ADDRESS) as pm:
        print(f"{'angle (deg)':>12}  {'power (uW)':>11}")
        for angle in ANGLES:
            reached = hwp.move_absolute(angle)
            power_w = pm.measure_power(averaging=10)["power"]
            print(f"{reached:12.2f}  {power_w * 1e6:11.3f}")


if __name__ == "__main__":
    main()

Two clients, driven independently. Nothing in this script connects them: it tells the stage where to go, then asks the meter what it reads.

Check the ports first
The two addresses in that script are guesses — the bench chose the real ones. Compare them with the Port column in the console, or with plesty-server status, and correct the script if they differ.

They will differ if anything else on this machine already held 5551 or 5552. Get one wrong and the client connects to the other instrument, or hangs waiting for something that is not there.
plesty-server status
NAME          STATE    PID    PORT  STARTED
pm100d        running  91645  5551  2026-08-20T10:07:59+00:00
k10cr1        running  91649  5552  2026-08-20T10:08:01+00:00
spectrometer  running  91654  5553  2026-08-20T10:08:02+00:00

Then run it:

uv run python -m plesty.demo_bench.sweep_power
 angle (deg)   power (uW)
        0.00        6.113
       12.00        0.263
       24.00        2.228
       36.00       10.864
       48.00       16.844
       60.00       24.149
       72.00       20.245
       84.00        9.555

Malus' law — and not invented numbers: that is what the powermeter read at those angles on 2026-08-04. Which raises the question of how the powermeter knew, given that nothing in the script told it.

Why you had to type absolute paths

Because a device server does not run in your checkout. plesty-server launches it in its own working directory under the bench home, so a relative ./data/pol-pl-demo.npz resolves against that directory instead.

The failure this avoids is the nasty kind: the mock would find no dataset, fall back to its synthetic frame, and the sweep would complete and look fine while containing nothing measured.

The home: line is the exception, because plesty-server reads that one itself and expands ~ before anything is launched. By the last lesson you stop editing any of this by hand.

How the mock bench stays consistent

There is one entry in the demo's fleet file that has no equivalent on a real bench, and it is worth understanding because it explains what a bench is.

Mock devices share no physics. The stage server turns its simulated stage, and the spectrometer server has no idea that happened — it is a separate process with no connection to it. Left alone, every row of the sweep would come back identical and the polarization map would be a flat smear. On a real bench the optical table does the coupling: light really does pass through the plate before reaching the detector.

So the demo gives the mocks a stand-in for the optical table — a directory:

    env:
      PLESTY_MOCK_BENCH: /path/to/demo-bench/bench

The stage writes the angle it reached into bench/bench.json after every move. The powermeter and the spectrometer read it and answer for that angle, out of the measured dataset. Three independent processes, one consistent measurement.

On a real bench nothing does this, because nothing has to: the light passes through the plate before it reaches the detector, and the instruments agree by being on the same table. The coupling only runs when a server has both --mock and PLESTY_MOCK_BENCH.

Because the lookup is by angle rather than by call count, the angles in sweep_power.py are not special. Edit the list, rerun, and the readings still correspond — which is what will let the experiment in lesson 4 choose its own sweep without any of this changing.

What a bench keeps

home/
  repos/     the installed module checkouts, one per version
  venvs/     their environments
  logs/      one log per device, kept across restarts
  run/       each server's working directory
  state/     process records: pid, port, command, env
  jobs/      installs, updates and field tests, with their output

The process record is what lets plesty-server put a server back exactly as it was — same interpreter, same arguments, same port — after an update or a field test.

Three servers are running. Next, driving them as a single instrument.

The same from the command line

Everything in this lesson is also a command, which is what a bench brought up on boot, or over SSH, actually uses:

plesty-server fleet up          # install what is missing, start the working fleet
plesty-server fleet status      # who is running, on which port
plesty-server fleet down        # stop them all
plesty-server fleet run         # bring up and keep up, for a headless bench
plesty-server status            # every declared device, checked or not
plesty-server log k10cr1 -n 20  # one device's output

fleet up exits non-zero if any device ended exited or stopped, so it works in a startup script. fleet run is the headless form: it brings the fleet up and stays in the foreground keeping it there, which is how a bench PC runs with no one logged in.

Next

Lesson 3: Composite Device — stop driving three clients by hand and make them one instrument.