Skip to content

FunVIP 1.0.0 merge#70

Merged
Changwanseo merged 73 commits into
mainfrom
development
Jul 13, 2026
Merged

FunVIP 1.0.0 merge#70
Changwanseo merged 73 commits into
mainfrom
development

Conversation

@Changwanseo

Copy link
Copy Markdown
Owner

FunVIP 1.0.0 merge

Changwanseo and others added 30 commits April 14, 2026 21:40
ete3 is abandonware broken on Python 3.13 (removed cgi module). ete4 is
the maintained successor with Cython traversal and Python 3.13 support.

Key API changes applied:
- Import paths: ete4 + ete4.treeview for TreeStyle/NodeStyle/faces
- iter_leaves() → leaves(), get_common_ancestor() → common_ancestor()
- ladderize(direction=1) → ladderize(reverse=True)
- Tree(file, format=N) → Tree(file) (ete4 autodetects newick)
- write(format=0, outfile=...) → write(outfile=...)
- Guard all node.support and node.dist comparisons against None
  (ete4 sets dist=None for root nodes with no explicit branch length,
  and support=None for leaf nodes — ete3 defaulted to 0.0)
- Normalize dist=None → 0.0 after tree load and copy("newick")

Also updates: PyQt5 → PyQt6, requires-python <3.13 → <3.14

Validated: full FunVIP --test Penicillium run completed end-to-end
in 866s with no errors through tree interpretation and visualization.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main.py imported PyQt5 to initialize Qt before setting QT_QPA_PLATFORM=offscreen.
This was missed when migrating from ete3/PyQt5 to ete4/PyQt6, causing
ModuleNotFoundError on envs that only have PyQt6 (e.g. Python 3.13).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ete4's resolve_polytomy() assigns support=0 to newly created internal
nodes, whereas ete3 assigned DEFAULT_SUPPORT=1.0. Set support=1.0 on
new nodes immediately after resolve_polytomy() so the NWK output writes
1 instead of 0, matching ete3 behavior.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reflects the ete3→ete4 migration and associated bug fixes landed
since 0.5.9.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Also fixes the gene-clobber bug where the concatenated per-partition overlap
was only applied to the first pair.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cluster leaves by <= zero_init branches and scan only within-cluster pairs for
diff_min; when diff_min < zero_init the answer is max(diff_min - 1e-8, floor)
regardless of max_identical, so the O(n^2) phylogenetic distance matrix and the
all-pairs scan are skipped. Falls back to the full scan only for well-separated
data. Byte-identical self.zero, ~13x faster overall, and far less RAM.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A near-zero branch is clamped to the engine minimum and then Brent's optimizer
stops within its absolute tolerance of it, so identical sequences land anywhere
in [min, min+tol] (FastTree emits both 5e-9 and 6e-9). Floor at the top of that
band so noise-split identical clusters flatten. Verified no divergent over-merge
on Lactarius. fasttree 5e-9->6e-9, iqtree/raxml 1e-6->2e-6.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Highly conserved genes (5.8S) leave a giant star polytomy that
reroot_outgroup's resolve_polytomy() combs into a ~3681-deep chain,
overflowing cpickle's recursion inside the worker. Three coupled fixes:

- seperate_clade: copy the input via deepcopy, not the default cpickle
  copy, which RecursionErrors on the deep comb.
- concat_all: rebuild same-taxon groups as a BALANCED (log-depth) tree
  instead of a left-deep comb, so the reconstructed result stays shallow
  enough to pickle back to the parent (also drops the O(n^2) build).
- pipe: clear the stale tree_info.outgroup_clade after reconstruct; it
  still referenced the deep pre-reconstruct tree and re-inflated it when
  the worker pickled its result on multiprocessing return.

Lactarius test: 5.8S now interprets (122/122 query ASVs assigned, was
54/122); species identification unchanged (only within-species clade
renumbering on db rows from the reshaped same-taxon clusters).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
seperate_clade deep-copied every child upfront, so the recursive descent
of the resolve_polytomy comb (~D deep for conserved genes like 5.8S)
re-copied the whole remaining subtree at each level -> O(D^2). Only copy
what is kept (a single tip appended to clade_list, or the reconstruct
input) and descend the zero-length comb in place; seperate_clade and
reconstruct never mutate their input.

Byte-identical result.csv on the Lactarius test (same md5); interpretation
9m55s -> 7m33s.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two changes to the cluster step:

- Efficiency: replace cluster()'s per-FI scan of the whole concatenated
  search table (V_cSR[V_cSR["qseqid"] == FI.hash], O(N_FI * N_rows), with the
  entire table pickled to every mp worker) with a single in-process
  groupby("qseqid"). Benchmarks ~6x at 1k queries to ~79x at 6k and growing
  (per-FI is quadratic, groupby linear); the mp.Pool is removed.
  append_query_group's per-row .apply becomes a vectorized .map.

- Behavior: when a query's cutoff set spans >=2 groups, assign by group
  plurality among the hits TIED at the best bitscore, descending a bitscore
  level only if that count ties (was: the single top-bitscore hit's group,
  tie-broken by pandas' unstable quicksort). Deterministic and resistant to
  database-composition bias. On the Lactarius test it moves 6/122 queries:
  4 arbitrarily-Russula ASVs -> Lactarius (the plurality of their equally-best
  hits), 2 -> Inonotus (Inonotus dominates even their best hits).

Verified: the new cluster() reproduces the intended assignments on the real
search table (exactly the 6 changes); the pipeline runs end-to-end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Foundation for cleaner errors/logging, plus cluster.py as the first module:

- New funvip/src/exceptions.py: a FunVIPError hierarchy (Config/Input/Search/
  Cluster/Dataset/Tree/ExternalTool) so callers (and main) can tell an expected,
  explained failure from a bug.
- main(): wrap the pipeline (now _run_funvip()) in a top-level handler that
  reports a FunVIPError cleanly, logs an unexpected crash with a full traceback,
  and no longer swallows KeyboardInterrupt.
- cluster.py: context-free `raise Exception` -> ClusterError carrying the
  offending FI/group/gene; bare `except:` -> the specific exception
  (KeyError/IndexError/ValueError) with a reason; diagnostic print() -> logging.

Behavior-preserving: compiles, imports, FunVIP --help works, and the full
cluster step runs unchanged (verified on the Lactarius session).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the context-free "[DEVELOPMENTAL ERROR]" raise in combine_alignment
with a DatasetError naming the group and the genes actually present, so a group
that reaches concatenation without a real gene dataset fails with an actionable
message.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- exist_dataset: bare `except:` -> `except KeyError:` (the only expected miss).
- Replace five context-free "[DEVELOPMENTAL ERROR]" raises with typed errors
  carrying the offending values: an unexpected --level -> ConfigError; the
  list_FI/dict_hash_FI conflicts (final_species, adjusted_group, bygene_species)
  and the --terminate alignment-validation stop -> DatasetError.

Behavior-preserving: cluster step (generate_dataset/remove_invalid_dataset)
runs unchanged on the Lactarius session.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace nine context-free raises with typed, actionable errors: unknown search
method (build/run) and fragment-merge inconsistencies -> SearchError; a missing
database file or a gene present in the query but absent from the database ->
InputError; no valid gene in the database -> ConfigError. Narrow the bare
`except:` guarding the (possibly deleted) df_search to `except NameError:`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Tree import: bare `except:` -> `except Exception as e: raise TreeError(...) from e`
  preserving the parse cause; missing tree file and duplicated tree_info -> TreeError
  with the offending path / group+gene.
- Species-label parse: bare `except:` -> `except (ValueError, IndexError):` (label
  without a trailing integer).
- The _safe per-item guards now log the full traceback (not just repr) on a failed
  (group, gene) interpretation/visualization, so a skipped item is debuggable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bring the module onto the logging/exception hierarchy (it previously used only
print and bare raise):

- Add `import logging`; route the 38 diagnostic/error/warning print()s (many
  ANSI-colored manual logging) to logging.debug/info/warning.
- Replace all 16 context-free `raise Exception` with TreeError carrying the
  offending clade/leaf/group/tree. In reroot_outgroup the control-flow
  raise->fallback still works (TreeError subclasses Exception, so the fallback
  `except` catches it).
- Narrow the 7 bare `except:` to the specific exception (KeyError/ValueError/
  TypeError/Exception) with a reason; the reroot and viz type-detection
  fallbacks are preserved and Ctrl-C is no longer swallowed.
- Fixes a latent NameError ({resety} typo) in a dead error path.

Verified result-preserving: `--continue --step visualize` on the Lactarius
session (same cached trees) gives a byte-identical result.csv, terrei runs
clean, and worker-process logging works with no deadlock.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two statically-wrong conditionals that silently degrade results:
- trim.py: `if trimming_result is not ((-1,-1))` compared a tuple by identity
  (always true) -> a failed/over-aggressive trim slipped through and produced a
  one-column alignment. Compare by value.
- tree.py: `if list_og_FI == 0` compared a list to 0 (always false) -> an
  outgroup-less dataset was never skipped. Use `len(...) == 0`.

ext.py: check every external tool's exit code and raise SearchError/
ExternalToolError instead of feeding empty/stale output forward (blast, mmseqs,
makeblastdb, makemmseqsdb, mafft, trimal, fasttree, iqtree); guard Gblocks
parsing against a missing/format-drifted .gb.txt (was UnboundLocalError/
FileNotFoundError); wrap RAxML's os.chdir in try/finally so cwd is always
restored; quote trimal's Linux paths; drop leaked os.devnull fds (use
subprocess.DEVNULL); typed raises; remove debug prints.

trim.py: guard the empty trimmed-FASTA read (IndexError); modeltest.py: drop the
undefined `model_dict["BIC"]` line that NameError'd into a misleading "cannot
parse" warning every run.

Verified: terrei smoke test runs clean (exit-code checks do not false-positive).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
validate_option.py:
- RESULT-AFFECTING: `CLUSTER-CUTOFF` preset key was written to cluster.evalue,
  not cluster.cutoff, so the shipped presets' 0.97 was silently discarded and
  clustering ran at the 0.95 default. Now applies (verified via a preset harness
  and a terrei run). This changes group-clustering results for --preset runs.
- Systemic: `key.lower() in ("string")` in update_from_preset was a substring
  test, not tuple membership (missing comma) -> preset keys collided (search->
  nosearchresult, trim->allow-innertrimming, solveflat->nosolveflat, cachedb->
  nocachedb). Converted all 51 single-string tests to 1-tuples and added the
  missing positive solveflat/cachedb branches.
- `~` (bitwise-not) on bools -> `not`; typos parser_dicy/parse_dict -> parser_dict;
  preset outgroupoffset wrote to wordsize -> outgroupoffset.
- CLI: `--all` was `~parser.all` (a no-op) -> False; `--outgroupoffset` read the
  nonexistent parser.cluster_outgroupoffset (AttributeError swallowed) -> read
  parser.outgroupoffset.
- validate(): method dict lookups `X_adjust[key]` KeyError'd on invalid input ->
  `.get(key, key)`; undefined `color` in the --highlight/--outgroupcolor error ->
  the actual attribute; max_target_seqs<=0 reset cluster.outgroupoffset ->
  max_target_seqs; `list_warning(...)` called a list -> .append; impossible
  `gt < 0 and gt >= 1` -> `or`.

validate_input.py: query count comprehension counted all seqs (no filter) ->
sum with condition; query tables saved to 01_DB -> 02_Query by datatype;
`opt.gene = list(set(...))` reordered genes nondeterministically -> dict.fromkeys
(preserve CLI order); `not x == None` -> `is not None`.

preset/accurate.yaml: `CONFIDENT: fale` typo -> false.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- reporter.py: the report.txt [OPTION] block printed the DB and QUERY paths
  swapped; the per-gene result guard tested `gene in FI.bygene_species` but then
  indexed `FI.seq[gene]` -> a latent KeyError that would abort result.csv
  generation if the two dicts diverged (now also requires `gene in FI.seq`).
- save.py: save_session had no per-key guard, so a single unpicklable object
  aborted the whole run at a checkpoint -> log a warning and continue; the xlsx
  size-limit fallback wrote CSV bytes to a `.xlsx`-named file (now writes a
  `.csv`) and the column guard was off-by-one (16383 -> 16384).
- tool.py: get_genus_species had a mutable default list under @lru_cache -> tuple.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cazlculation->calculation, peformed->performed, stasifies->satisfies,
Pararellize->Parallelize, reoprt->report, delimitaion->delimitation,
unexpectively->unexpectedly, simultaniously->simultaneously.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
decode() compiled `"|".join(hash_dict.keys())` -- a regex with one alternative
per hash -- and re-escaped every match to look it up, giving O(text * N_hashes)
behaviour that is a hotspot at metabarcoding scale. Every hash has the fixed,
self-delimiting form HS<number>HE, so match that single token `HS\d+HE` once and
dict-look-up each occurrence (a token absent from the dict is left unchanged).
re.escape was a no-op for these alphanumeric tokens, so dropping it (here and in
decode_df, which also loses an unnecessary deepcopy) changes nothing.

