Conversation
…Hamilton NTRs - `HeadTool(Resource)`: what a channel carries, with `fitting_depth`, `collar_height`, `pick_up_location` and `kind()`. `Tip` is a `HeadTool`; `HamiltonCoreGripperTool` models the CO-RE grip tool. - Tips are children of their `TipSpot` or `TipMountingShaft`, placed from their pick-up location. `Resource.comparable_children()` keeps a carried tool out of equality. The STAR simulator reads tips from the shafts, and the STAR driver keys its tip type table on `kind()`. - `StandingTipRack` with `stacking_z_height`; `ResourceStack` nests standing tip racks; `NestedTipRack` is deprecated. `LiquidHandler` refuses racks with something stacked on them. - Hamilton racks are built on `hamilton_96_tiprack_standard` (renamed from `hamilton_tiprack_standard`) and `hamilton_96_tiprack_ntr`. There are new 10 uL and 300 uL NTRs, the 50 uL NTR's spots are corrected, and `hamilton_tip_carrier_L5_ntr_a00` (renamed from `TIP_CAR_NTR_A00`) and `hamilton_mfx_resource_holder_ntr4` are added. - `test_nested_tip_racks_ground_truth` checks the NTR pick-up and drop firmware against Venus. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1a38c1b to
add331a
Compare
The names point at meshes this branch does not ship; they stay on v1-master-branch, where the meshes are. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A model name says which product a tip is, as a rack's does; it does not need a mesh to exist. This reverts edaaf7e. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…or the racks it holds Renamed from `hamilton_mfx_resource_holder_ntr4`, which has not been merged, so no alias. Comments say where a value comes from as Hamilton's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The modules measured from the deck subtract 18.2, so every rack and plate stays where it was, and the NTR pick-up and drop commands are unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…easured MFX modules are 134 mm across and are modelled as the 135 mm their carrier slot is, since the track's sliding blocks centre them in it. The rack stays where it was, so the NTR pick-up and drop commands are unchanged. Probing with Z-touch and cLLD against the deck put the module top at 38.1 mm and the rack seat at 29.1 mm above the deck, as modelled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The tip rack holder is main's `hamilton_mfx_tiprackholder_standard`, at this branch's 18.2 mm carrier sites; the NTR4 module keeps its own definition, with its rack centred in the module's pocket as the 0.5 mm forward sites need. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The raised racks take this branch's tip spot convention: the 7.2 mm hole, centred on the rack, so their spots stand where they did. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| ops=[ | ||
| Drop( | ||
| self.deck.get_trash_area(), | ||
| tip=tips[3], | ||
| tip=tips[0], | ||
| offset=offsets[0], | ||
| ), | ||
| Drop( | ||
| self.deck.get_trash_area(), | ||
| tip=tips[2], | ||
| tip=tips[1], | ||
| offset=offsets[1], | ||
| ), | ||
| Drop( | ||
| self.deck.get_trash_area(), | ||
| tip=tips[1], | ||
| tip=tips[2], | ||
| offset=offsets[2], | ||
| ), | ||
| Drop( | ||
| self.deck.get_trash_area(), | ||
| tip=tips[0], | ||
| tip=tips[3], | ||
| offset=offsets[3], | ||
| ), |
There was a problem hiding this comment.
this seems wrong? it is dangerously changing the behavior of legacy
| # What the machine has to be told about this tool, in the same tip type table a tip goes in: | ||
| # a grip tool holds no liquid, but the table demands at least 1.0 uL, and the value plays no | ||
| # part in picking the tool up. | ||
| self.has_filter = False | ||
| self.maximal_volume = 1.0 | ||
| self.tip_size = TipSize.UNDEFINED | ||
| self.pickup_method = TipPickupMethod.OUT_OF_RACK |
There was a problem hiding this comment.
why does it have a volume?
|
|
||
| def comparable_children(self) -> List["Resource"]: | ||
| """The children compared in equality: all of them, unless a subclass holds some as state.""" | ||
| return self.children | ||
|
|
||
| def __eq__(self, other): |
There was a problem hiding this comment.
this is ugly. why do we need it?
| if ( | ||
| self.direction == "z" | ||
| and isinstance(upper, Plate) | ||
| isinstance(upper, Plate) | ||
| and upper.stacking_z_height is not None | ||
| and isinstance(lower, Plate) | ||
| and lower.lid is None | ||
| ) or ( | ||
| isinstance(upper, StandingTipRack) | ||
| and upper.stacking_z_height is not None |
There was a problem hiding this comment.
In the future I would like a NestedTipRackStack(ResourceStack) subclass that has a counting feature, but that is for a follow up
| def __hash__(self): | ||
| return hash( | ||
| ( | ||
| self.has_filter, | ||
| self.total_tip_length, | ||
| self.nominal_volume, | ||
| self.maximal_volume, | ||
| self.fitting_depth, | ||
| self._collar_height, | ||
| ) | ||
| ) | ||
|
|
||
| def __eq__(self, other: object) -> bool: | ||
| if not isinstance(other, Tip): | ||
| return NotImplemented | ||
|
|
||
| return ( | ||
| self.has_filter == other.has_filter | ||
| and self.total_tip_length == other.total_tip_length | ||
| and self.nominal_volume == other.nominal_volume | ||
| and self.maximal_volume == other.maximal_volume | ||
| and self.fitting_depth == other.fitting_depth | ||
| and self._collar_height == other._collar_height | ||
| ) |
| } | ||
| for held_by_the_holder in ("location", "parent_name"): | ||
| data.pop(held_by_the_holder, None) |
| @property | ||
| def collar_height(self) -> float: | ||
| """Return collar_height, raising if it is None.""" | ||
| if self._collar_height is None: | ||
| raise ValueError(f"collar_height is not defined for this tool: {self!r}") | ||
| return self._collar_height | ||
|
|
||
| @property | ||
| def has_collar_height(self) -> bool: | ||
| """Whether this tool states the height of its collar.""" | ||
| return self._collar_height is not None | ||
|
|
There was a problem hiding this comment.
why is this like this? why not just check .collar_height is None
| @property | ||
| def is_named(self) -> bool: | ||
| """Whether this tool has a name.""" | ||
| return self._is_named | ||
|
|
There was a problem hiding this comment.
shouldn't resources always have names? what is the reason they dont have it here? locations where tips spawn like tip spots should probably have a counter for making unique names
| @property | ||
| def _available(self) -> bool: | ||
| """Whether nothing, a lid or another rack in its stack, sits on top of this rack.""" | ||
| # A rack's spots are assigned when it is made; anything put on the rack comes after them. |
There was a problem hiding this comment.
this name is a bit confusing. how about _can_accept_op or sth
| def serialize(self) -> dict: | ||
| """Serialize the tip spot.""" | ||
| return { | ||
| """Serialize the tip spot. Its tip is state, not a serialized child.""" | ||
| data = { | ||
| **super().serialize(), | ||
| "prototype_tip": self.make_tip().serialize(), | ||
| } | ||
| data.pop("children", None) | ||
| return data |
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

A tip is not a resource on
main. It lives only inside its spot'sTipTracker, so nothing in the tree says where a tip is, the backends each re-derive its position from the spot, and a channel carrying a tip has no geometry below it. The CO-RE grip tool, which a channel picks up exactly like a tip, has no model at all.StandingTipRackis a stub, and the only nested tip rack, aNestedTipRackfor the 50 µL NTR, has its tip spots where the instrument doesn't put them.This PR
Tipis aHeadToolresourceHeadTool(Resource): what a channel carries. It hasfitting_depth,collar_height,pick_up_location(the centre of the top of the opening the channel enters) andkind(), which is equal for tools a backend can use interchangeably and keys the firmware tip-type tables.Tipis aHeadTool.HamiltonTiptakes its diameter andcollar_heightfromtip_size.HamiltonCoreGripperTool/hamilton_core_gripper_toolmodel the grip tool, withtotal_length= 30 mm to its grip line, as the firmware counts it.TipSpot: centred, with its pick-up locationcollar_heightabove the spot. A mounted tool is a child of itsTipMountingShaft: centred, with its pick-up locationfitting_depthabove the shaft's end.Resource.comparable_children()keeps a carried tool out of equality, so a spot with a tip still equals one without.StandingTipRackstacking_z_height.NestedTipRackis deprecated.ResourceStacknests standing tip racks by theirstacking_z_height, as it does plates.LiquidHandlerrefuses to pick up tips from, or drop tips to, a rack with a lid or another rack on top of it (TipRack._available).Hamilton NTRs
hamilton_96_tiprack_standard: renamed fromhamilton_tiprack_standard, with a deprecated alias. Its spots are the 7.2 mm holes, with the same centres.hamilton_96_tiprack_ntr: an SLAS footprint with SLAS tip positions and spots at the collar seat.hamilton_96_tiprack_10uL_NTRand_300uL_NTRare new._50uL_NTR's spots move from 13.5 to 55 mm.hamilton_tip_carrier_L5_ntr_a00(182074) replacesTIP_CAR_NTR_A00, which remains as a deprecated alias, and keeps Hamilton's sites.hamilton_mfx_module_tiprackholder_ntr(191425) is new.Behaviour:
kind()to compare types);pick_up_location;get_all_children().Tests:
test_nested_tip_racks_ground_truth: for the 10, 50 and 300 µL NTRs, on both the MFX NTR4 module and the NTR carrier, the channel and 96-head pick-up and drop commands match what Hamilton's own software sends.TipSpotand shaft placement, grip tool geometry,kind(), stacked-rack nesting and the covered-rack guard.Full suite passes (3410 passed, 4 skipped).
ruff format,ruff check --select I,ruff checkandtyposare clean.mypyshows only the existing LI-COR test errors onmain.This touches
TipSpot.__init__next to #1251, so whichever lands second carries a small merge.🤖 Generated with Claude Code