Skip to content

Optional lastz scoring by divergence: a table, or per-pair last-train models (prototype) - #2027

Closed
glennhickey wants to merge 6 commits into
masterfrom
lastz-score-models
Closed

glennhickey wants to merge 6 commits into
masterfrom
lastz-score-models

Conversation

@glennhickey

@glennhickey glennhickey commented Oct 4, 2026 •

Copy link
Copy Markdown
Collaborator

Prototype for scoring lastz by divergence. lastz scores every pair with its default, HOXD70, whether the two genomes are 0.01 or 1.5 apart. Draft. Per-pair training is on by default (<blast lastzTrain="1">); the table is opt-in.

What it adds

A table of lastz scoring models by distance. Any number of these, in <blast>:

<lastzScoreModel maxDistance="0.08" matrix="A:A A:C ... T:T (16 scores)" gapOpen="744" gapExtend="22"/>

A pair is aligned with the model of smallest maxDistance that covers its distance (the same distance that picks its lastzArguments), and with HOXD70 past the last one. The model goes to lastz as a --scores file. lastz lets the command line override such a file, so each divergence class keeps its thresholds. Given HOXD70 itself as a model, lastz's output is byte-identical to the default.

Per-pair training (<blast lastzTrain>, on by default).

  • When: each ancestor's blast step first trains a model with last-train on every pair of genomes it will align with lastz: the ingroup pairs, and each ingroup against each outgroup. Every lastz job for that pair then scores with it.
  • Whole genomes: training uses the full sequences. Outgroups after the first are aligned against a trimmed ingroup (whatever earlier outgroups left), so training there would fit leftovers.
  • Scale: each model is rescaled to HOXD70's λ, so lastz's thresholds and the chaining's, which are in HOXD70's units, keep their meaning.
  • Fallback: if the smaller genome has under 500 kb of sequence, or training fails, the pair falls back to the table, else HOXD70.
  • Options: lastzTrainGaps="0" keeps HOXD70's gap costs with the trained matrix; lastzTrainCores sets the threads, up to what Toil allows a job (--maxCores, or a single machine's cores).
  • Repeat masking: training uses the inputs' own soft-masking (lastdb -R10, which lastal follows) rather than LAST's default of masking only simple repeats with tantan. Cactus's Red preprocessing provides that masking, the reconstructed ancestors keep it, and it's also what lastz itself doesn't seed in.

Training doesn't apply to KegAlign, minimap2 or FastGA; their alignments keep their default scoring, and KegAlign warns once per ancestor. A lastzScoreModel table with KegAlign still stops with an error.

Evolver mammals

Pooled leaf-pair precision / recall against the truth. bar is stock abPOA unless noted.