Verified byte-identical to the previous implementation across all newick/svg
combinations, decode_df, and edge cases (adjacent hashes, prefix-overlapping
HS1HE/HS12HE, tokens not in the dict, unicode/special-char values); a terrei run
produces a fully decoded tree and a result.csv identical to the pre-change run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…alized

get_genus_species(genus_list=None) read the module global `genus_file`
(path.genusdb, ~3094 lines) with `open(...)` on EVERY call, and validate_input
calls it ~15x per sequence -> the 37KB genus DB was re-opened and re-parsed tens
of thousands of times at metabarcoding scale. Cache the parsed list keyed by
path (read once). If the global was never set by initialize_path() the open
raised a cryptic `NameError: genus_file`; now raise an actionable ValueError.

get_genus_species is @lru_cache'd, so this pairs with the earlier endwords
list->tuple fix (hashable args); pipeline callers already pass genus_list as a
tuple. Verified: uninitialized -> clear ValueError, results identical to passing
genus_list explicitly, and a terrei run produces a byte-identical result.csv.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Chang Wan Seo and others added 29 commits July 8, 2026 09:46
Runs the full pipeline on the bundled terrei dataset and asserts stable
invariants (79 queries, all Aspergillus, all species assigned) rather than an
exact byte match, since mmseqs/mafft/fasttree are nondeterministic across runs.
Marked `integration`; skipped unless external tools are on PATH and
FUNVIP_TEST_EMAIL is set.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
command.py: correct help strings whose stated defaults no longer matched the code
or the default fast preset (--search mmseqs->blast, --mode validation->
identification, --fsize 10->14, --maxoutgroup 1->3, --cluster-evalue 1e-7->1e-4,
--backgroundcolor colours, --collapsebscutoff/--bootstrap notes); drop the [WIP]
placeholder on --step and describe it; add missing type= on --maxoutgroup (int)
and --mafft-algorithm/--trimal-algorithm (str); add defaults to --trimal-gt and
--criterion; lowercase the --test dataset names.

docs/: add gen_param_reference.py (introspects the live argparse parser) and the
generated docs/parameters.md, so the parameter table can't drift from command.py.

