Skip to content

v2.0.0: pyrodigal-gv gene caller, ProstT5+Foldseek structural phage detection, open-source MFD solver, direct CoverM, Snakemake 8+ - #68

Merged
Vini2 merged 46 commits into
Vini2:developfrom
gbouras13:feature/prostt5-foldseek-detection
Aug 12, 2026
Merged

v2.0.0: pyrodigal-gv gene caller, ProstT5+Foldseek structural phage detection, open-source MFD solver, direct CoverM, Snakemake 8+#68
Vini2 merged 46 commits into
Vini2:developfrom
gbouras13:feature/prostt5-foldseek-detection

Conversation

@gbouras13

Copy link
Copy Markdown
Contributor

43 commits. Bumps to 2.0.0

New

  • --phagedetection prostt5-foldseek — a second phage-gene detection
    method alongside the existing PHROGs/MMseqs2 search: predicts each unitig
    protein's 3Di structural alphabet with ProstT5
    and searches it with Foldseek against a curated subDB of phage hallmark
    protein structures (head-and-packaging/connector/tail/lysis PHROG
    categories, subset from phold's
    structure DB). Catches distant homologs sequence search misses, at the cost
    of needing a GPU for reasonable throughput. phables install fetches the
    pre-built hallmark subDB automatically (~105MB, hosted on
    Zenodo); --hallmark-db/
    --hallmark-categories auto-resolve once installed, same as the existing
    databases. --gpu-backend {cpu,cuda,rocm} selects which PyTorch build the
    ProstT5 conda env solves against (confirmed working on both NVIDIA/CUDA and
    AMD MI250X/ROCm — Setonix); --foldseek-gpu optionally accelerates the
    Foldseek search itself (CUDA-only). Full writeup: docs/hallmark_db.md.
  • --genecaller {pyrodigal-gv,fraggenescan} (default: pyrodigal-gv) —
    pyrodigal-gv as a modern, faster drop-in replacement for FragGeneScan,
    selectable per-run. Same {unitig}_{start}_{end}_{strand} protein-id
    convention preserved so both callers share every downstream consumer
    unchanged.
  • Open-source MFD solverFD_Inexact.py now goes through
    flowpaths (HiGHS-backed by default)
    instead of raw gurobipy calls. No commercial license needed anywhere in
    the pipeline. Validated end-to-end against the bundled test fixture
    (6/6 components resolved, sensible lengths/GC/coverage) — not yet
    numerically diffed against a live Gurobi run (no license available to test
    against).
  • Direct CoverM, koverage removed entirely (was: koverage as a nested
    Snakemake wrapper around CoverM) — both the preprocess (MFD-critical) and
    postprocess (report-only) coverage steps now call minimap2/samtools/
    coverm directly. Removes a nested Snakemake dependency from inside an
    already-Snakemake workflow. The postprocess step's estimator changed
    (CoverM's own columns vs. koverage's native-mode columns) — the three
    values phables consumes still exist and the reader's indices were updated
    to match, but this is report-only (sample_genome_*.tsv, not read by
    anything downstream) and hasn't been numerically regression-tested against
    old output on real data.
  • Snakemake 8+ (was: >=7.14.0) — needed for the executor-plugin
    architecture (snakemake-executor-plugin-slurm etc.) that
    resources: gpu=N--gpus=N auto-translation depends on. python_requires
    bumped to >=3.11 to match (every 8.x/9.x Snakemake release on PyPI
    requires it).
  • Optional SLURM GPU-routing profile (phables/workflow/profiles/gpu-routing/)
    — routes only predict_3di/scan_hallmark to a GPU partition via
    --workflow-profile, opt-in; every other rule (and every user without a
    GPU) is unaffected by default.
  • build_hallmark_db.py — the real script that built the hosted hallmark
    subDB, ported in and documented (docs/hallmark_db.md) for anyone who
    wants to verify it or rebuild against a newer phold snapshot.

Fixes bundled in along the way

  • igraph 1.0's get_all_simple_paths renamed cutoffmaxlen (long-read
    path only).
  • Order-dependent/missing single-copy-marker-gene exclusion in
    get_components.
  • --minlength wasn't being applied to linear resolved paths, only
    single-unitig components.
  • PHROG category matching was substring-based, not exact ("tail" matching
    "tail fiber" and similar false positives).
  • phables install silently ignored --databases (missing merge_config).
  • Tree-building (MAFFT + IQ-TREE) made opt-in (--build-tree) — was
    unconditional and slow at metagenome scale.
  • Several small CLI/pipeline robustness fixes (see individual commits).

Testing

  • Full pytest suite (12 tests: gene-caller unit tests, format_koverage_results.py
    column-layout regression test, CLI smoke test) passes in a clean venv
    mirroring CI exactly — not just locally, where several of these tests were
    initially masked by globally-installed packages that CI's own
    build/environment.yml doesn't provide (fixed: CI's stale Python 3.9/3.10
    matrix incompatible with the >=3.11 requirement, and missing
    pyrodigal-gv/pandas/biopython for the two non-Snakemake-rule test files).
  • Real snakemake --dry-run (Snakemake 9.25.1) against the bundled
    tests/data/ERR1301161 fixture across every major conditional branch:
    mmseqs, prostt5-foldseek, --genecaller fraggenescan, --longreads,
    --gpu-backend cuda --foldseek-gpu, the documented --foldseek-gpu+non-CUDA
    rejection, --workflow-profile gpu-routing. All resolve cleanly.
  • Real, non-dry-run phables install + phables run --phagedetection prostt5-foldseek --dry-run against the actual hosted hallmark_db tarball
    (genuine download, not a placeholder) — confirms the whole auto-install →
    auto-resolve chain works with zero --hallmark-db/--hallmark-categories
    flags passed.
  • ProstT5+Foldseek path exercised on real hardware: NVIDIA/CUDA (Phoenix,
    Adelaide) and AMD MI250X/ROCm (Pawsey Setonix), both via a plain conda env
    (no container needed on either backend).

Known gaps

  • HiGHS solver not numerically compared against a live Gurobi run (no
    license to test against — inherent limitation).
  • CoverM-vs-koverage postprocess estimator not numerically regression-tested
    on real data.
  • GPU-routing profile and the Snakemake 8 migration verified via --dry-run
    parsing/job-graph construction only, not a real SLURM cluster submission.
  • No direct precision/recall comparison yet between mmseqs and
    prostt5-foldseek phage detection on a labelled benchmark set.

Happy to split any of this into smaller follow-up PRs if that's easier to
review — flagging as one PR per earlier discussion, but the commit history is
intentionally kept granular (each commit is a self-contained, individually
reviewable change) in case that's useful.

gbouras13 and others added 30 commits August 3, 2026 11:38
FragGeneScan is split out of scan_smg into its own rule (call_genes) so the
caller is swappable without touching the marker-gene HMM search downstream.
pyrodigal-gv proteins are emitted with FragGeneScan-compatible headers
({unitig}_{start}_{end}_{strand}); get_smg_unitigs recovers the unitig name by
stripping the last three underscore fields, so any other naming scheme
silently empties the marker-gene set and disables bacterial filtering.