lastz scoring P / R
HOXD70 (shipped) 0.816 / 0.740
table from an evolver last-train ladder (0.013–0.60) 0.829 / 0.743
same, matrices only (HOXD70 gaps) 0.826 / 0.744
same, gaps only (HOXD70 matrix) 0.821 / 0.740
per-pair training 0.830 / 0.744
per-pair training, matrix only 0.829 / 0.745
pecan per-pair models in bar (#2024), lastz HOXD70 0.841 / 0.755
pecan per-pair models + lastz table 0.854 / 0.759
pecan per-pair models + per-pair lastz training 0.856 / 0.760
  • Gain: the trained matrices are what help, mostly by penalising mismatches, transitions especially, much harder than HOXD70 at short and moderate distances. The gain adds to bar's.
  • Cost: ancestor recall falls 0.4–0.9 points at the higher ancestors, while ancestor precision and base accuracy rise everywhere. Matrices without the trained gaps lose about half as much recall.
  • Primates: evolver primates don't move (0.9943/0.9863 to 0.9945/0.9863).
  • Ancestor pairs: this is where per-pair models differ most from the table, because the table's distance adds the ancestors' heights. The two ancestors at the root trained to 93.6% identity; the table gave them its most distant model, fit for about 65%.
  • Reproducibility: training is reproducible. The first ancestors' blast output was byte-identical across runs.

Cost on real genomes

Whole genomes from the 577-way tree, 4 threads. The mouse database was split into 4 GB volumes to fit a 31 GB machine.

pair (raw tree distance) total CPU, LAST's default masking with the inputs' masking (-R10) model change two equally good samples differ by
human vs mouse (0.33) 4,494 s 991 s 5.6% 8.3%
human vs fish (1.50) 1,144 s 753 s 21.7% 16.8%
two mouse assemblies (0.0003) 4,148 s 936 s identity 99.35% → 100.00% 4.7%

"Model change" is the largest relative change among the eight distinct lastz parameters.

  • Two mouse assemblies: with interspersed repeats left in, the default masking trained on alignments between repeat copies. The 100% with the inputs' masking is the right answer for two assemblies of one species.
  • Evolver: accuracy was the same either way (0.830/0.744).
  • For the 577-way: about 4,020 pairs, roughly 800–1,100 CPU-hours (about 0.3% of the 329,000 CPU-hours its lastz took). Training adds about 7 minutes before a mammal ancestor's lastz can start.

Ruled out, as the models moved well beyond that noise:

  • Capping the database: 7–76% model change. The sampled query loses its true ortholog, so the model fits paralogs (close pairs) or junk (distant pairs).
  • Sparse seeding (-uRY4): 11–66% change.

A smaller query sample stays within the noise but is no longer worth it, since the passes are now cheap.

LAST

With this PR, progressive cactus needs LAST by default. LAST is now built from a submodule (upstream 1651) rather than by downloadPangenomeTools, as of #2034, which is merged.

This uses only stock LAST: 1651 gives the same models. last-train is given --tolerance=1 when it has it (a patch we drafted, not in LAST). That saves a few passes, which now cost little.

Testing

The evolver runs were made on this code on top of the bar work in #2024. It was then rebased onto the current master, which has since gained #2029 (90 Mbp lastz chunks), #2030 (Red -sketch) and pangenome-only changes. The blast code (local_alignment.py, paf.py, common.py) is identical between the two branches.

CI caught one failure: testEvolverPrepareToil. cactus-prepare's Toil workflow runs each cactus-blast with --maxCores 1, and Toil refused the training job's 4 cores. Training now asks for no more cores than Toil allows a job, through a cactus_clamp_cores that works like cactus_clamp_memory. With that fix, the test's cactus-prepare-toil workflow ran to completion locally and passed the test's accuracy check. That run was on #2024's branch, which carries the same blast code. The test's last step, cactus-phast, wasn't run locally, as it needs phast tools that aren't installed here.

Not in this PR

  • A table built from real genomes, as a fallback for pairs too small to train. Real-data validation of the defaults needs the 577-way pilot.
  • Sharing one database across all of an ingroup's pairs, which would cut training CPU by roughly 2–3×.

🤖 Generated with Claude Code

glennhickey and others added 6 commits October 9, 2026 08:03
lastz scored every pair with its default HOXD70, however diverged.
<blast> can now hold any number of <lastzScoreModel maxDistance= matrix=
gapOpen= gapExtend=>.  A pair is aligned with the model of smallest
maxDistance its distance is within -- the distance that picks its
lastzArguments -- written to a scores file and passed as --scores, and
with HOXD70 past them all.  lastz lets the command line override a
scores file, so each divergence class keeps its thresholds.  None are
configured by default, so nothing changes unless a config adds them.
KegAlign does not take them, and a run that asks for both stops rather
than ignoring the models.

HOXD70 given as a model gives byte-identical lastz output to the
default.  On evolver mammals, with bar stock, a table of last-train
models trained on evolver pairs from 0.013 to 0.6 apart, rescaled to
HOXD70's lambda, raised pooled leaf-pair precision from 0.816 to 0.829
and recall from 0.740 to 0.743; ancestor precision rose and recall fell,
by up to 0.9 points.  Evolver primates were unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With <blast lastzTrain="1">, each ancestor's blast step first trains a
model with last-train on every pair of genomes it will align with lastz
-- the ingroup pairs, and each ingroup against each outgroup -- and every
lastz job for the pair scores with it, rescaled to HOXD70's lambda so
that lastz's thresholds and the chaining's mean what they did.  Training
uses the whole genomes: the ingroup aligned to an outgroup after the
first is only what earlier outgroups left of it.  A pair whose smaller
genome has under 500kb, or whose training fails, falls back to the
lastzScoreModel table, else HOXD70.  lastzTrainGaps="0" keeps HOXD70's
gap penalties with the trained matrix; lastzTrainCores sets the threads.
last-train is given --tolerance=1 when it has it (a proposed LAST patch).

last_scoring.py gets the conversion: last-train's fine-scale model,
which parse_train_file's integer one is too coarse to rescale, put on
HOXD70's scale.  It gives the evolver ladder's models exactly as the
table experiments had them.

On evolver mammals all 18 pairs trained, about 9s each.  Per-pair
models matched or beat the table built from the same evolver pairs:
pooled leaf-pair precision/recall 0.830/0.744 against the table's
0.829/0.743 with stock bar, and 0.856/0.760 against 0.854/0.759 with
pecan's per-pair models (HOXD70 with stock bar: 0.816/0.740).  Where
they differ most is pairs with a reconstructed ancestor: the two
ancestors at the root trained to 92.9% identity, where the table, going
by a distance with both ancestors' heights added, gave them its most
distant model, for ~57%.  Training is reproducible: the first ancestors'
blast output was byte-identical between two runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The training job's memory, disk and walltime were asked for by file size,
which for a gzipped fasta (as an ancestor's is) is about a quarter of its
bases, and for a plain one counted the bases four times over in memory.
They are now by estimated bases, a gzipped fasta counting four per byte:
lastdb's database is about 4.6 bytes per base (6.3 GB for a 1.4 Gb fish
genome) and lastdb, and lastal in training, hold it all.  The same
estimate picks the smaller genome as the database.  On evolver mammals
the blast output was byte-identical to before.

The identity logged with a trained model was the last one last-train
prints, which is the one its final integer scores imply: their rounding
moved it from the fitted 69.9% to 76.2% for human against mouse.  It is
now the last pass's, from the alignments it counted.  The scores lastz
gets are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lastdb -R10 in place of LAST's default -R01: the inputs' soft-masking,
which cactus's Red preprocessing puts there and the reconstructed
ancestors keep, masks repeats from seeding, as it does for lastz, and
lastal follows lastdb's -R.  The default uppercased the inputs and
masked only simple repeats, with tantan, which was 29% of lastdb's time,
and left interspersed repeats seeding.

On whole genomes (4 threads) human against mouse took 991 CPU-s in all
rather than 4494, the training passes 135 rather than 1916, and the model
moved 5.6%, inside the 8.3% two equally good samples differ by; human
against a fish took 753 rather than 1144, moving 21.7% against 16.8%.
Two mouse assemblies trained to 100.00% identity rather than 99.35%:
the default had been fitting alignments between repeat copies.  On
evolver mammals the alignment was as accurate as before (pooled
leaf-pair precision/recall 0.830/0.744 both ways).

LAST 1651, as cactus installs it, gives the same models as the patched
last-train with --tolerance, in a few more passes that now cost little.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
<blast lastzTrain> is now on.  It passes over the mappers it does not
apply to rather than stopping them: minimap2 and FastGA quietly, KegAlign
with a warning (it used to be an error).  A pair it cannot train, as when
LAST is missing, falls back to the lastzScoreModel table or HOXD70 as
before.  On evolver mammals it raised pooled leaf-pair precision from
0.816 to 0.830 and recall from 0.740 to 0.744; on the 577-way it would
cost about 0.3% of the lastz CPU.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Toil refuses a job that asks for more cores than --maxCores, or on a
single machine than it has, so lastz training's 4 (lastzTrainCores)
failed every cactus-blast run with fewer -- as cactus-prepare-toil's,
whose blast jobs run cactus-blast with --maxCores 1.  cactus now keeps
that limit in CACTUS_MAX_CORES, as it does --maxMemory's, and
cactus_clamp_cores holds a job to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant