From b969be14e57a36cc0fc529844c9b440ac0305d56 Mon Sep 17 00:00:00 2001 From: Glenn Hickey Date: Fri, 2 Oct 2026 09:52:40 -0400 Subject: [PATCH 1/6] blast: optional lastz scoring models, chosen by divergence lastz scored every pair with its default HOXD70, however diverged. can now hold any number of . 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 --- src/cactus/cactus_progressive_config.xml | 6 ++++ src/cactus/paf/local_alignment.py | 37 ++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/cactus/cactus_progressive_config.xml b/src/cactus/cactus_progressive_config.xml index 9cbe53932..92cdb1639 100644 --- a/src/cactus/cactus_progressive_config.xml +++ b/src/cactus/cactus_progressive_config.xml @@ -145,6 +145,12 @@ five="--step=2 --ambiguous=iupac,100,100 --ydrop=4000 --hspthresh=2400 --gappedthresh=2600 --queryhspbest=300000" default="--step=1 --ambiguous=iupac,100,100 --ydrop=4000 --hspthresh=2200 --gappedthresh=2400 --queryhspbest=300000" /> + + + + + is within, and with HOXD70 when past them all; with lastzTrain, a pair's trained model comes first and + this table is its fallback. Keep a model on HOXD70's scale (its lambda): the thresholds above, and the + chaining's, are in HOXD70's units. Not supported with KegAlign. --> + cannot be trained (the smaller genome under 500kb, or last-train failing, as without LAST) falls back to the + lastzScoreModel table, else HOXD70. It does not apply to KegAlign, minimap2 or FastGA. On evolver mammals + it raised pooled leaf-pair precision from 0.816 to 0.830 and recall from 0.740 to 0.744, for about 0.3% of + the 577-way's lastz CPU; set it to 0 for lastz's HOXD70 everywhere --> @@ -131,7 +133,7 @@ pickIngroupPrimaryAlignmentsSeparatelyToOutgroups="1" includeRootAsOutgroup="1" bigGenomeChunkScale="2" - lastzTrain="0" + lastzTrain="1" lastzTrainGaps="1" lastzTrainCores="4" > diff --git a/src/cactus/paf/local_alignment.py b/src/cactus/paf/local_alignment.py index b8dca6f60..5934f813f 100755 --- a/src/cactus/paf/local_alignment.py +++ b/src/cactus/paf/local_alignment.py @@ -68,16 +68,16 @@ def write_lastz_scores(model, path): def lastz_train_enabled(params): - """ Whether to train lastz's scores on each pair of genomes it aligns: """ + """ Whether to train lastz's scores on each pair of genomes it aligns: , on by default, so + it passes over the mappers it does not apply to, minimap2, FastGA and KegAlign, rather than stopping them """ lastz_params_node = params.find("blast") if not getOptionalAttrib(lastz_params_node, 'lastzTrain', typeFn=bool, default=False): return False if lastz_params_node.attrib["mapper"] != "lastz": - logger.warning(" is ignored: it is for lastz, and the mapper is {}".format( - lastz_params_node.attrib["mapper"])) return False if getOptionalAttrib(lastz_params_node, 'gpu', typeFn=int, default=0): - raise RuntimeError(" is not supported with KegAlign") + logger.warning(" does not apply to KegAlign, whose alignments keep its default scores") + return False return True From 2988906c5a5858e3e952d2c39dd1e9083dc19d91 Mon Sep 17 00:00:00 2001 From: Glenn Hickey Date: Tue, 6 Oct 2026 07:37:11 -0400 Subject: [PATCH 6/6] blast: ask for no more cores for training than Toil allows 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 --- src/cactus/paf/local_alignment.py | 4 ++-- src/cactus/shared/common.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/cactus/paf/local_alignment.py b/src/cactus/paf/local_alignment.py index 5934f813f..8eaabd844 100755 --- a/src/cactus/paf/local_alignment.py +++ b/src/cactus/paf/local_alignment.py @@ -25,7 +25,7 @@ from cactus.preprocessor.checkUniqueHeaders import sanitize_fasta_headers from cactus.preprocessor.unmasking import unmask_contigs_all from cactus.preprocessor.cactus_preprocessor import clean_if_different -from cactus.shared.common import cactus_clamp_memory +from cactus.shared.common import cactus_clamp_memory, cactus_clamp_cores def get_divergence_class(distance, params): """ The bucket a pair at the given distance falls in ("one" through @@ -1143,7 +1143,7 @@ def make_paf_alignments(job, event_tree_string, event_names_to_sequences, ancest db_letters = min(letters[event_a.iD], letters[event_b.iD]) score_models[lastz_pair_key(event_a.iD, event_b.iD)] = root_job.addChildJobFn( train_lastz_score_model, event_a.iD, genome_a, event_b.iD, genome_b, params, - cores=train_cores, + cores=cactus_clamp_cores(train_cores), memory=cactus_clamp_memory(int(1e9 + 5.5 * db_letters)), disk=int(2e9 + 5 * db_letters + genome_a.size + genome_b.size), walltime=cactus_walltime(1800 + 1500 * db_letters / 1e9, io_bytes=genome_a.size + genome_b.size)).rv() diff --git a/src/cactus/shared/common.py b/src/cactus/shared/common.py index 05244c851..bdcaacf0c 100644 --- a/src/cactus/shared/common.py +++ b/src/cactus/shared/common.py @@ -229,6 +229,13 @@ def cactus_override_toil_options(options): os.environ['CACTUS_MAX_MEMORY'] = str(max_mem) os.environ['CACTUS_DEFAULT_MEMORY'] = str(human2bytes(str(options.defaultMemory)) if options.defaultMemory else 2**31) + # and the most cores a job can ask for: Toil refuses a job that asks for more than --maxCores, or on a single + # machine than the machine has (see cactus_clamp_cores) + max_cores = sys.maxsize if options.maxCores in (None, float('inf')) else int(options.maxCores) + if options.batchSystem.lower() in ['single_machine', 'singleMachine']: + max_cores = min(max_cores, cactus_cpu_count()) + os.environ['CACTUS_MAX_CORES'] = str(max(1, max_cores)) + # store the walltime knobs here so cactus_walltime() can get at them without carrying # options around, and so they propagate to workers just like the memory limits above os.environ['CACTUS_WALLTIME_FACTOR'] = str(getattr(options, 'walltimeFactor', WALLTIME_FACTOR)) @@ -261,6 +268,11 @@ def cactus_override_toil_options(options): except: pass +def cactus_clamp_cores(cores): + """ use the environment variable from --maxCores (and a single machine's cores) to clamp a job's cores, as + cactus_clamp_memory does its memory: Toil refuses a job that asks for more """ + return max(1, min(int(os.environ.get('CACTUS_MAX_CORES', cores)), int(cores))) + def cactus_clamp_memory(memory_bytes): """ use the environment variables from --maxMemory and --defaultMemory to clamp a given memory value """ return max(min(int(os.environ['CACTUS_MAX_MEMORY']), int(memory_bytes)), int(os.environ['CACTUS_DEFAULT_MEMORY']))