Ablated on two virome assemblies (one HPC-assembled, 259 Mbp) against a
bacterial marker-gene HMM search: pyrodigal-gv finds a net 6 more SMG-bearing
unitigs than FragGeneScan across both graphs, with 1 loss on a very short
(700 bp) unitig where FragGeneScan calls a short trailing ORF that
pyrodigal-gv's gene model does not propose. Short unitigs are unlikely to be
flagged as real phage candidates regardless, so this is an acceptable
trade-off. Details in notes/phables_audit.md (project-level, not in this repo).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
FD_Inexact.py now encodes the minimum flow decomposition through
flowpaths.AbstractPathModelDAG instead of raw gurobipy calls. flowpaths
defaults to HiGHS (via highspy) and isn't overridden here, so this is a
genuine open-solver swap, not a Gurobi dependency in disguise.

Two fixes found while validating the swap end-to-end against the bundled
test fixture:

- SolveInstances stopped creating results_MFD.txt / results_MFD_details.txt
  (always-empty files even before this change, but declared as required
  outputs by test_phables.smk -- a missing declared output is a hard
  Snakemake failure). Restored the two-line creation.
- phables.yaml still pinned gurobi>=10.0.0 and the now-unused
  more-itertools; the switch to stdlib itertools.pairwise needs Python
  3.10+, which the existing <3.11 upper bound didn't guarantee. Dropped
  gurobi, tightened the Python pin, added flowpaths via pip (it isn't on
  any conda channel -- checked conda-forge, bioconda, and anaconda.org
  directly). build/environment.yml is untouched: it never depended on
  Gurobi, since it only carries the CLI/Snakemake shell, not the solver.

Validated by running phables.py's real pipeline against test_data/
directly (bypassing Snakemake, since the PHROGs MMseqs DB isn't available
locally): 6/6 components resolved, 7 genomes, sensible lengths/GC/coverage,
identical before and after the two fixes above. Not yet validated against
a live Gurobi run -- no license survives the fresh clone of this repo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… --phagedetection

Adds prostt5-foldseek as an alternative to the mmseqs/PHROGs phage-gene
detection path: pyrodigal-gv-called proteins -> ProstT5 3Di prediction
(pholdlib) -> foldseek search against a hallmark PHROG structure subDB
(head and packaging/connector/tail/lysis) -> phables_utils/hallmark_utils.py.

hallmark_utils parses foldseek's own convertalis output directly rather than
mimicking mmseqs' TSV format -- its own file format, its own parser, matching
gene_utils.get_phrog_unitigs' exact (unitig_phrogs, phrog_dict) contract so
component_utils.get_components needs no changes. It also asserts an invariant
get_phrog_unitigs never had to: every phrog id in unitig_phrogs must have a
category in the passed-in categories file, raising a clear error naming the
drifted ids if a stale categories file is ever paired with a rebuilt DB,
instead of a bare KeyError deep inside get_components.

New Snakemake rules (predict_3di, build_hallmark_query_db, scan_hallmark)
mirror the call_genes pattern, gated behind `if PD == "prostt5-foldseek"`
in genes.smk. That gating is load-bearing, not just tidiness: scan_hallmark's
hallmark_db input defaults to None in config.yaml when unused, and Snakemake
parses every rule's input/output at load time regardless of the target list,
so an un-gated reference breaks a plain mmseqs-path `phables run` with a
SyntaxError even though the rule would never execute.

