Skip to content

STARBackend: 96-head Y/Z drive speed & acceleration set/request, scoped to each move#1088

Merged
BioCam merged 18 commits into
PyLabRobot:mainfrom
BioCam:head96-speed-accel-pr
Jun 19, 2026
Merged

STARBackend: 96-head Y/Z drive speed & acceleration set/request, scoped to each move#1088
BioCam merged 18 commits into
PyLabRobot:mainfrom
BioCam:head96-speed-accel-pr

Conversation

@BioCam

@BioCam BioCam commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

Adds set/request for the 96-head Y and Z drive speed and acceleration (H0 AA / H0 RA), and uses them so a move's speed and acceleration are scoped to that single call. Builds on #1086 (the Z move primitives) and #1087 (the per-drive defaults).

A ZA / YA move writes its speed and acceleration into the drive's volatile register and leaves them there, so a later move - or a C0-level command (aspirate96 / dispense96) that sends none of its own - silently inherits the previous move's values. Confirmed on hardware: a speed=20 Z move reads zv back as 20.

  • head96_set_y_speed / head96_set_y_acceleration / head96_set_z_speed / head96_set_z_acceleration (H0 AA): save a persistent drive parameter standalone, no move - the same mechanism slow_iswap uses on the iSWAP via R0 AA.
  • head96_request_y_speed / head96_request_y_acceleration / head96_request_z_speed / head96_request_z_acceleration (H0 RA): the read counterparts.
  • head96_move_stop_disk_z and head96_move_y reset the drive speed/acceleration to the head's default after the move, skipping the reset when the caller left a parameter at its default (so a plain move adds no register writes); reset_z_parameters / reset_y_parameters opt out.
  • head96_move_stop_disk_z retracts to Z-safety on any firmware error (e.g. the head crashing into something) before re-raising - at a quarter of the max Z speed, since the head may be submerged in liquid - via retract_on_crash; head96_move_to_z_safety passes it False so the retract cannot recurse.
  • Head96Information gains z_speed_range and z_acceleration_range (constant fields, verified unchanged across the 2008/2013/2025 firmware command sets). head96_move_stop_disk_z, head96_set_z_speed, and head96_set_z_acceleration now validate against the record instead of hardcoded literals, mirroring how head96_move_y already uses y_speed_range. The crash retract speed is derived from z_speed_range[1] rather than a magic number.

Behaviour change to note for review: head96_move_y's speed / acceleration now default to None (resolving to the firmware default, e.g. 390.62 mm/s) instead of a hardcoded 300, matching head96_move_stop_disk_z.

Validated on hardware: RA reads match the firmware-resolved defaults in Head96Information, and an AA set followed by RA round-trips exactly.


Update 2026-06-15 (revised during review)

Following review, the public surface moved from standalone set/request methods to client-tracked, runtime-settable defaults. The original description above still explains the underlying register behaviour; this section records what changed on top of it.

  • The 96-head Y/Z drive speed and acceleration defaults are now exposed as range-checked properties (head96_y_drive_speed_default, head96_y_drive_acceleration_default, head96_z_drive_speed_default, head96_z_drive_acceleration_default), seeded from Head96Information at setup. A move resolves its None default and restores the register through these, so a user can set their own default at runtime, have it drive plain moves, and have it survive the post-move restore. Head96Information stays frozen as the factory reference; the live values live on the backend.
  • The standalone AA register writes are now private (_head96_set_*); the public knob is the property. The RA reads stay public as diagnostics for troubleshooting or specialized use. This addresses the review point that the set methods should not be public.
  • Head96Information gains y_acceleration_range, resolved per firmware (max 500.0 on 2008, 781.25 on 2013+) like y_speed_range rather than a constant - the 2008/2013/2025 command sets show the max changed at the 2013 firmware. head96_move_y and the Y-acceleration setter now validate against it; the previous hardcoded 78.125..781.25 literal over-permitted 2008-era heads.
  • The head96_move_y default-to-None note above still holds, but None now resolves to the overridable property rather than the raw Head96Information value.
  • The 96-head section is regrouped so it opens with the conversion helpers, then the overridable-default properties, the request reads, the private setters, then the movement commands.

BioCam and others added 2 commits June 11, 2026 17:40
…A/RA

Add head96_set_y_speed / head96_set_y_acceleration / head96_set_z_speed / head96_set_z_acceleration, each saving the persistent drive parameter (yv/yr/zv/zr) standalone via H0 AA - no move - so subsequent moves (including the C0-level 96-head commands) inherit it, the same mechanism slow_iswap uses with R0 AA on wv/tv.
Add the read counterparts head96_request_y_speed / _y_acceleration / _z_speed / _z_acceleration via H0 RA, with z-acceleration inverting the firmware-version scaling that the setter applies.
Validation mirrors the existing move methods; setters are @_requires_head96, getters unguarded.