README.md: fix the Results section to the actual outputs ({runname}.result.csv /
.dataset.csv / .report.txt and 07_Tree/*.svg, not the old xlsx names); drop the
stale 0.3.25 python-version notice; link the parameter reference; add a
`--test terrei` quick-check and a "Running the tests" section.

Verified: FunVIP --help renders, terrei runs clean, explicit new-type args parse.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
version.py probed all 8 external tools unconditionally at startup with no error
handling, so a user who has only the tools for their chosen methods (e.g. the fast
preset's blast/mafft/fasttree) crashed on the first unrelated missing tool (mmseqs)
with a bare FileNotFoundError.

- Version now probes via a robust helper that returns '' for any missing or
  unparseable tool instead of crashing, and collapses the three near-duplicate
  per-platform branches.
- New preflight(opt): checks only the tools the selected --search/--trim/
  --modeltest/--tree (+ TCS) will actually invoke, logs a "tools found" summary,
  warns-and-skips for optional TCS (generalizing the existing graceful-degrade),
  and raises a clear ConfigError listing the missing required tools and how to
  install them. Wired into main.py right after logging setup, so a missing tool
  fails fast with an actionable message instead of mid-run.

Verified: terrei runs clean and logs the summary; the preflight raises for a
missing required tool, warns for missing optional TCS, and is method-aware
(terrei's TRIM=none correctly omits trimAl).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The reliable install path is: external tools via conda (bioconda/conda-forge),
FunVIP + its Python deps (incl. ete4) via pip -- ete4 4.4.0 pip-installs cleanly
on Linux/macOS (the Cython issue is Windows-specific).

- environment.yml: one-command conda/mamba env with the 8 tools (+ optional
  t-coffee) and FunVIP from PyPI; a comment explains the from-source variant.
- Dockerfile: miniforge-based image that bundles everything, with a headless-Qt
  env var and FunVIP as the entrypoint -- for Windows/WSL or awkward installs.
- README: a self-contained "Quick install (conda / mamba)" + Docker section, so
  users are not solely dependent on the external tutorial links.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Statistics: re-implement the per-group identification summary (it was dead
  code, and the removed version even counted STATUS strings that no longer exist).
  update_statistics() aggregates the real STATUS values (match/assigned ->
  IDENTIFIED, new species -> NEW SPECIES CANDIDATE, conflict -> MISIDENTIFIED,
  failed -> UNDETERMINED) per GROUP_ASSIGNED with a grand-total row, written to
  {runname}.statistics.csv and a [STATISTICS] block in report.txt. Verified the
  counts match the raw STATUS column exactly.
- HTML report: implement the report_html stub (it also had a self-less
  signature) -> a self-contained {runname}.report.html with inline CSS, the
  statistics and identification tables, and relative links to the collapsed tree
  SVGs; wired into the report step. Verified well-formed on terrei.

(Provenance was already covered: report.txt carries [VERSIONS] with FunVIP +
external-tool versions, [COMMAND], and [OPTION].)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Unit test for Report.update_statistics: verifies STATUS values map to the right
categories per group with a correct grand-total row, and that an empty result is
handled. Add tabulate to the CI unit-test deps (reporter imports it; it does not
pull in ete4, so the tests stay lightweight).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- installation.md: standardize on python 3.12 everywhere (Linux said 3.11, a
  leftover from the old TCS constraint); add the missing external-tool install
  step to the from-source recipe; rewrite the Windows section to conda/WSL/Docker
  since a bare `pip install` fails on ete4's Cython there; add the recommended
  `environment.yml` one-liner; note that TCS/t-coffee is optional and `--test` is
  case-insensitive.
- development.md: drop the two references to genus_count (removed as dead code).
- tutorial.md: point the install links at installation.md with valid anchors
  (they pointed at nonexistent README anchors).
- README.md: fix the installation.md links (double-## -> single-# lowercase
  anchors).
- tutorial2.md / advanced.md: typo fixes (sequneces, sould, specie).

Verified against the working env: the Linux/from-source tool list and the pip+
conda split match what actually installs; `--test Terrei` works because the code
lowercases it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reproduced on a fresh env: the bioconda t-coffee build leaks memory unboundedly
even on `t_coffee -help`/`-version` (consumed ~140 GB before being killed), and
hangs on the real `-evaluate` call. FunVIP made this catastrophic because:
  - the TCS availability probe ran `t_coffee -help` via subprocess with NO
    timeout, so option validation hung and ate all RAM; and
  - the actual TCS call ran t-coffee with no memory or time bound.

Fixes:
- validate_option.py: check t-coffee with shutil.which (presence only) instead of
  executing `t_coffee -help` -- never run t-coffee just to detect it.
- ext.py TCS: run t-coffee under an 8 GB address-space cap (RLIMIT_AS) in its own
  session, with a 300 s timeout that kills the whole process group -- a leak is
  capped, a hang is bounded, no orphaned t-coffee.
- dataset.py: make the TCS step non-fatal (log and skip on failure) and guard the
  .tcs score-file consumption (skip missing/unparseable files) so a failed TCS
  does not crash the run at the reader.
- environment.yml: stop installing t-coffee by default (commented, with a note),
  so the recommended install cannot trigger this. TCS stays opt-in; FunVIP skips
  it when t-coffee is absent.

Verified on a fresh conda env WITH the leaky t-coffee: run completes (result.csv
produced), TCS times out -> killed -> skipped, peak memory bounded to ~4.5 GB
(vs ~140 GB before). With t-coffee absent, terrei is byte-identical.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diagnosed why t-coffee "leaks all memory" (it is not a leak). The bioconda
t-coffee indexes a static array by the raw OS PID but is compiled with
MAX_N_PID=260000. Modern kernels default kernel.pid_max=4194304, so t-coffee's
own PID overflows that array on essentially every run -> out-of-bounds ->
SIGSEGV -> its signal handler re-runs the faulting instruction forever, growing
the heap until all RAM is gone. It is a crash-loop, fires on ANY invocation
(even `t_coffee -version`), and is independent of MAX_N_PID_4_TCOFFEE, which is
not compiled into the binary (only the "Recompile" error string is present).

Evidence: strace shows a flood of identical SIGSEGV(SEGV_MAPERR) at one address
after "--ERROR: MAX_N_PID exceded (current: 260000 Requested: <pid>)"; peak RSS
is identical for MAX_N_PID_4_TCOFFEE unset/1000/4194304. No runtime userspace
fix works here: unprivileged user namespaces are denied (uid_map EPERM) and
t-coffee reads the PID via a direct syscall (LD_PRELOAD getpid() shim bypassed).
Working TCS requires a t-coffee built with MAX_N_PID >= pid_max, or a host with
pid_max < 260000 -- neither of which FunVIP can do at runtime, so the cap +
timeout + skip added earlier is the correct handling. This commit only corrects
the code comments and the environment.yml note to state the true root cause.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TCS (T-Coffee's alignment-consistency score) is an optional FunVIP feature, but
the prebuilt bioconda t-coffee crash-loops and can consume all system memory on
modern kernels: it indexes a table by raw PID, sized by a compile-time
MAX_N_PID=260000, and kernel.pid_max=4194304 overflows it (SIGSEGV handler
re-faults forever). Setting MAX_N_PID_4_TCOFFEE is not enough -- the table is
allocated with the compile-time constant regardless -- so the only real fix is to
recompile with MAX_N_PID >= pid_max.

Because T-Coffee is GPL with an added "non-commercial pipelines only" caveat,
FunVIP does not redistribute a t-coffee binary. Instead this adds a build recipe
the user runs themselves (accepting T-Coffee's license directly):
- tools/tcoffee/build_tcoffee_for_tcs.sh: downloads the pinned, checksum-verified
  T-Coffee source, applies the one-line MAX_N_PID patch, builds only the t_coffee
  binary with modern-g++ flags, self-tests it (TCS on a tiny alignment; fails if it
  still crashes), and installs it. Verified end-to-end: t_coffee -version and a
  real TCS run go from a ~3 GB crash-loop to 20 MB / 0.5 GB and a valid .tcs.
- tools/tcoffee/max_n_pid.patch: the exact one-line diff (documents the change).
- tools/tcoffee/README.md: the root cause, usage, and the T-Coffee license notice.
- installation.md: stop telling users to `conda install t-coffee` (both recipes);
  explain TCS is optional and point to the build script.
- validate_option.py: the "t-coffee not found" warning now points to the recipe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ete4 is a hard FunVIP dependency but upstream ships no Windows build: PyPI has an
sdist only (so pip compiles, which fails on Windows) and conda-forge has no win-64.
That is the real blocker to a pip/conda FunVIP install on Windows (t-coffee/TCS is
Linux-only and unrelated). ete4 is plain GPL-3.0-or-later, so redistributing a
self-built Windows wheel is allowed (offer source, ship the patch, keep LICENSE).

Adds a manual (workflow_dispatch) workflow that downloads the pinned ete4 sdist,
applies the small etetoolkit/ete PR #783 path-separator fix, builds win_amd64
wheels for CPython 3.10-3.13 with cibuildwheel, import-tests the compiled
extension, and uploads them as an artifact.

- tools/ete4-windows/ete4-windows.patch: the 3-line PR #783 diff (setup.py uses
  os.path.sep for Cython module names; config.py uses expanduser('~') not
  os.environ['HOME']). Verified: applies cleanly to 4.4.0 and is a no-op on Linux
  (the patched source still builds a normal Linux wheel).
- tools/ete4-windows/README.md: rationale, how to run, how to host/use the wheels,
  the "upstream it" option, and the ete4 GPL notice.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n build)

The intro wrongly listed conda as a Windows install option. Neither pip nor conda
can install FunVIP on Windows because ete4 ships no Windows build (PyPI is
source-only; conda-forge has no win-64). Correct the intro and the Windows section
to say WSL/Docker only, and point to the in-progress tools/ete4-windows/ wheel
recipe for future native support.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Match the project's no-em-dash convention; replace with commas, colons,
semicolons, or parentheses. No content change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ete4 has no Windows wheel on PyPI and cannot compile there, which blocked any
pip/conda install of FunVIP on Windows. Instead of asking Windows users to fetch a
wheel manually, FunVIP now carries prebuilt ete4 Windows wheels and installs the
right one automatically on first run, so the Windows recipe is the same four lines
as the other platforms (conda create / activate / pip install FunVIP / run).

- pyproject.toml: ete4 is now a dependency only off Windows
  (`platform_system != 'Windows'`), so `pip install FunVIP` no longer tries to
  compile ete4 on Windows. ete4's own runtime deps (bottle/cheroot/brotli/requests)
  are added as Windows-only deps so the bundled wheel installs with --no-deps and
  stays fully offline. funvip/_vendor/ete4_wheels is packaged as data.
- funvip/main.py: `_ensure_ete4()` runs at the start of `_run_funvip()` (before any
  ete4 import). On Windows, if ete4 is missing it pip-installs the bundled wheel
  matching the interpreter (cp310-cp313), with a clear error if none is bundled.
  No-op on Linux/macOS and once ete4 is importable.
- funvip/_vendor/ete4_wheels/: drop-in location for the wheels (built by the
  build-ete4-windows-wheels workflow), with ete4's GPLv3 LICENSE and a README. The
  wheels themselves are added at release time.
- installation.md: Windows section is now the same recipe as Linux/macOS.
- tools/ete4-windows/README.md: document dropping the wheels into _vendor.

Verified on Linux: main.py compiles; _ensure_ete4() is a clean no-op when ete4 is
present; the ete4 marker requires ete4 on Linux but not Windows; the wheel-selection
glob picks the right file; and a built FunVIP wheel ships funvip/_vendor/ete4_wheels.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Trim the ete4/bundling and t-coffee internals from the user-facing install docs
(they confused readers). Windows is now the same four-step recipe as Linux/macOS;
the TCS note is a short pointer. The technical details still live in
tools/ete4-windows/ and tools/tcoffee/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
These 25 stale .pyc files were committed before __pycache__/ was added to
.gitignore; untrack them (they can shadow current source and bloat the repo).
.gitignore already excludes __pycache__/ and *.pyc, so they will not return.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Previously _ensure_ete4 errored out on Windows if no prebuilt wheel matched the
interpreter. Now it falls back to building ete4 from source: download the sdist,
apply the Windows patch (etetoolkit/ete PR #783, applied in-process so no git/patch
tool is needed), and pip-install it. This needs a C/C++ compiler and internet and
runs at most once; a clear message (including `conda install -c conda-forge
cxx-compiler`) is shown if it fails. Bundling wheels is still preferred so ordinary
Windows users need no compiler.

Verified end-to-end on Linux: download + in-process patch + compile + install
produces a working ete4 (~50s); patch helper is idempotent and raises on upstream
text drift; the no-op path (ete4 already importable) is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The source-build path used `pip download --no-binary :all:`, which forced ALL
packages (including Cython) to compile from source; building Cython on Windows
failed with a linker error (LNK1104). Scope it to `--no-binary ete4` so Cython and
setuptools install as wheels and only ete4 is built. Verified end-to-end on Linux.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ete4 auto-build was broken for every fresh Windows install: it fetched
the sdist via `pip download --no-binary ete4`, which runs the sdist's
own (unpatched) setup.py through pip's build-hook resolution before we
ever get a chance to apply the Windows path-separator patch, so it
always failed with "'ete4\core\operations' is not a valid module
name". Fetch the raw sdist tarball via the PyPI JSON API instead,
bypassing pip's build backend until after patching.

Separately, validate_input.py invoked GenMine as a bare shell command,
so a stale/incompatible GenMine elsewhere on PATH could silently
shadow the one installed alongside FunVIP. Its older output format
doesn't match what FunVIP parses, so accessions were never replaced by
sequences and the run failed downstream with "no valid gene found in
the database" -- with no indication of the real cause. Resolve GenMine
relative to sys.executable instead, and normalize download_dict keys
to match the version-stripped lookup.

Also fix .gitignore gaps left over from the funid/funip -> funvip
rename (MAFFT_Windows, mmseqs busybox symlinks, the runtime db cache)
and ignore local .venv* test environments.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
_resolve_genmine used os.path.dirname(sys.executable), which on Windows is the env
root, not Scripts\, so GenMine.exe was never found there and it fell back to a bare
PATH lookup -- defeating the anti-shadowing purpose on the platform it targets. Use
sysconfig.get_path("scripts") (bin/ on POSIX, Scripts\ on Windows). Verified the
resolver still finds GenMine on Linux.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Explain that 0.5.x (ete3) and 1.0 (ete4) are different packages, so rebuild the
conda environment instead of pip-upgrading across the jump. Corrects the earlier
draft's overstated "FunVIP fails to import" wording (with the Windows bundled-ete4
marker a pip upgrade would not fail that way; the real issue is stale mixed deps).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Built by reusing the download+patch steps from _build_ete4_from_source
in funvip/main.py (fetch the PyPI sdist, apply the PR #783 Windows
path-separator patch), then `pip wheel . --no-deps` instead of `pip
install`. Verified MSVC's cl.exe only warns on the upstream `-O3` flag
(D9002: unknown option, ignored -- compiles at /O2 instead) rather
than failing, so the build succeeds unmodified on Windows with VS
2022 installed.

With this wheel present, Windows users on Python 3.12 get ete4 via
`_ensure_ete4()`'s bundled-wheel path instead of compiling from source,
so no C/C++ compiler is required. cp310/cp311/cp313 wheels are left
for the build-ete4-windows-wheels GitHub Action to produce on a clean
runner.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bump version 0.6.0 -> 1.0.0 (single source of truth in pyproject.toml; FunVIP
--version reads it via importlib.metadata). Add CHANGELOG.md (the ete3->ete4 port,
performance, fixes, Windows support) and RELEASING.md (maintainer checklist:
bundle ete4 wheels, verify on all platforms, build/publish, tag).

Verified on Linux: FunVIP --version -> 1.0.0, 28 unit tests pass, terrei
integration passes end-to-end (~69s).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
conda-forge's ete4 lags PyPI (max 4.3.0) and has no 4.4.0, so
`conda install "ete4>=4.4.0,<4.5.0"` had no solve candidate and the job failed. ete4
is not reliably a conda package; the documented install uses conda for the external
tools and pip for FunVIP + ete4. Drop the conda-install-ete4 step and let
`pip install -e ".[test]"` pull and build ete4 from the PyPI sdist (as the unit
matrix and local builds already do). Unit jobs were unaffected (they install no ete4).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The install-smoke job's `python -c "import funvip.main"` failed: importing
funvip.main runs _ensure_deterministic_hash, which re-execs the interpreter to set
PYTHONHASHSEED, and for `python -c` the re-exec drops the code string and errors
("Argument expected for the -c option", exit 2). Use `FunVIP --version` instead --
it re-execs cleanly (console-script argv survives) and still imports the whole
chain including ete4. The pip-install step (ete4 via pip) already passed; this was
the only remaining failure.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The preset referenced FunVIP_Sanghaungporus_db.xlsx (transposed "au"), but the
bundled file is FunVIP_Sanghuangporus_db.xlsx, so `--test sanghuangporus` could not
find its database and failed. Corrected the reference. Verified: the run now
completes (exit 0) and writes a 274-row result CSV (single-gene ITS with
CONCATENATE:true works).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The install-smoke job kept failing on the import/entry-point step because ete4
imports PyQt6 (QtGui/QtWidgets) at import time, and PyQt6's manylinux wheel bundles
only ICU + Qt6 -- it needs libGL/libEGL/libX11/libxcb/libxkbcommon/libdbus/glib/...
from the system, a large runner-dependent set. Rather than chase those libs, keep
the smoke reliable: the install step already proves ete4 builds from its PyPI sdist
and the whole dependency set resolves in a fresh env; the smoke then verifies only
that funvip's (import-safe) package imports and the unit tests pass. The full
entry-point/GUI path is exercised by the terrei integration test and real runs, not
by CI. Verified locally: `import funvip` pulls neither ete4 nor PyQt6; pytest passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Paste-ready notes (highlights, install/upgrade, full grouped changes, known issues)
derived from CHANGELOG.md, for the v1.0.0 GitHub release.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds an install-smoke job on macos-latest (arm64) mirroring the Linux one: fresh
conda env, pip install FunVIP (which builds ete4 from its PyPI sdist -- conda-forge
lags the pinned version), then the Qt-free smoke + unit tests. The macOS-specific
value is confirming ete4 compiles on osx-arm64. Lets the Mac install be verified on
GitHub's macOS runners without owning a Mac.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Changwanseo
Changwanseo merged commit 424a6fa into main Jul 13, 2026
20 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.

1 participant