build_foldseek_query_db.py encodes two non-obvious foldseek mechanics found
while validating this against real data: `foldseek createdb` doesn't accept
plain protein FASTA (use `tsv2db` on numbered AA/3Di/header TSVs instead,
matching phold's own create_foldseek_db.py), and `foldseek search` between
two coordinate-free DBs fails unless the auto-created empty _ca companion
file is deleted first.

Validated end-to-end through the real wired pipeline (not standalone
scripts): 126 called proteins -> 126 3Di predictions -> 97 hallmark hits on
the bundled test graph -> 4 resolved genomes. Cross-checked against the
mmseqs path on the same graph: the 4 resolved genomes are byte-for-byte
identical to 4 of the mmseqs path's 7 (same coverage/length/GC%/node order),
confirming the integration is correct where it fires -- but 3 mmseqs-found
components are missed under the current untuned --hallmark-evalue/--hallmark-
minbits defaults, a real precision/recall calibration question rather than a
bug. Full analysis in notes/phables_audit.md (project-level, not in this
repo).

Neither the hallmark DB nor the ProstT5 checkpoint are auto-downloaded yet --
both must be supplied explicitly via --hallmark-db/--hallmark-categories/
--prostt5-checkpoint. No hosted distribution point for the custom hallmark
DB exists yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…lax Python pin

Splits the single prostt5.yaml env into prostt5-{cpu,cuda,rocm}.yaml,
selected via --gpu-backend / config gpu_backend, since conda envs are
solved once from a static file and can't switch PyTorch builds at
runtime. cpu/cuda use a conda build-string wildcard (pytorch=*=cpu*,
pytorch=*=cuda*) rather than a pinned CUDA minor version, so the solver
picks whatever tagged build matches the rest of the env instead of this
needing to be bumped by hand as new PyTorch releases land -- confirmed
these wildcards resolve on both the pytorch and conda-forge channels.
rocm has no conda build at all (checked both channels), so it stays
pip-based against download.pytorch.org's rocm6.3 wheel index.

Wanted --no-deps on the pholdlib pip install to protect the conda-solved
torch build, but that isn't achievable through a conda/mamba pip:
section at all -- both write it to a real requirements.txt, and current
pip rejects --no-deps as a file directive (confirmed empirically under
both conda and mamba, not just from reading docs). Checked what actually
happens without it instead: pholdlib's own torch requirement is
unconstrained, so a real install against the conda-built env left torch
untouched; it reinstalled a couple of pholdlib's smaller transitive deps
to match its own metadata, which is harmless.

Adds --foldseek-gpu, mirroring phold's own GPU search code
(phold/features/run_foldseek.py) rather than the foldseek README alone:
the {target_db}_gpu naming convention and --gpu 1 --prefilter-mode 1
flags come from phold's actual working implementation, which is a
better bet than the README's suggestion that one padded DB works for
both CPU and GPU. Raises a clear error if foldseek_gpu is requested with
gpu_backend != cuda, since foldseek's GPU mode is CUDA-only.

Adds --prostt5-container for running predict_3di against a container
that already has pholdlib + torch installed (e.g. phold's own image)
instead of building a conda env. Deliberately does NOT combine
container: with conda: on this rule -- Snakemake's documented way to do
that builds a separate, isolated conda env inside the container rather
than exposing what's already installed there, which would just
reinstall a second redundant torch and ignore the image's own
verified-working one. Needs --use-singularity passed through phables'
existing snake_args passthrough.

None of the GPU/container code paths are verified against real hardware
-- this machine has neither CUDA, ROCm, nor Apptainer/Singularity. All
wiring is dry-run tested: env/container selection resolves correctly
for every --gpu-backend value and with/without --prostt5-container, and
invalid combinations (e.g. --foldseek-gpu with gpu-backend rocm) raise
a clear error at parse time instead of failing silently later.

Also relaxes phables.yaml's python pin from <3.11 to <3.13. Traced the
history: the upper bound was added to fix a solver conflict attributed
to pysam, but `mamba search -c gurobi gurobi>=10.0.0` shows that package
only ever published py310 builds -- almost certainly the real cause,
now moot since Gurobi is gone. Confirmed the remaining (post-Gurobi)
dependency set solves cleanly through 3.12; 3.13 has unrelated solver
friction, likely numpy<2.0.0 build lag.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Both traced back to Gurobi/numpy-2.0-transition issues that no longer
apply now Gurobi is gone and numpy 2 has been out for well over a year:

- numpy<2.0.0 was added for GH issue Vini2#55 (June 2024), right when numpy
  2.0 shipped and broke pandas' ABI compatibility across the ecosystem.
  That was real at the time. Verified today's stack doesn't have the
  problem: built a real env with numpy 2.5.1 + pandas 3.0.5 and tested
  phables' actual pandas usage (combine_cov.py, format_koverage_results.py
  -- DataFrame construction, axis=1/join="inner" concat, concat-as-append,
  read_csv/to_csv) against it directly, not just that the imports succeed.
- The previous commit's <3.13 upper bound was itself downstream of the
  numpy<2.0.0 pin colliding with 3.13's package availability, not a real
  3.13 problem -- confirmed by dry-run solving 3.10 through 3.13 cleanly
  once numpy was unpinned.

Ran the full real pipeline (component detection, HiGHS solve via
flowpaths, output writing) against a from-scratch env with numpy 2.5.1,
pandas 3.0.5, python 3.12: byte-identical resolved_genome_info.txt versus
the previous numpy<2/python3.10 run. Gene-caller test suite also passes
unchanged (8/8) against the new env.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Bumps snakemake>=7.14.0 -> snakemake>=8.0 (setup.py, build/environment.yml)
and the accompanying python_requires from >=3.9,<3.11 to >=3.11 -- every
8.x/9.x release on PyPI requires Python 3.11+, confirmed by attempting to
install snakemake>=8 into a 3.10 env, which refused to resolve any 8.x/9.x
version at all.

Why: Snakemake 8.0.0 introduced the external executor-plugin architecture
(snakemake-executor-plugin-slurm etc) and the set-resources/executor: keys
in workflow profiles. 7.x's older built-in --cluster/--drmaa flags don't
auto-translate rule resources like `gpu=1` into SLURM flags (`--gpus=1`)
at all -- that mapping is new plugin-side machinery. This is what lets
predict_3di/scan_hallmark's GPU work be routed to a GPU partition via an
optional profile, while every other rule stays on a cluster's default
partition, without hardcoding SLURM specifics into the Snakefile itself.

Bumping surfaced a real, fork-wide incompatibility, independent of the new
profile mechanism: every rule with a resources: block set both `mem_mb` and
`mem` (a string with a unit) for the same underlying memory requirement.
Snakemake 7 allowed this; Snakemake 9 rejects it outright ('The following
resources are equivalent and cannot be simultaneously provided to a rule').
Found across coverage.smk, genes.smk, and postprocess.smk (4 rules) by
actually attempting a dry-run under Snakemake 9, not by inspection. Fixed by
dropping the redundant `mem` string and keeping only `mem_mb` -- the
canonical resource key the SLURM executor plugin's own docs map to `--mem`.

Verified: dry-run under Snakemake 9.24.0 (the version `snakemake>=8.0`
resolves to today) succeeds past the point where it previously failed with
the resource conflict. Confirmed no Snakemake-8-removed directives
(dynamic(), version:, subworkflow, remote()) are used anywhere in this
workflow, so this migration doesn't lose any functionality on that front.
Not yet tested against a real SLURM cluster -- only Snakemake's own
--dry-run parsing and job-graph construction, no hardware submission.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
chore/snakemake8-migration fixed this in the pre-existing rules; the new
call_genes/predict_3di/scan_hallmark rules added on this branch had the same
copy-pasted pattern and needed the identical fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ents

phables' own mmseqs-based phage detection (gene_utils.get_phrog_unitigs)
reads product/category info from a bundled phrog_annot.tsv shipped in the
repo. Compared it against two external sources: pharokka's phrog_annot_v4.tsv
and phold's phold_annots_updated.tsv (the more recently curated one, per
Vijini). The bundled file matched phrog_annot_v4.tsv almost exactly (9
differences out of 109 404 PHROGs) and differed from the updated one on
5 867 -- meaning phables has been shipping the outdated categorisation.

The pattern in what changes is systematic, not noise: PHROGs the outdated
file calls "unknown function" get correctly assigned to real hallmark
categories in the updated file, with product descriptions that plainly
justify it -- e.g. phrog_19684 "head-tail connector protein" unknown ->
connector, phrog_72652 "tail tube" unknown -> tail, phrog_45215 "virion
structural protein" unknown -> head and packaging. Every one of these was
previously invisible to component_utils.get_components' category test,
meaning components whose only PHROG evidence fell on one of these ~5 800
IDs were being excluded as if they had no phage-like content at all.

Fixed by replacing the annot/category columns from the updated source while
preserving the bundled file's own color column unchanged -- confirmed
get_phrog_unitigs never reads that column (only strings[2] and strings[3]),
so it's a pure display/passthrough field, safe to leave as-is rather than
needing a color scheme for the newly-corrected categories.

Verified: re-ran the real pipeline against phables' own bundled test_data/ --
component count unchanged (6), since none of that fixture's PHROG ids happen
to be among the ~5 800 that changed. Spot-checked three of the corrected
PHROGs directly in the new file to confirm the category values, not just the
diff count.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Only predict_3di (and scan_hallmark, when --foldseek-gpu is set) benefit
from a GPU -- every other rule in this workflow is CPU-only. Deliberately
not hardcoded into the Snakefile: none of the rules declare slurm_partition
or a gpu resource directly, so a user without GPU access (or on a cluster
with no GPU partition) gets an entirely CPU-routed run by default, with
nothing to opt out of.

This profile is the opt-in path for users who do want GPU routing: pass it
via phables' existing snake_args passthrough
(--workflow-profile phables/workflow/profiles/gpu-routing), and only
predict_3di/scan_hallmark get routed to a GPU partition; everything else
still defers to the cluster's own default profile/sbatch defaults.

Needs the snakemake>=8 migration (separate commit) -- the set-resources/
executor: mechanism this profile uses (and the auto-translation of
resources: gpu=N to SLURM's --gpus=N) is new in Snakemake 8's external
executor-plugin architecture; Snakemake 7's older --cluster/--drmaa flags
don't support it.

Verified with --dry-run under Snakemake 9.24.0 (snakemake-executor-plugin-
slurm installed): the profile is recognised and predict_3di/scan_hallmark
resolve correctly with it; the same run also verified successfully without
--workflow-profile at all (--phagedetection prostt5-foldseek with no GPU
routing) and with the plain mmseqs default path, confirming GPU routing is
genuinely opt-in rather than forced on anyone. Not tested against a real
SLURM cluster -- no such access here, and the exact partition name/gres
syntax needs matching to whatever cluster this is actually used on (noted
in the profile's own comments).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1. `phables run`/`install`/`test` crashed before Snakemake ever started
   whenever --output pointed at a directory that didn't already exist:
   snaketool_utils' copy_config() opens f"{output}/phables.log" to write
   its first log line, and that open doesn't create missing parent
   directories. Fixed with a callback on --output itself (in
   common_options, so it covers run/install/test uniformly) that creates
   the directory as soon as the option's value is resolved -- before
   --configfile/--log run their own default_to_output callbacks, since
   --output is processed first.

2. --databases was config-file-only despite looking like a normal CLI
   option; passing --databases directly failed with `snakemake: error:
   unrecognized arguments: --databases` because nothing consumed it before
   it fell through to Snakemake's own arg parser. Added a real --databases
   click option in common_options; it flows into config the same way
   --genecaller/--phagedetection etc already do, via merge_config=kwargs.

3. run_combine_cov used `sed -i '1d' {input}` to strip the coverage.tsv
   header before summing, mutating its own declared input file. Not
   idempotent: a retry after a partial failure would see an
   already-stripped input and silently drop the first real data row as if
   it were still the header, producing a wrong-but-plausible coverage.tsv
   instead of erroring. Replaced with `awk 'NR>1 {...}'`, which skips the
   header without touching the input at all -- verified identical output
   to the old two-step version on representative CoverM-shaped data, and
   confirmed it can be re-run any number of times against the same
   untouched input with the same correct result.

Verified 1+2 together with a real dry-run: `--output <new-dir> --databases
<path> --dry-run` succeeds and the output directory exists afterward,
without a pre-existing mkdir and without needing --configfile to smuggle
--databases in.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
component_utils.get_components tested category membership with substring
matching against phrog_dict[phrog], which held the PHROG's product/annot
text concatenated with its category (e.g. "tail completion or Neck1
protein connector"). `"tail" in phrog_dict[phrog]` fires on that string
whether or not the PHROG's real category is tail -- and plenty of
connector-category genes describe the tail apparatus they connect to in
their product name.

Confirmed against the real, current annotation table (post the
phold_annots_updated.tsv fix): 110 PHROGs hit this exact failure mode --
103 false "tail" matches, 7 false "connector" matches -- almost all
connector-category genes ("tail completion", "tail terminator",
"head-tail adaptor", "baseplate wedge tail fiber protein connector", ...)
whose product text happens to mention another hallmark word.

The practical impact is narrow but real: since get_components only needs
*any* hallmark category present to keep a component (an OR across all
four), a false match rarely changes whether a component survives at all.
It does change `likely_complete`, which requires *all four* categories
(an AND) -- a component with real head/connector/lysis evidence plus a
connector-category gene whose product text happens to say "tail" would
be wrongly marked likely_complete=1 with no real tail gene anywhere on
it. Confirmed this concretely: reproduced the old string-concatenation
format standalone and showed it computes likely_complete=1 for exactly
this case (phrog_2 + phrog_5 + phrog_7), where the fixed code correctly
computes 0.

Fixed at the source rather than patching the substring tests: gene_utils.
get_phrog_unitigs now stores just the category in phrog_dict, not the
product text concatenated with it -- confirmed phrog_dict has no other
consumer anywhere in the codebase, so nothing downstream depended on the
product text being there. component_utils.get_components then does exact
equality instead of substring containment.

Verified with a test that empirically discriminates old vs new behaviour
(not just asserts the new one): the phrog_2+phrog_5+phrog_7 case above,
proven to compute likely_complete=1 under the old string format and 0
under the fix. Also re-ran the real pipeline against phables' own bundled
test_data/ -- component count unchanged (6), no regression.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Gene caller, HiGHS solver, ProstT5+Foldseek detection, Snakemake 8 migration, and bug fixes
install() passed **kwargs to run_snakemake but not merge_config=kwargs,
so --databases never reached install.smk's config -- it silently fell
back to <repo_root>/databases regardless of what was passed on the CLI.
run() already did this correctly; install() just needed the same treatment.
…us chain

cogent3>=2026.7.6a0 depends on scinexus>=2026.5.16, which only resolves to
a tqdm build requiring python-3.14.0rc1 (needs the _python_rc virtual
package) -- unsatisfiable on any real install. No bioconda cogent3 build
through 2026.4.13a0 has a scinexus dependency at all.
At metagenome scale (thousands of genomes) the postprocess phylogenetic
tree step is slow and most workflows want to run phylogenetics as a
separate step anyway (per Vijini). Adds --build-tree/--no-build-tree
(default off); genomes_aligned.fasta and genomes_phylogenetic_tree.tree
are only added to postprocessTargets when set. Verified via dry-run:
default run omits build_msa/build_tree from the job DAG entirely,
--build-tree restores both.
…: entrypoint

The CLI entrypoint's --model-dir defaults to ~/.cache/prostt5 via argparse,
but the script: entrypoint passes config['prostt5_model_dir'] straight
through, which is None unless --prostt5-model-dir was explicitly set.
hf_cache_present() then crashed building Path(None). Fallback now lives
in run() itself so both entrypoints get it.
transformers' check_torch_load_is_safe() (CVE-2025-32434 hardening) refuses
torch.load()-based checkpoint loading below torch 2.6, unconditionally --
this broke loading Rostlab/ProstT5_fp16 (no safetensors file) the moment the
unpinned pytorch=*=cuda*/cpu* wildcard let the solver pick something older.
Verified: dry-run solve of prostt5-cpu.yaml resolves pytorch 2.13.0 cleanly
under the new pytorch[version=">=2.6",build=cpu*] spec; rocm6.3's wheel
index has torch 2.7.0-2.9.1 available for the pip-installed rocm variant.
Reduces spurious low-confidence hallmark hits at the default threshold.
…g components

component_utils.get_components already required single-unitig candidates to
clear minlength before ever being resolved, but multi-unitig LINEAR paths had
no length floor at all -- any component with a single phage-hallmark hit
produced a "resolved genome" regardless of how short the MFD-resolved path
came out. Pilot10 output showed this concretely: 72% of prostt5-foldseek's
227 resolved paths and 71% of mmseqs's 211 were under 5kb, well below any
plausible phage genome size.

Filters final_genomic_paths in short_utils.py/long_utils.py right before it
feeds both all_resolved_paths (-> resolved_genome_info.txt) and
write_path/write_path_fasta (-> resolved_paths.fasta, resolved_phages/*.fasta)
-- a single insertion point so all three outputs stay consistent, rather than
filtering only the returned list in phables.py after the FASTA files are
already written. Circular paths are exempt: a closed cycle is completeness
evidence independent of length, same reasoning the existing single-unitig
gate already used.

Verified: standalone filter-logic test against short/long/circular/linear
GenomePath cases (boundary case at exactly minlength correctly dropped,
matching the existing strict '>' convention). Real `phables test` run against
the bundled fixture succeeds with all 7 genomes preserved (none happened to
be sub-threshold linear paths in that tiny graph) -- confirms no regression,
not the drop case itself.

Also fixes test_phables.smk, found while validating this: its params block
never got phagedetection/hallmark_* added when that feature landed, so
`phables test` crashed unconditionally (AttributeError on
snakemake.params.phagedetection) regardless of this change.
…nly)

igraph 1.0.0 renamed get_all_simple_paths' path-length-limit kwarg from
`cutoff` to `maxlen`, so the long-read branch crashed with
"TypeError: got an unexpected keyword argument 'cutoff'" the moment it ran
(get_all_sub_paths is only reached under --longreads, which is why the
short-read path never surfaced this).

Detects the kwarg name from the installed igraph's signature rather than
pinning igraph, so this works either side of that release. Verified against
igraph 1.0.0 that both spellings mean the same thing -- they count *edges*,
so a value of 2 yields paths of up to 3 vertices, matching what the
len(path)==3 / len(path)==2 branches below expect.
Hit on Setonix: predict_3di crashed on `from pholdlib.prostt5.device import
parse_gpus` with `OSError: libomp.so: cannot open shared object file`.
Root cause: importing pholdlib.prostt5 runs pholdlib/prostt5/model.py's
`from transformers import T5EncoderModel, ...`, and recent transformers
routes all model imports through a shared loss registry
(transformers/loss/loss_utils.py) that unconditionally imports
transformers/loss/loss_rnnt.py for ParakeetForRNNTLoss -- an audio ASR
loss totally unrelated to T5/protein embeddings. That file does a plain
`import torchaudio`, and torchaudio's compiled extension needs libomp.so
(LLVM's OpenMP runtime), which this container's image doesn't have
linkable.

We never touch anything audio-related, so if the real torchaudio import
fails with OSError, install a harmless stub in sys.modules first so
transformers' import chain completes anyway. Verified the mechanism
directly: reproduced the same OSError with a minimal package shaped like
transformers' actual import chain (unconditional `import torchaudio` +
a class referencing it only in a method body, never at class-definition
time), confirmed the stub resolves it cleanly. Only engages when the real
import already failed -- no-op everywhere torchaudio works normally.
The previous fix (afe38c5) unblocked the original OSError but introduced
a new one: before transformers ever reaches the `import torchaudio` line
that actually needed a stub, it first runs a lightweight availability
check (is_torchaudio_available() -> importlib.util.find_spec("torchaudio")),
which explicitly raises `ValueError: torchaudio.__spec__ is None` when
sys.modules already has an entry for the name with no spec -- exactly
what a bare types.ModuleType has by default. Hit this for real on Setonix.

Fix: give the stub an actual (empty) importlib.machinery.ModuleSpec, plus
a __version__ attribute as a defensive fallback (transformers falls back
to reading torchaudio.__version__ directly if the installed-distribution
metadata lookup ever fails). Verified all three points in the real chain
locally: find_spec succeeds, the __version__ fallback path succeeds, and
a deep unconditional `import torchaudio` + class referencing it only in a
method body (matching loss_rnnt.py's actual shape) succeeds.
…karound

Switching to the conda-env ROCm path (--gpu-backend rocm, no
--prostt5-container) for Setonix after repeatedly hitting container-specific
problems unrelated to phables/pholdlib itself: broken torchaudio needing
libomp.so, --home overriding the container's apparent CWD so relative
output paths silently resolved to the wrong place, and a missing MIOpen
AI-heuristic kernel file for gfx90a. A plain conda env has no container
namespace/bind-mount surface to hit any of these on.

Pin torch==2.7.1 exactly (not just >=2.6) to match Setonix's own
already-verified-working Pawsey container tag
(quay.io/pawsey/pytorch:2.7.1-rocm6.3.3) -- confirmed the exact wheel
exists on the rocm6.3 index for cp39-cp313.

Also broadened predict_3di.py's torchaudio workaround from `except OSError`
to `except (OSError, ImportError)`: a plain conda env installing only
torch + pholdlib won't have torchaudio installed at all (unlike a generic
"pytorch" container image, which tends to bundle the full audio/vision
extras), so the same unconditional import inside transformers' loss
registry would raise ModuleNotFoundError instead of OSError -- previously
uncaught, meaning our own diagnostic `import torchaudio` would have
crashed predict_3di.py unnecessarily in exactly this env. Verified both
exception paths locally.
gbouras13 and others added 13 commits August 10, 2026 16:37
Per Vijini's recommendation: drop the koverage Snakemake-wrapper dependency
for the preprocess unitig-coverage step and call minimap2 + CoverM directly.

Reproduces Koverage's own real "coverm" mode chain exactly (confirmed against
its actual source, github.com/beardymcjohnface/Koverage workflow/rules/coverm.smk,
and its shipped default config.yaml -- not reimplemented from guesswork):
  - same minimap2 mapping command (-ax sr --secondary=no) + samtools
    sort/view -F 4/index chain
  - same `coverm contig` --methods list/order: count, rpkm, tpm, mean,
    covered_fraction, variance (phables never overrode Koverage's own default)
  - same per-sample -> long-format Sample/Contig/<method...> TSV reshape

sample_coverm_coverage.tsv keeps the same shape, so run_combine_cov's awk and
coverage_utils.py's BAM globbing need no changes. Verified the reshape +
downstream Mean-summed-per-contig reduction end-to-end against synthetic
CoverM-style output.

Scope: only the preprocess step (feeds MFD via coverage.tsv). postprocess.smk's
koverage_genomes rule uses Koverage's separate native (non-CoverM) engine and
is untouched -- a different question, not covered by PLAN.md §4.8/the audit.
No CRAM/--reference wiring yet (§3.2) -- no pipeline produces CRAM reads yet,
so there's nothing to test that against.

Preserved bug-for-bug from Koverage's own rule, not newly introduced: no -t
passed to `coverm contig`, and -ax sr used even for --longreads runs. Both
flagged separately for follow-up.

Not yet dry-run against a real Snakemake install (none available in this
environment) -- run `phables run ... --conda-create-envs-only` and the
koverage-vs-CoverM-direct coverage.tsv regression check PLAN.md §4.8 calls
for before trusting this on real data.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
format_koverage_results.py's parsing (strings[4]=RPKM, strings[7]=Mean) was
suspected of assuming the wrong Koverage output columns, since Koverage's
`coverm` subcommand mode uses a different, shorter header. Traced
postprocess.smk's koverage_genomes rule (`koverage run --reads ... --ref
...`, no `coverm` subcommand) to Koverage's native "map" mode default target,
and confirmed its real sample_coverage.tsv header against upstream source
(coverage.smk's all_sample_coverage rule + sampleCoverage.py):

  Sample Contig Count RPM RPKM RPK TPM Mean Median Hitrate Variance
    0      1      2    3   4    5   6   7     8       9        10

Under that header the existing indices (2/4/7 = Count/RPKM/Mean) were already
correct -- no data bug. Documented both header layouts and why "map" mode
applies here, factored the row parsing into parse_koverage_row() with named
index constants, and added a standalone regression test (no pytest
dependency, since this repo has none) that locks the mapping in against a
synthetic row copied from Koverage's own format.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
coverm_map always mapped with -ax sr regardless of config["longreads"],
matching Koverage's own coverm_map_pe upstream behavior exactly but now
baked into phables' own rule file rather than hidden in a wrapper. Long-
read (ONT/PacBio) runs via myloasm were thus having their unitig coverage
computed with a short-read-tuned minimap2 preset feeding the MFD flow
decomposition -- exactly the "wrong-but-plausible coverage numbers produce
wrong-but-plausible genomes" risk PLAN.md's risk table calls out.

LR (config["longreads"], set in 02_phables_preflight.smk, already included
before coverage.smk) now selects the preset: -ax sr unchanged for short
reads, -ax map-ont for long reads. --longreads is a bare boolean flag with
no ONT/PacBio distinction anywhere in phables, and map-ont is confirmed
correct for the only long-read data actually exercising this path so far
(../metagenomic_phage_discovery/pilot10_longreads: all Oxford Nanopore,
assembled with myloasm's default R10.4-targeting mode).

Verified both branches with a Snakemake dry-run (phables-dev env, snakemake
7.32.4): confirmed the emitted minimap2 command switches -ax sr <-> -ax
map-ont correctly with longreads False/True. Full run_pilot_phables.sh
before/after regression on real long-read data still needed on Phoenix
(GPU node, real databases) -- not run here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Real error on Setonix: LibMambaUnsatisfiableError, every samtools 1.14-1.24
rejected on a libdeflate lattice conflict. Root cause: coverm's own bioconda
recipe already declares minimap2>=2.28 and samtools>=1.9 as its own
dependencies (plus a large pile of other bundled tools -- bwa, fastani, gsl,
skani, strobealign, etc.). Explicitly pinning a second, independent
minimap2>=2.24/samtools>=1.15 on top of that forced mamba to jointly solve two
separate constraint surfaces across coverm's already tightly-pinned dependency
graph, rather than the one bioconda actually built/tested coverm against.

Fix: list only coverm -- it pulls in a mutually compatible minimap2 + samtools
on its own, which is everything the coverm_map rule's shell commands need.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Real error on Setonix: samtools index rejected the "ERR1301161.bam" output
with "is in a format that cannot be usefully indexed" -- the file was
actually SAM text, not BAM. samtools view's documented default output format
is SAM regardless of input format or `>` redirection target name; -b/--bam
must be passed explicitly (confirmed against htslib's own samtools-view docs).
The prior version of this rule lacked -b -- likely dropped during an earlier
LLM-summarized "verbatim" fetch of Koverage's upstream source used to write
this rule, not present in a byte-exact read of the real file. Verified this
time against samtools' documented flag semantics directly rather than a
fetched transcription of someone else's shell command.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…overage

Completes PLAN.md §4.8. The earlier preprocess-only change (a1ac8fd) swapped
the MFD-critical coverage step to CoverM but left koverage installed and still
running a nested Snakemake -- so §4.8's actual headline benefit ("removes a
nested Snakemake dependency from inside what is already a Snakemake workflow")
was not achieved. It is now.

Of the three koverage-prefixed rules in the DAG, exactly ONE actually depended
on koverage:
  - koverage_tsv        -> plain metasnek fastq_finder call, no koverage.
                           Renamed samples_tsv (localrules updated to match).
  - koverage_genomes    -> the real dependency (conda: koverage.yaml). Replaced
                           with coverm_map_genomes / coverm_bam2counts_genomes /
                           coverm_combine_genomes, mirroring coverage.smk.
  - koverage_postprocess-> runs phables.yaml, no koverage. Renamed
                           format_genome_coverage.
koverage.yaml deleted; no live koverage reference remains in the workflow.

ESTIMATOR CHANGE (unlike the preprocess swap, which was shape-preserving):
koverage's native "map" mode emitted
  Sample Contig Count RPM RPKM RPK TPM Mean Median Hitrate Variance
`coverm contig` emits
  Sample Contig Count RPKM TPM Mean Covered_fraction Variance
All three values phables consumes (Count/RPKM/Mean) still exist, and
format_koverage_results.py's indices move 2/4/7 -> 2/3/5 accordingly. But the
two tools compute them via different code paths, so reported numbers may differ
slightly. This is REPORT-ONLY (sample_genome_*.tsv are not read by anything
downstream; the MFD math uses preprocess/coverage.tsv). Not numerically
regression-tested against old output on real data -- worth doing per §4.8's own
advice before relying on absolute values.

Also: postprocess BAMs are marked temp(). koverage's native mode piped minimap2
into a python counter and never wrote a postprocess BAM, so keeping them would
be a pure disk regression at 10k-sample scale; nothing consumes them after the
counts step.

Verified: regression test updated to lock in the CoverM indices (and to assert
the legacy 4/7 indices are NOT in use -- confirmed it genuinely fails if they
are reintroduced, catching RPKM silently reporting TPM); the three new rules
structurally diffed against their coverage.smk counterparts and confirmed
identical modulo ref/paths/names. NOT dry-run tested -- no snakemake available
in this environment; run --conda-create-envs-only / a dry-run before trusting
on real data.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Warrants a major bump: new gene caller (pyrodigal-gv), new phage-detection
method (ProstT5+foldseek structural search), open-solver MFD swap (Gurobi ->
flowpaths/HiGHS), Snakemake 8+ migration, and the koverage removal -- a
substantial feature/behavior set, not a patch release.

Updated phables.VERSION (the single real source, read by both setup.py and
__main__.py's --version) plus six hardcoded "1.5.0" strings that don't read
from it (module __version__ attributes and logger names) -- found via a plain
grep for the old string, not guessed at from memory.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two real, confirmed-in-a-clean-env gaps found auditing the test suite before
the PR, neither caught before now because this Mac happens to have pandas/
biopython/pyrodigal-gv installed globally, masking both locally.

1. CI's python-version matrix was still ["3.9", "3.10"], despite setup.py's
   own python_requires=">=3.11" (needed by snakemake>=8, per that migration's
   own commit). Every CI run on this branch would fail `pip install .`
   outright, on every combination. Bumped to ["3.11", "3.12"] in both the
   `tests` and `finish` jobs.

2. Both real test files that import a workflow script directly (not through a
   Snakemake rule, which would have its own per-rule conda env) need packages
   build/environment.yml -- all CI installs -- doesn't provide:
     - tests/test_gene_caller.py needs pyrodigal_gv (envs/genecall.yaml, a
       per-rule env). Fixed by pip-installing it in the CI test step only,
       not the base build/environment.yml (keeps end users who never touch
       --genecaller pyrodigal-gv from an unneeded install).
     - phables/tests/test_format_koverage_results.py needs pandas/biopython
       purely because format_koverage_results.py imported them at module
       level even though the row-parsing logic the test exercises never
       touches either. Fixed properly instead of papering over it: moved
       both imports inside main() (the only place they're used) -- runtime
       behaviour is unchanged (envs/phables.yaml, the rule's real conda env,
       already provides both), but the module is now importable standalone
       for unit testing without a full data-science stack.

Verified for real: built an isolated venv (this Mac's own python3 has a
broken/incomplete pygments install unrelated to phables that blocks a plain
`pytest` invocation entirely), ran `pip install .` + the exact CI steps
against it -- all 12 tests pass. Also confirmed the reverse: simulated a
clean env with pandas/Bio import-blocked via a builtins.__import__ hook and
confirmed format_koverage_results.py still imports and its row-parsing
function still works correctly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
--hallmark-categories' own help text already said "from build_hallmark_db.py"
-- that script didn't exist anywhere in the repo. Ports the real script that
built the actual reference hallmark_db used to validate --phagedetection
prostt5-foldseek this whole session (previously living outside the fork, at
../metagenomic_phage_discovery/db/build_hallmark_db.py), unchanged in logic,
into its promised home.

Subsets phold's full structure DB down to the four structural/virion PHROG
categories phables_utils/component_utils.py's get_components actually screens
for (head and packaging, connector, tail, lysis), plus a separate integration/
excision channel kept apart rather than folded in (integrases are abundant on
bacterial chromosomes/MGEs too, would dilute the structural signal). Confirmed
its output format matches hallmark_utils.load_hallmark_categories()'s own
docstring expectation exactly (phrog_id-with-prefix + category, header row) --
not assumed, read both sides.

Verified by porting AND re-running against synthetic phold-format fixtures
(phrog IDs, non-numeric IDs to confirm exclusion, a lookup file with an
over-cap PHROG family to confirm the stride-sample math): correct category
counts, correct stride-sampled row indices, correct file format, non-PHROG
channels (acr_*, etc.) correctly excluded.

docs/hallmark_db.md explains the category choice, the stride-sample rationale,
exact usage, and carries the real reference-build numbers (5,389 hallmark
PHROGs, 436 integration/excision, 49,869/2,610 structures after capping, 137MB,
independently confirmed queryable via a self-search) as a sanity-check anchor
for anyone rebuilding it. Registered in mkdocs.yml's nav. Also documents,
explicitly, why this stays a manual step rather than something `phables
install` fetches -- phold's structure DB is a multi-GB, separately-versioned
asset this project shouldn't silently pin/re-host a copy of.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
createsubdb needs its own separate call for _ca (C-alpha coordinates), same
as _ss/_h -- confirmed against steineggerlab/foldseek#97, which builds an
AFDB subset the identical way (separate createsubdb calls against afdb,
afdb_ss, and afdb_ca). Not a side effect of the main "" createsubdb call.

Caught because the user's own real reference build (on HPC, against the
actual full phold DB) has hallmark_db_ca/integration_excision_db_ca files
that this script's suffix loop -- ("", "_ss", "_h") -- would never produce.
Without _ca, foldseek's structural search/alignment is missing the actual
3D coordinates, not just AA+3Di sequence -- an incomplete subDB, not merely
a smaller one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Changes the design from "every user runs build_hallmark_db.py themselves" to
"phables install fetches a pre-built tarball", matching how marker.hmm/the
PHROGs MMseqs profile DB already work -- per explicit direction: don't make
every installation build this.

- databases.yaml: new hallmark_db_url/_file/_folder keys. Named
  hallmark_db_url, NOT hallmark_db -- config.yaml already uses the bare
  "hallmark_db" key for the *runtime* --hallmark-db path, and configfile:
  entries share one flat config dict, so reusing the name would have silently
  clobbered the runtime value with this download URL. Caught before it
  shipped by re-reading both files side by side, not by running into it.
- install.smk: new hallmark_db_download rule + target, alongside the existing
  three. Extracts into a freshly-created output dir (mkdir -p {output}; tar
  -xf ... -C {output}) rather than straight to DBPATH like
  phrogs_mmseqs_download does -- that rule's tarball already contains its own
  top-level folder; this one's tarball members are expected at the archive
  root (see docs/hallmark_db.md's packaging section for the exact `tar -czhf`
  invocation this assumes, including why -h/dereference is required for
  createsubdb's .lookup/.source symlinks).
- 00_database_preflight.smk: when --hallmark-db/--hallmark-categories are
  unset (None, config.yaml's default), resolve them to the DBPATH-relative
  location phables install just fetched -- same "just works once installed"
  behaviour --databases's own marker.hmm/phrogs_mmseqs_db already have. An
  explicit --hallmark-db/--hallmark-categories still overrides this (only a
  None gets filled in).
- __main__.py: updated --hallmark-db/--hallmark-categories help text to
  describe the new auto-resolve behaviour instead of "(required for
  --phagedetection prostt5-foldseek)", which is no longer accurate.
- docs/hallmark_db.md: reframed around "phables install handles this" as the
  normal path; build_hallmark_db.py is now documented as the
  rebuild/reproducibility script, not a per-user step. Added a "Packaging a
  rebuild for redistribution" section with the exact tar command (verified
  locally that -h genuinely dereferences symlinks into portable real content,
  not just asserted).

hallmark_db_url is a real placeholder ("CHANGE-ME: ...") -- fill in once the
tarball is actually uploaded (e.g. to Zenodo).

Verified end-to-end with real Snakemake 9.25.1 dry-runs (not just read):
`phables install --dry-run` now includes hallmark_db_download as a target
alongside the other three downloads, and `phables run --phagedetection
prostt5-foldseek --dry-run` resolves cleanly with NO --hallmark-db/
--hallmark-categories flags passed at all, correctly finding the
placeholder files at the auto-resolved --databases-relative path. Full
pytest suite (12/12) still passes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Real hallmark_db_url filled in (https://zenodo.org/records/21884331/files/
hallmark_db.tar.gz?download=1), replacing the CHANGE-ME placeholder. This
supersedes an intermediate record (21883465) that had a stray nested
hallmark_db.tar.gz (~5MB, an earlier/partial tarball swept up by a `tar czf
... .` run from inside a directory that already had one) -- fixed upstream
by re-packaging cleanly, not by anything on this side.

Also corrected the -h/--dereference packaging guidance from the previous
commit: real testing confirmed phables doesn't need .lookup/.source at
query time at all (foldseek convertalis resolves target/query via the _h
header db, which createsubdb builds properly for the subset -- .lookup/
.source are createsubdb's own build-time input, not consulted downstream).
The actual hosted tarball has neither file, dereferencing is unnecessary,
and the packaging section now recommends --exclude'ing them outright
instead.

Verified for real, twice (once per Zenodo record): downloaded the real
tarball and inspected its full member listing (confirmed no .lookup/
.source/symlinks, files at archive root matching hallmark_db_download's
extraction assumption); ran actual `phables install --databases <dir>`
end-to-end against the live URL (not a dry-run -- a genuine download +
extract, "Databases are successfully setup!"); ran `phables run
--phagedetection prostt5-foldseek --dry-run` against that real installed
database with zero --hallmark-db/--hallmark-categories flags and confirmed
it resolves correctly. Full chain confirmed working on the actual artifact,
not a placeholder.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Was still describing pre-fork phables -- the static --help block hadn't been
updated since --databases, --build-tree, --genecaller, --phagedetection and
its whole ProstT5/Foldseek/hallmark-* family, and --gpu-backend/--foldseek-gpu
were added. Byte-diffed the replacement block against real `phables run
--help` output (only difference: a stray backspace control char in Click's
raw output, not meaningful content -- confirmed, not assumed).

Added:
- New flags to the "Run options explained" list (--build-tree, --genecaller,
  --databases).
- A dedicated "Phage-gene detection: --phagedetection" subsection covering
  both methods and every prostt5-foldseek-only flag, including the
  auto-resolve behaviour for --hallmark-db/--hallmark-categories.
- A worked example under "Example usage" for --phagedetection prostt5-foldseek,
  with and without --gpu-backend.
- Fixed the `preprocess` output description, which only ever documented the
  mmseqs path (phrogs_annotations.tsv) -- now covers both outputs
  (hallmark_hits.tsv for prostt5-foldseek), added the missing gene-calling
  step, and noted coverage is now via CoverM (PLAN.md §4.8), not koverage.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@codacy-production

codacy-production Bot commented Aug 11, 2026

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 3 critical · 6 high · 3 medium · 2 minor

Alerts:
⚠ 14 issues (≤ 0 issues of at least minor severity)

Results:
14 new issues

Category Results
ErrorProne 5 high
Security 2 minor
1 high
3 critical
3 medium

View in Codacy

🟢 Metrics 132 complexity · 2 duplication

Metric Results
Complexity 132
Duplication 2

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

gbouras13 and others added 3 commits August 12, 2026 10:03
The existing container/Dockerfile (3 prior commits: "first docker container",
"Dockerfile with all dependencies included", "updating the dockerfile and how
to use it") was a pre-fork artifact -- gurobi/optimizer:latest as a second,
unused FROM base, `mamba install phables=1.3.2` (a PyPI version from long
before this fork's pyrodigal-gv/ProstT5-Foldseek/koverage-removal/HiGHS work),
libfabric built from source. None of it reflects the current codebase.
Replaced entirely, not incrementally patched.

New Dockerfile builds FROM quay.io/pawsey/pytorch:2.7.1-rocm6.3.3 -- the same
base phold's own container (../phold/container/hpci/Dockerfile) uses,
deliberately: it's the exact torch==2.7.1+rocm6.3 build
envs/prostt5-rocm.yaml already pins and that's confirmed working on real
Setonix MI250X hardware this session. Nothing in this Dockerfile reinstalls
torch -- pholdlib and every other Python dependency pip-install directly into
the base image's own system Python, so predict_3di sees the same
already-correct torch/ROCm stack everything else does, per explicit
direction: don't rebuild a separate (and possibly conflicting) prostt5 env
for this container.

Covers every envs/*.yaml this workflow has: foldseek (direct binary, matching
phold's own install step exactly), minimap2/samtools/coverm/mmseqs2(=13.45111,
the exact pin)/fraggenescan/hmmer/mafft via a standalone micromamba into
/opt/conda, and phables' own Python deps (pyrodigal-gv, biopython,
python-igraph, pysam, flowpaths, cogent3/piqtree, ...) via pip into the base
image's Python. phables itself installed from this exact build context
(COPY . + pip install, not a fetched git ref) so CI builds exactly what was
committed. Real bugs caught and fixed on careful re-read before this ever ran
anywhere: three unquoted `pkg>=X.Y` version specs (coverm, pholdlib,
pyrodigal-gv) where the shell would have consumed `>` as redirection instead
of passing it to micromamba/pip -- confirmed the actual shell behavior with a
synthetic reproduction, not just inspection. Also confirmed the real
FragGeneScan binary name (run_FragGeneScan.pl, not `fraggenescan`) against
genes.smk's own invocation rather than guessing at the sanity-check command.

CI (.github/workflows/build_container.yaml) builds and pushes to
quay.io/<QUAY_USERNAME>/phables:<commit-sha> on every push/PR -- mirrors
phold's own real, working workflow
(../phold/.github/workflows/build_container.yaml) almost exactly, including
the free-disk-space cleanup step that workflow's own comments explain is
required (the ~14GB base image otherwise exceeds a fresh GitHub Actions
runner's free disk on the first FROM line).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extends the existing predict_3di-only --prostt5-container mechanism to the
whole workflow. Real motivation: --use-conda's per-rule env creation has a
genuine race condition at HPC scale, hit in production this session -- every
rule's conda env is named by a hash of its env file, so every sample's
`phables run` needs the SAME env directory, but each sample runs as its own
separate Snakemake process (a SLURM array task). Snakemake only serializes
env creation WITHIN one process's DAG, not across processes, so two array
tasks both needing an env for the first time at the same moment can race
`mamba env create` into the same directory and corrupt it ("Fatal Python
error: init_fs_encoding ... no codec search functions registered"). A single
pre-built container with everything already installed has nothing left to
race.

New `--container` CLI flag / config['container'] -> CONTAINER_IMAGE global
(02_phables_preflight.smk). Every rule across coverage.smk, genes.smk,
gfa2fasta.smk, phables.smk, phylotree.smk, postprocess.smk now does:
    conda:
        None if CONTAINER_IMAGE else os.path.join("..", "envs", "X.yaml")
    container:
        CONTAINER_IMAGE
-- applied programmatically (a verified regex transform, not 15 manual edits)
to guarantee identical treatment everywhere, then confirmed via a real
Snakemake dry-run that both the untouched default path and the new
--container path resolve the identical 18-job DAG cleanly.

predict_3di (already container-capable via --prostt5-container) needed no
rule changes at all: PROSTT5_CONTAINER now falls back to CONTAINER_IMAGE when
unset, so --container alone routes predict_3di through it too, while
--prostt5-container still overrides it specifically if ever needed (e.g.
pointing just that rule at a different image).

docs/container.md explains the race condition, how to run
(--container ... --use-singularity --no-use-conda), and is explicit about
what's NOT yet verified: no real `docker build` or Apptainer/Singularity
execution against Setonix has happened, only the DAG-structural dry-run.
docs/usage.md and mkdocs.yml updated to match; --help block re-verified
byte-for-byte against the real `phables run --help` output after adding the
flag (same as the prior CLI-surface documentation pass this session).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
cogent3 has no non-prerelease PyPI release, so a plain version-range
pip install (cogent3<2026.7) fails outright (pip filters prereleases
from range specs by default). Rather than pin an exact alpha version,
just drop cogent3/piqtree from the image -- phylotree/--phylotree
isn't part of this workflow's usage. Note --phylotree therefore isn't
supported under --container (build_tree still points at
CONTAINER_IMAGE, which now lacks these deps).
@Vini2
Vini2 merged commit bc07b10 into Vini2:develop Aug 12, 2026
13 of 17 checks passed
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