NOTE: unverified on hardware - that AA accepts these parameter names and RA reads them back in the assumed field widths is inferred by analogy to slow_iswap; confirm with a set/request round-trip on the device.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…retract Z on crash

A ZA/YA move leaves its speed and acceleration in the drive's volatile register, so a later move
or C0-level command inherits them (confirmed on hardware: a speed=20 Z move reads zv back as 20).
head96_move_stop_disk_z and head96_move_y reset the drive parameters to the head's default after
the move, skipping the reset for parameters the caller left at default (no churn on plain moves);
reset_z_parameters / reset_y_parameters opt out.

head96_move_stop_disk_z retracts to Z-safety on any firmware error before re-raising, via
retract_on_crash (head96_move_to_z_safety passes False so the retract cannot recurse).

head96_move_y speed/acceleration now default to None (firmware default) instead of 300, matching
head96_move_stop_disk_z.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@BioCam BioCam requested a review from rickwierenga June 11, 2026 21:24
BioCam and others added 2 commits June 11, 2026 22:37
…nformation`

Add z_speed_range and z_acceleration_range to Head96Information as constant fields (verified
unchanged across the 2008/2013/2025 firmware command sets). head96_move_stop_disk_z,
head96_set_z_speed, and head96_set_z_acceleration now validate against the record instead of
hardcoded literals, mirroring head96_move_y's existing use of y_speed_range; head96_set_z_speed
gains the missing Head96Information guard.

The crash-recovery retract now moves at a quarter of the max Z speed (z_speed_range[1] * 0.25)
rather than the default - the head may be submerged in liquid after a crash.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TestHead96CrashRecovery covers head96_move_stop_disk_z's firmware-error path: a ZA error retracts
the head to z_range[1] (a second ZA) before re-raising the original error, and when the retract
itself errors the move sends exactly two ZA commands (no recursion) and surfaces the original error,
not the retract's. The crash is injected via send_command side_effect; z targets are read from the
record so the tests track the resolved Z window.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment on lines +8134 to +8135
speed: Movement speed in mm/sec. Valid range: [0.78125, 390.625 or 625.0]; None uses the head's
y_drive_speed_default.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does the range depend on? it says or

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See resolve commands: it depends on the firmware/variant

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dug into this — the or was masking that the range is firmware-version-dependent, and (the part that bit us) that the speed and acceleration cutoffs land on different firmware years:

  • Speed max: 390.625 mm/s pre-2021 → 625.0 on 2021+ — _head96_resolve_y_speed_range (year <= 2021)
  • Acceleration max: 500.0 mm/s² pre-2010 → 781.25 on 2013+ — _head96_resolve_y_acceleration_range (year >= 2010)

So the single Note that only documented the 2021 speed cutoff was actually wrong for acceleration.

What we did in head96_move_y:

  • dropped the or from the Args and spelled out both ranges explicitly, each pointing at the Head96Information field that resolves it (y_speed_range / y_acceleration_range)
  • rewrote the Note to document both cutoffs (speed @ 2021, acceleration @ 2010) instead of only speed

Docs-only; the validation already reads from the resolved range fields, so behaviour is unchanged.

Comment on lines 8139 to 8141
reset_y_parameters: If True (default), reset an overridden speed/acceleration to the head's
defaults after the move so it does not persist; set False to deliberately keep it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should always be true and the parameter should not even exist. it is generally good practice for functions to either do something or update state, very rarely both. this prevents bugs and makes it easier to use. (if we want to set the default, the user can do that on the client side imo, by updating 96head info, and we would still send AA twice just to keep the machine state untouched. we do not want state in two places if we can avoid it, and we easily can)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in other words the code should be as self-contained/"atomic" as possible

@BioCam BioCam Jun 12, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updating 96head info

is not possible, for safety reasons Information is frozen

regarding the reason where this has become really important is described in the comment below about the same topic

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 78ea87f — removed reset_y_parameters / reset_z_parameters entirely. head96_move_y and head96_move_stop_disk_z no longer take a flag that makes them also mutate persistent state: the move now always cleans up after itself, so there's no "do a thing and leave state changed" mode to opt out of.

Comment on lines +8201 to +8205
finally:
if restore_speed:
await self.head96_set_y_speed(self._head96_information.y_drive_speed_default)
if restore_acceleration:
await self.head96_set_y_acceleration(self._head96_information.y_drive_acceleration_default)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should always restore to the value that was there before the change, not to the PLR default. Users might've edited it using AA themselves. this wouldn't be consequential to us, since we ALWAYS get values from PLR, but they might have hackery or other software that depends on this in a strange way (it's not transparent, which makes it more dangerous)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure I understand:

We always expect the default speed/acceleration to be set if none has been explicitly declared.

(we might want to make the default speeds/accelerations to be made class property variables rather than part of the Head96Information, that way they are easy to modify for all commands by end-users who need more power? - but making that change is a separate PR from this set recovery PR)

But I the restore_speed: bool actually exists for a different purpose: to enable building more complex commands while avoiding roundtrip churn: e.g. if you build a complex command with multiple z positions, changing speed across each, not all commands should re-set in between commands because the next command will instantly overwrite the memory state anyway, i.e. this flag avoids unnecessary command roundtrips.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 78ea87f. The move now snapshots whatever speed/acceleration are on the robot before it runs (read via RA), then restores those exact values afterwards — so an external AA edit a user made is preserved, and the command leaves the persistent register exactly as it found it. No longer restores to the PLR default. The restore AA is skipped when the move's value already matched what was there (compared in increments, the unit actually stored).

Tradeoff: this adds 2 RA reads per move — intrinsic to restoring the actual prior value rather than a tracked default (you can't restore what you didn't read).

Comment thread pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py Outdated
BioCam and others added 8 commits June 13, 2026 11:32
…erridable

Expose the 96-head Y/Z drive speed and acceleration defaults as range-checked properties (head96_{y,z}_drive_{speed,acceleration}_default), seeded from Head96Information at setup. A move's None-default resolution and its post-move register restore both read these, so a user can set their own default at runtime, have it drive plain moves, and have it survive the restore. Head96Information stays frozen as the factory reference; the live values live on the backend.

Add Head96Information.y_acceleration_range, resolved per firmware (max 500.0 on 2008, 781.25 on 2013+) like y_speed_range rather than a constant, and validate Y acceleration against it in head96_move_y and the Y-acceleration setter. The previous hardcoded 78.125..781.25 literal over-permitted 2008-era heads.

Make the on-device AA register writes private (_head96_set_*); the public knob is the property. The RA reads stay public as diagnostics for troubleshooting.

Regroup the 96-head section so it opens with the conversion helpers, then the overridable-default properties, the request reads, the private setters, then the movement commands.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cstrings

Wrap symbol references (Head96Information fields, the `*_range` records, the `head96_*_default` properties, `_head96_set_*`, `head96_move_stop_disk_z`) in backticks across the 96-head drive-default docstrings, matching the convention used elsewhere in the file. Docstrings only - comments and error-message strings are left as-is. No behaviour change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the ambiguous '390.625 or 625.0' / '500.0 or 781.25' range
notation in head96_move_y with explicit firmware-keyed ranges, and note
that the speed and acceleration cutoffs differ (speed flips at 2021,
acceleration at 2010), matching the _head96_resolve_y_* resolvers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…accel

Remove the reset_{y,z}_parameters flags and the restore-to-PLR-default
behaviour. head96_move_y and head96_move_stop_disk_z now snapshot the
speed/acceleration currently on the robot (read via RA, so an external AA
edit the user made is preserved), then restore those exact values after
the move - skipping the AA write when the move's value already matches
(compared in increments, the unit actually stored). The command is now
self-contained: it leaves the persistent drive register exactly as it
found it, rather than overwriting it with the PLR default.

Addresses review: a move should not also mutate persistent machine state,
and any restore must return the register to what was there before, not to
our default.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
head96_move_stop_disk_z now snapshots the current Z speed/accel (to restore
after the move) via head96_request_z_speed/head96_request_z_acceleration. The
crash-recovery tests mock send_command to only answer ZA, so those reads hit a
bare {} response and raised KeyError: 'zv'. Mock the two request methods with
in-range values so the tests exercise only the ZA crash-retract path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
reStructuredText needs a blank line between a paragraph ending in ':' and the
following bullet list; without it docutils raised "Unexpected indentation",
failing the warnings-as-errors docs build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…docs

After moves were changed to snapshot and restore the live drive register, the
`head96_*_drive_*_default` values are no longer "what a move restores to" - they
are just the default speed/accel a move uses when the caller passes none. Update
the now-wrong comments/docstrings accordingly.

Also stop copying the frozen Head96Information factory defaults into mutable
backing fields at setup. The frozen dataclass holds the immutable factory
defaults; the backend keeps an Optional user override (None = use factory). Each
getter returns the override if set, else falls back to Head96Information. This
drops the redundant seeding block and keeps the user-override + range-check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@rickwierenga rickwierenga force-pushed the head96-speed-accel-pr branch from 5578204 to 4e04e01 Compare June 16, 2026 05:48
@rickwierenga

rickwierenga commented Jun 16, 2026

Copy link
Copy Markdown
Member

since Head96Information is frozen, it is not a good place for storing user configurable values like _head96_y_drive_speed_default. we should either

  1. unfreeze it
  2. store the defaults somewhere else, whether on the backend or another data class
    my preference would be 2 with backend for now, and then make it attributes of the capability backend in v1

…roperties

The per-drive factory defaults were resolved by backend `_head96_resolve_*_default`
methods and stored as Head96Information fields. They depend only on fw_version and
the encoder resolutions already on the dataclass, so make them computed @Property
methods on Head96Information instead and drop the resolve methods and stored fields.

Ranges stay resolved-and-stored: z_range's max comes from a hardware probe at setup,
so they cannot all be pure properties.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@rickwierenga rickwierenga force-pushed the head96-speed-accel-pr branch from 3980115 to 7229f0c Compare June 16, 2026 06:13
@BioCam

BioCam commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator Author

since Head96Information is frozen, it is not a good place for storing user configurable values like _head96_y_drive_speed_default. we should either

  1. unfreeze it
  2. store the defaults somewhere else, whether on the backend or another data class
    my preference would be 2 with backend for now, and then make it attributes of the capability backend in v1

There are 2 different "default":

  1. the manufacturer defaults -> these are the ones which are stored and resolved in the Head96Information, they don't change ever after setup and that's why I placed them there
  2. the PLR programmer's defaults -> these are configurable and during (re-)assignment have to be checked whether the assigned value is valid against the allowed range; that is what I built into this PR as the getter setter class attributes

having both means it is always possible by a PLR programmer to set back to default (because the information is always retrievable from the frozen dataclass)

@rickwierenga

Copy link
Copy Markdown
Member

why do we care about manufacturer defaults? why not just have the PLR default for everything since that's the value that's actually configurable and used? if we want to use the device defaults, we can load those values into the plr default attribute. saving it for people seems like a crutch and it makes everything more chaotic

@BioCam

BioCam commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator Author

One reason is that PLR does then not choose the starting default but instead we use the starting default that the device has been designed for:
When you restart the STAR it only has the device defaults set into it.
When you run a PLR script and overwrite it then the PLR default will take its place on volatile device memory ... until a power cycle comes along again.

And this manufacturer default changes based on firmware/variant.
To keep a clear overview of what is happening we would need a firmware/variant-adaptive record of what is this specific device meant to run on. Then we can make an explicit decision to change it from that reference and always be able to quickly compare from what OEM value it was changed to what PLR value.

We could just always set a PLR default without keeping an explicit record of what the manufacturer intended?

Are you suggesting that initial PLR default is computed during setup based on firmware/variant and then set in PLR, i.e. exactly the same as now but just no record being made of it in Head96Information?

BioCam and others added 5 commits June 18, 2026 15:24
The Y and dispensing-drive area-of-operation windows are pure functions of
fw_version and the encoder resolutions, so compute them as Head96Information
properties (mirroring the drive-default properties) instead of resolving them
at setup. Drops the five _head96_resolve_* helpers and their constructor
kwargs. z_range stays a setup field since its max is a hardware probe.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…etup

The Y/Z drive speed/acceleration defaults were computed as frozen
Head96Information properties from fw_version. Move them to mutable STARBackend
attributes seeded from the machine's registers at setup (via head96_request_*),
so a run can override them through the existing range-checked setters. The
backend getters drop the frozen fallback; the chatterbox overrides the four
register reads with canned 2013+ factory values. Dispensing/squeezer factory
defaults stay on the frozen record (no machine read yet; wired by the dispense
work).

Also rewrites the crash-recovery tests to use mock sequencing/introspection
instead of hand-rolled call counters.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts:
#	pylabrobot/liquid_handling/backends/hamilton/STAR_chatterbox.py
@BioCam BioCam merged commit 8689f0e into PyLabRobot:main Jun 19, 2026
21 checks passed
@BioCam BioCam deleted the head96-speed-accel-pr branch June 19, 2026 08:23
@BioCam

BioCam commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator Author

Thank you @rickwierenga for the thorough review process 😊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants