You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat(track): choose the tracker: ultrack, tracksdata, HOCT or Trackastra - #427
biahub track can now choose its tracker. Besides ultrack (still the default), a track config can set:
tracker: tracksdata: tracksdata's ILP on the cellpose labels (region nodes, distance + IoU edges).
tracker: hoct: HOCT's learned edge scores, then an ILP.
tracker: trackastra: Trackastra's pretrained transformer predicts the links (greedy or ILP).
These three link the instance labels of the label image biahub segment wrote (labels:, the default segmentation_method: labels) as they are, so they need output_mode: 2D on a projected (Z=1) plate; foreground_contour and 3D are refused. Every tracker writes into the plate like ultrack after #413: the tracked labels labels/tracks_<labels> (value = track id; or labels/<output_label>) and tracks_<fov>.csv. The sibling filter (#415) applies to all of them. The tracked labels are the final segmentation and are preferred downstream over labels/<labels>; only ultrack changes the masks (it picks among its own segmentation hypotheses). Unlike ultrack they write no tracks.geff yet (#441), and they remove a stale one left by an earlier ultrack run.
labels: nuclei # the label image `biahub segment` wrotetracker: trackastra # ultrack (default) | tracksdata | hoct | trackastratrackastra_config:
model: general_2d # pretrained name or a trained model foldermode: greedy # greedy | greedy_nodiv | ilp# image_channel: nuclei_prediction # default: the channel segment recorded# tracksdata_config: {distance_threshold_um, n_neighbors, distance_norm_um, node/appearance/disappearance/division weights, border_margin_um, ...}# hoct_config: {model: general_v1, distance_threshold_um, n_neighbors, max_delta_t, solver}
Distances in um, converted with the plate's pixel size. Defaults are the A549 benchmark's settings at 0.1133 um/px; tuning comes in a follow-up.
HOCT and Trackastra read the image the labels were segmented from: the first channel biahub segment recorded in the label image's biahub-segment provenance, or hoct_config.image_channel / trackastra_config.image_channel when set (to override it, or for a label image segment did not write). --init checks that the channel exists. HOCT runs in pixel space (as its models were trained); HOCT 0.3 promotes 2D movies to (T, 1, Y, X) internally, and the conversion squeezes it back.
GPU: when biahub track submits the jobs, hoct and trackastra get a gpu node; ultrack and tracksdata stay on the CPU preempted partition.
Model weights (HOCT, Trackastra) are downloaded once by --init.
One-child links: HOCT links across skipped frames, and converting its solution (or Trackastra's CTC table) can leave a parent with one child, which the sibling filter refuses. Such a child is merged into its parent, in the tracks and the labels.
A new skill, track-with-biahub, runs and compares trackers on a segmented projected plate: one A549 config template per tracker (each with its own output_label, so runs sit side by side in the plate), a run script, and compare_tracks.py (counts, and division precision/recall against TrackEdit annotations).
Dependencies
tracksdata (git pin: no stable release yet), hoct>=0.3 and trackastra[ilp] join the existing track extra. The lock only adds packages, except gurobipy 13.0.3 -> 12.0.3 (HOCT requires <13); Gurobi licences work with older versions. Gurobi is optional: without a licence nothing fails. ultrack falls back to CBC; tracksdata, HOCT and Trackastra's ilp mode (via ilpy) use SCIP; Trackastra's default greedy mode needs no ILP solver. Biohub HPC has a Gurobi token-server licence, but each user must be added to it; then ~/gurobi.lic with TOKENSERVER=license01.czbiohub.org enables it for ultrack. SCIP can be slow on larger problems (cuOpt is an alternative, not wired in). ilpy does not: it takes LicenseID == 0 to mean gurobipy's size-limited pip licence, and a token-server licence also reports 0, so the ilpy trackers use SCIP even with it. That is the Solver failed with Gurobi, trying Scip warning in the runs below (not a GPU-node limitation).
Real data
The A549 track config of #415 (60-plane focus window, cellpose cpsam_v2, min_sibling_length: 3), changing only tracker:; untuned defaults for the new trackers.
2026_04_28 (4 TrackEdit FOVs, 78 divisions):
tracker
division F1
precision
recall
mock F1
DENV F1
tracks (GT 287)
ultrack
0.68
0.60
0.79
0.70
0.63
376
trackastra
0.63
0.71
0.57
0.65
0.60
355
hoct
0.53
0.68
0.43
0.59
0.47
311
tracksdata
0.52
0.47
0.57
0.48
0.55
331
2026_04_21 (5 DENV FOVs, 38 divisions from mitosis labels):
tracker
mitotic cells detected
divisions found
predicted divisions
tracks
ultrack
0.95
19
80
382
trackastra
0.95
14
45
419
hoct
0.96
7
21
334
tracksdata
0.94
17
76
344
ultrack stays the best default. Trackastra and HOCT are more conservative (higher precision, fewer divisions); tracksdata needs tuning. Each new tracker took ~13-16 min for 4-5 FOVs on one GPU node, cellpose included, ~11 GB peak.
Tests
Written failing first (21 new tests, counting parametrized cases): settings (tracker choice, 2D required, um defaults, unknown keys), each tracker on a synthetic movie with one division, um -> pixel conversion and the image passed to HOCT, an end-to-end biahub track --cluster debug run per tracker, --init warming the weights, one-child merging (gaps, chains, divisions kept), and the skill templates loading on the current schema. HOCT's and Trackastra's models are stubbed in CI; with their weights cached, real-model tests also run (both find the synthetic division). Full suite: 637 passed, 13 skipped; ruff clean.
main gained #424 (biahub project), #413 (biahub segment writes the labels; track reads labels: and writes into the plate) and #430 (pyramids) after this PR was opened, so the trackers were reworked on top of them as described above. The settings, outputs, tests and the track-with-biahub skill all follow the new flow, and the real-data tables above predate the merge (in-track cellpose).
Tests: full suite 735 passed, 13 skipped; ruff clean.
Real data after the merge: v11 2026_08_11_A549_SEC61B_DENV_1_proj.zarr C/2/000000, using the same labels/nuclei from biahub segment for every tracker, with min_sibling_length: 3 and untuned defaults. Each ran in-process on one GPU node.
tracker
tracks
divisions
detections kept (of 1461)
mean track length
time
ultrack (the v11 pipeline run)
200
22
94.3%
6.9
—
tracksdata
226
54
90.7%
5.9
82 s
hoct
87
15
98.9%
16.6
86 s
trackastra
197
13
95.3%
7.1
23 s
For every tracker, each frame's label ids equal the CSV's track ids for that frame. HOCT and Trackastra read nuclei_prediction, which they resolved from segment's provenance.
biahub track gains two trackers that link fixed labels. tracksdata is pinned to
the commit the A549 benchmark used (no stable release yet). HOCT requires
gurobipy<13, which moves the lock from gurobipy 13.0.3 to 12.0.3; Gurobi
licences work with older versions, and both ILPs fall back to SCIP without one.
A track config can set tracker: tracksdata (ILP on the cellpose labels) or
tracker: hoct (HOCT's learned edge scores, then an ILP) instead of ultrack.
Each has a config block with distances in um, converted with the plate's pixel
size; defaults are the A549 benchmark's settings. Both need cellpose labels and
output_mode 2D, write the same tracks CSV and labels as ultrack, and go through
the sibling filter. HOCT runs in pixel space as its models were trained, sees the
image cellpose segmented, and --init downloads its weights once.
tracker: trackastra links the cellpose labels with Trackastra's pretrained
transformer (general_2d by default, or a trained model folder), greedily or with
an ILP (motile). Its graph_to_ctc relabels the masks by track; the tracks table
takes each track's centroid per frame, so the output matches the other trackers.
--init downloads its weights once. trackastra[ilp] joins the track extra; the
lock adds packages only.
HOCT links across skipped frames, and converting its solution (or Trackastra's
CTC table) splits such a track into parent -> one child. ultrack's sibling filter
accepts only 0 or 2 children, so the A549 HOCT run failed there. The child now
takes its parent's id in the tracks and labels; divisions are kept.
A skill to track a plate with biahub track and choose the tracker: one A549
config template per tracker (identical but for tracker:), a GPU run script, and
compare_tracks.py for per-tracker counts and, with TrackEdit annotations,
division precision/recall (it reproduces the benchmark scorer within 0.01 F1).
References cover each tracker's settings, the solver fallback and known
pitfalls, and how to read a comparison. A test keeps the templates on schema.
… matches
The newest annotation version is chosen in natural order, and division F1 is
2 * matched / (predicted + annotated), so a tracker with no matching or no
predicted divisions scores 0 instead of NaN.
…ad count
A label tracker with segmentation_method: cellpose but no cellpose_config used to
pass validation, then crash at --init or fall back to ultrack. The ILP thread
count uses SLURM_CPUS_PER_TASK, then CPU affinity where the platform has it,
then os.cpu_count().
…le movie
With no candidate edges (one frame, or objects beyond distance_threshold_um) the
pinned tracksdata solver refuses to solve and the position failed after
segmentation; each detection is now kept as its own track. compare_tracks counts
detections over the movie's full length, so frames a tracker leaves empty lower
its cells per frame.
Handle both empty detections and empty solver results. With an all-background movie, the pinned RegionPropsNodes never registers y/x, so the border-coordinate lookup in _solve_tracksdata fails before the no-edge guard. Even with detections and candidate edges, the solver can select nothing: under the defaults, a stationary interior cell in two frames costs 5, versus 0 for an empty solution. This exporter then raises ValueError("Graph is empty"). Return zero labels and an empty tracks table with the normal typed columns for both cases, and bypass sibling filtering for empty tracks so those results can reach the CSV and label writers. Add regression coverage with the sibling filter enabled.
This tuning instruction is reversed: tracksdata minimizes the objective, and division_weight is a division cost. Increasing it discourages divisions rather than allowing more, as references/trackers.md:23 correctly states. Correct the comment so users tuning missed divisions do not move the parameter in the wrong direction.
…to track
An all-background movie failed before solving, and a solution that selects
nothing (it can be the cheapest one) failed in the conversion. Both now give zero
labels and an empty tracks table with the usual columns, and the sibling filter
is skipped for empty tracks, so the CSV and labels are written.
…eight hint
Division F1 is indexed by run, so comparing annotated and unannotated runs no
longer fails. The tracksdata template's comment had the direction reversed:
division_weight is a cost, lower it to allow more divisions.
Re the two previously-missed findings: empty detections and empty solutions now give zero labels and an empty tracks table (the sibling filter is skipped for empty tracks), with tests for an empty movie, a solution that selects nothing, and an end-to-end run per tracker with the filter on. The tracksdata template's division_weight comment is corrected (a cost: lower it to allow more divisions).
HOCT 0.3.0 skips each inference window with at most one candidate link; when it
skips them all (e.g. links only at frames 0->1 and 18->19 of a long movie) it
fails on an empty prediction list. biahub then keeps every detection and its
candidate links (each is the only option where it is), one incoming link per
detection, and says so in the log.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
The three new backend integrations and solver compatibility changes warrant final human validation of real-model execution beyond the primarily stubbed CI coverage.
Real-model validation, since CI stubs the HOCT and Trackastra models:
Local run with the weights cached (HOCT general_v1, Trackastra general_2d), at 43ea1d8: the 5 real-model tests pass (Trackastra and HOCT each find a synthetic division; HOCT's solver path with an override; a single candidate link; temporally separated links).
Real data, end to end through biahub track --cluster debug on GPU nodes (SCIP, as Gurobi is not licensed there): all three trackers on the 4 TrackEdit FOVs of 2026_04_28 and 5 FOVs of 2026_04_21; scores in the description (e.g. 04_28 division F1: ultrack 0.68, Trackastra 0.63, HOCT 0.53, tracksdata 0.52). The HOCT run is what surfaced the one-child-link fix (77fc383).
main moved segmentation out of track (#413: `biahub segment` writes labels/<model>
into the plate; track reads `labels:` and writes labels/tracks_<labels>, tracks.geff
and the CSV into it) and Z projection into `biahub project` (#424), and added
pyramids (#430). The trackers of this PR are reworked on top of that:
- tracksdata, hoct and trackastra link the instance labels of `labels:` (the
default segmentation_method: labels) instead of running cellpose in track;
foreground_contour and output_mode 3D are refused for them.
- hoct and trackastra read the image the labels were segmented from: the first
channel `biahub segment` recorded in the label image's biahub-segment
provenance, or hoct_config/trackastra_config.image_channel when set (override,
or for a label image segment did not write). --init checks it exists.
- Their outputs follow #413: labels/<tracked_label> (5D) and the CSV. They write
no tracks.geff yet (ultrack's export reads its own database; #441) and remove a
stale one from an earlier run.
- hoct and trackastra request a GPU when biahub submits them; ultrack and
tracksdata stay on the CPU partition.
- pyproject: main's track extra plus tracksdata (git), hoct and trackastra[ilp],
with the gurobipy fork by Python version; uv.lock regenerated from main's.
- Tests and the track-with-biahub skill moved to labels written into the plate
(project -> segment -> one track run per tracker, side by side by output_label).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@tayllatheodoro heads-up: I merged main into this branch (16fcd4e, a merge commit, no force-push). #424, #413 and #430 landed after this PR opened, and they changed what the trackers build on, so this is more than a conflict fix:
Input: tracksdata, hoct and trackastra now link the label image biahub segment writes (labels: <model>) instead of running cellpose in track. segmentation_method: cellpose, cellpose_config and z_slicing are refused in track on main.
Image for hoct and trackastra: the first channel segment recorded for that label image, or a new image_channel in their configs, which overrides it or covers a label image segment didn't write.
@tayllatheodoro how is gurobi licensing handled? Would Sookie or Allyson be able to run the pipeline (in general) without a gurobi license? Would they be able to run tracking without a license? Is it just slower or does it fail? I don't think I have a gurobi license and for me tracking runs within a minute - is gurobi absolutely necessary or can it be an optional add-on?
@ieivanov Gurobi is optional: tracking never fails without a licence, it falls back to an open-source solver. Biohub users can get the Gurobi licence through the Biohub licence server: a ~/gurobi.lic containing TOKENSERVER=license01.czbiohub.org.
I checked with and without a licence (licence file pointed to a nonexistent path, on the same ~5000-variable test problem):
licence (TOKENSERVER=license01.czbiohub.org)
no licence
ultrack (python-mip)
Gurobi
warns "Gurobi environment could not be loaded", then uses CBC and solves
tracksdata, hoct, trackastra ilp (ilpy)
SCIP (see below)
SCIP, solves
trackastra greedy (default)
no ILP solver
no ILP solver
So Sookie or Allyson can run the pipeline, tracking included, without a licence. The difference is speed (and solution quality within the time limit) on larger problems, which is likely why your v11 FOV ran within a minute without one. Even with the token server, ilpy reported "Gurobi license is not available" and used SCIP. That is what the "Solver failed with Gurobi, trying Scip" warnings in the tracksdata/HOCT runs were, not a GPU-node issue as the description says; I'll correct it.
@JoOkuma could you add context on how much Gurobi vs CBC/SCIP matters in practice for ultrack and the ILP trackers (speed, and solution quality within the time limit on large movies)? And is ilpy expected to pick up a token-server licence?
… token-server licence
ultrack uses Gurobi when licensed (Biohub token server) and CBC otherwise.
ilpy reads LicenseID 0 as gurobipy's size-limited pip licence, and token-server
licences also report 0, so tracksdata, HOCT and Trackastra's ilp mode solve
with SCIP either way. The skill blamed the GPU nodes; correct it.
@ieivanov thanks for porting this onto #424/#413. I went through the merge, and it looks right to me:
Input and image: HOCT and Trackastra resolve the image channel from segment's provenance (or image_channel). They see the same raw projected image as before the merge and normalize it themselves (HOCT normalize_image, Trackastra normalize_imgs=True), so the benchmark numbers in the description should carry over.
Frames and channels: blank frames are filled in the image as in the labels, so they stay frame-aligned. A missing channel is caught at --init.
Settings checks: a config with no labels, foreground_contour or 3D is refused for these trackers, and old cellpose_config / z_slicing configs get a clear error.
Outputs and jobs:
A stale tracks.geff is removed.
The full settings, tracker included, are recorded on the tracked label image by --init.
The hoct/trackastra jobs go to gpu.
Tests: the tracker tests pass locally (129: backends, skill, example settings, track CLI).
Small things:
Gurobi wording (fixed in e65d803, docs and comments only): the skill blamed the GPU nodes for the SCIP fallback. The real cause is ilpy's licence check (LicenseID == 0, which token-server licences also report). The PR description now says the same.
Image channel checked on one position:--init checks the channel on the first position only, so a plate whose other positions lack it would fail inside the jobs. Minor, fine to leave.
A question on segmentation: ultrack does not keep the segment labels as they are. It builds a hierarchy from their contours and picks the segments that track best, which corrects some cases (e.g. merging an over-split nucleus, or splitting two touching ones). tracksdata, HOCT and Trackastra link the cellpose labels unchanged. So:
the tracked label image is a different segmentation depending on the tracker;
comparing trackers mixes segmentation correction with linking (on your v11 FOV, the share of the 1461 detections each tracker kept ranged from 90.7% to 98.9%, ultrack 94.3%);
downstream steps that read labels/tracks_<labels> get ultrack-corrected or uncorrected masks depending on the config.
How would you like to handle this? Options I see:
(a) treat the tracked labels as the final segmentation, and document that only ultrack corrects it;
(b) let the label-linking trackers optionally take ultrack's corrected labels as input;
(c) keep the segment labels authoritative, and record per tracker whether the tracked labels can differ from them.
@JoOkuma could you add context on how much Gurobi vs CBC/SCIP matters in practice for ultrack and the ILP trackers (speed, and solution quality within the time limit on large movies)?
Gurobi is faster and uses less memory than CBC or SCIP.
CBC is only used in ultrack as a fallback.
SCIP is the new fallback because it's easier to share, but it's really bad and can easily take too long to solve moderately large problems, which might or might not be your case, so you need to test.
The OPS project has been using cuOPT backend to avoid using gurobi, see royerlab/hoct#9, it might be a good alternative.
All ILPs, if solved to the same GAP, should yield similar results, though some might take much longer to achieve this.
And is ilpy expected to pick up a token-server licence?
HPC has a token server license where each user needs to be added manually.
Thanks Taylla! I think we should "treat the tracked labels as the final segmentation, and document that only ultrack corrects it". Segmentation and tracking a coupled problems. Within biahub all trackers output a labels layer, so any tracker could have the ability to edit segmentation masks. In the presence of tracking, labels/tracks_<labels> should be preferred over labels/<labels>.
…server is per user
Only ultrack changes the masks; tracksdata, HOCT and Trackastra keep the
segment labels, and downstream steps should prefer labels/tracks_<labels>
over labels/<labels>. The Biohub Gurobi token server needs each user added;
SCIP can be slow on larger problems, cuOpt is an alternative.
@ieivanov thanks, it's ready for review now. ultrack stays the default tracker (in biahub and in the A549 track.yml); the other three are opt-in with untuned defaults.
Plan after this merges
Choose the A549 default tracker and its parameters from the tuning we're running on the annotated data, and set them in a follow-up PR (biahub defaults + nextflow/configs/a549/track.yml). The tuning so far, with settings chosen on 04_28 + 07_24 and checked on held-out 08_26 and the mitosis-labelled FOVs:
HOCT and Trackastra need the input downsampled, since they were trained on much smaller nuclei. HOCT general_v1 shrunk 6x: held-out division F1 0.58 vs ultrack's 0.55, and mitosis recall 0.72 vs 0.64.
Fine-tuning: Trackastra fine-tuned on our TrackEdit data (4x): held-out F1 0.57, mitosis recall 0.71. Fine-tuning HOCT's edge head didn't help.
tracksdata overfits: its best tuning settings drop to 0.46 on held-out data.
Swaps are rare for every tracker on these FOVs (0-2 per tracker).
Caveat: the held-out set is small (34 divisions). Each tracker is now also being tested with the division classifier on top.
Keep testing the default on the current annotations and on new ones as they come in.
Then extend the track-with-biahub skill so users can run and evaluate tracking on new kinds of data (other cell lines, markers, magnifications) and set up their own pipeline.
Your guidance on decisions
What should decide the default? Division F1 on held-out data, fragmentation and ID switches, or also cost: HOCT and Trackastra need a GPU and model weights, while ultrack runs on CPU. And what margin over ultrack would justify switching, given how small the held-out set is?
Downsampling HOCT/Trackastra: OK to add as a downsample setting in their configs, with tracking done at the coarser scale and the result mapped back to the full-resolution labels?
Three-way divisions happen in our cells, but ultrack's sibling filter only accepts 0 or 2 children; HOCT produced one and crashed the filter. Options: the filter skips tracks with 3+ children, or it treats each daughter separately. Do you have a preference?
The division classifier (a random forest that removes false divisions; on A549 division F1 goes 0.68 -> 0.82 on 04_28, 0.47 -> 0.68 and 0.41 -> 0.73 on the two v1 datasets) is trained on only 4 annotated FOVs, so it comes in a later PR, after more annotation. For when we get there: would you put it inside biahub track or in a separate command? And should biahub project write the phase-texture image it uses (Z std of Phase3D over the focus window)?
@ieivanov one more question on scope: should we merge this PR now (ultrack as default, the other trackers opt-in) and do the plan above in follow-up PRs (tuned defaults + downsample + A549 track.yml, then the classifier after more annotation)? Or would you rather keep everything in this PR until the default is settled? We're happy either way; merging now keeps each PR smaller to review.
@ieivanov could you approve and merge this when you have a moment? ultrack stays the default.
The tuning results so far don't justify changing the default yet. On data no method was tuned on (08_26, 34 divisions; 04_21 mitosis labels), HOCT general_v1 shrunk 6x and a fine-tuned Trackastra are slightly ahead of ultrack, but within noise:
Thanks @ieivanov! Follow-up discussion is in #444 (default tracker, downsample for HOCT/Trackastra, three-way divisions, division classifier). cpdino as a segmentation option is in #445.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
biahub trackcan now choose its tracker. Besides ultrack (still the default), a track config can set:tracker: tracksdata: tracksdata's ILP on the cellpose labels (region nodes, distance + IoU edges).tracker: hoct: HOCT's learned edge scores, then an ILP.tracker: trackastra: Trackastra's pretrained transformer predicts the links (greedy or ILP).These three link the instance labels of the label image
biahub segmentwrote (labels:, the defaultsegmentation_method: labels) as they are, so they needoutput_mode: 2Don a projected (Z=1) plate;foreground_contourand 3D are refused. Every tracker writes into the plate like ultrack after #413: the tracked labelslabels/tracks_<labels>(value = track id; orlabels/<output_label>) andtracks_<fov>.csv. The sibling filter (#415) applies to all of them. The tracked labels are the final segmentation and are preferred downstream overlabels/<labels>; only ultrack changes the masks (it picks among its own segmentation hypotheses). Unlike ultrack they write notracks.geffyet (#441), and they remove a stale one left by an earlier ultrack run.biahub segmentrecorded in the label image'sbiahub-segmentprovenance, orhoct_config.image_channel/trackastra_config.image_channelwhen set (to override it, or for a label image segment did not write).--initchecks that the channel exists. HOCT runs in pixel space (as its models were trained); HOCT 0.3 promotes 2D movies to (T, 1, Y, X) internally, and the conversion squeezes it back.biahub tracksubmits the jobs, hoct and trackastra get agpunode; ultrack and tracksdata stay on the CPUpreemptedpartition.--init.track-with-biahub, runs and compares trackers on a segmented projected plate: one A549 config template per tracker (each with its ownoutput_label, so runs sit side by side in the plate), a run script, andcompare_tracks.py(counts, and division precision/recall against TrackEdit annotations).Dependencies
tracksdata (git pin: no stable release yet),
hoct>=0.3andtrackastra[ilp]join the existingtrackextra. The lock only adds packages, except gurobipy 13.0.3 -> 12.0.3 (HOCT requires<13); Gurobi licences work with older versions. Gurobi is optional: without a licence nothing fails. ultrack falls back to CBC; tracksdata, HOCT and Trackastra'silpmode (via ilpy) use SCIP; Trackastra's defaultgreedymode needs no ILP solver. Biohub HPC has a Gurobi token-server licence, but each user must be added to it; then~/gurobi.licwithTOKENSERVER=license01.czbiohub.orgenables it for ultrack. SCIP can be slow on larger problems (cuOpt is an alternative, not wired in). ilpy does not: it takesLicenseID == 0to mean gurobipy's size-limited pip licence, and a token-server licence also reports 0, so the ilpy trackers use SCIP even with it. That is theSolver failed with Gurobi, trying Scipwarning in the runs below (not a GPU-node limitation).Real data
The A549 track config of #415 (60-plane focus window, cellpose cpsam_v2,
min_sibling_length: 3), changing onlytracker:; untuned defaults for the new trackers.2026_04_28 (4 TrackEdit FOVs, 78 divisions):
2026_04_21 (5 DENV FOVs, 38 divisions from mitosis labels):
ultrack stays the best default. Trackastra and HOCT are more conservative (higher precision, fewer divisions); tracksdata needs tuning. Each new tracker took ~13-16 min for 4-5 FOVs on one GPU node, cellpose included, ~11 GB peak.
Tests
Written failing first (21 new tests, counting parametrized cases): settings (tracker choice, 2D required, um defaults, unknown keys), each tracker on a synthetic movie with one division, um -> pixel conversion and the image passed to HOCT, an end-to-end
biahub track --cluster debugrun per tracker,--initwarming the weights, one-child merging (gaps, chains, divisions kept), and the skill templates loading on the current schema. HOCT's and Trackastra's models are stubbed in CI; with their weights cached, real-model tests also run (both find the synthetic division). Full suite: 637 passed, 13 skipped; ruff clean.Not in this PR
Updated from main (merge 16fcd4e)
main gained #424 (
biahub project), #413 (biahub segmentwrites the labels; track readslabels:and writes into the plate) and #430 (pyramids) after this PR was opened, so the trackers were reworked on top of them as described above. The settings, outputs, tests and thetrack-with-biahubskill all follow the new flow, and the real-data tables above predate the merge (in-track cellpose).2026_08_11_A549_SEC61B_DENV_1_proj.zarrC/2/000000, using the samelabels/nucleifrombiahub segmentfor every tracker, withmin_sibling_length: 3and untuned defaults. Each ran in-process on one GPU node.For every tracker, each frame's label ids equal the CSV's track ids for that frame. HOCT and Trackastra read
nuclei_prediction, which they resolved from segment's provenance.