Claude Skill

alterlab-phylogenetics

Build phylogenetic trees end-to-end from raw sequences — MAFFT multiple sequence alignment, optional TrimAl trimming, IQ-TREE 3 maximum-likelihood inference with model selection and bootstraps, FastTree for large datasets, then visualize with ETE3 or FigTree. Use when reconstruct

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download alterlab-ieu-alterlab-academic-skills-skills_bioinformatics_alterlab-phylogenetics-e4836c0.zip · 13 KB
Part of alterlab-ieu/alterlab-academic-skills — 94 skills

Install

skills CLI npx skills add https://github.com/AlterLab-IEU/AlterLab-Academic-Skills/tree/main/skills/bioinformatics/alterlab-phylogenetics
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install alterlab-ieu-alterlab-academic-skills@llmmart
Git git clone https://github.com/AlterLab-IEU/AlterLab-Academic-Skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole alterlab-ieu/alterlab-academic-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Phylogenetics

Overview

Phylogenetic analysis reconstructs the evolutionary history of biological sequences (genes, proteins, genomes) by inferring the branching pattern of descent. This skill covers the standard pipeline:

  1. MAFFT — Multiple sequence alignment
  2. IQ-TREE 3 — Maximum likelihood tree inference with model selection
  3. FastTree — Fast approximate maximum likelihood (for large datasets)
  4. ETE3 — Python library for tree manipulation and visualization

Installation: The aligners and tree builders are compiled CLI tools (not on PyPI). Install the binaries via bioconda or Homebrew; install the Python visualization layer with uv.

# CLI binaries — bioconda (cross-platform) ...
conda install -c bioconda mafft iqtree fasttree trimal   # iqtree is now IQ-TREE 3
# ... or Homebrew on macOS (Apple Silicon): IQ-TREE/TrimAl live in the brewsci/bio tap
brew install mafft fasttree
brew tap brewsci/bio && brew install brewsci/bio/iqtree brewsci/bio/trimal

# Python visualization layer
uv pip install "ete3==3.1.3"   # also needs numpy<2 and PyQt5 for rendering

Binary name: bioconda's iqtree package is now IQ-TREE 3 (3.1.x) and installs the executables iqtree and iqtree3 — there is no iqtree2 in it. Call iqtree, which works on both the 2.x and 3.x packages; only reach for a versioned name when two releases coexist on PATH. IQ-TREE 3 adds MixtureFinder, multi-tree mixture models (MAST), gene/site concordance factors, and CMAPLE for very large pathogen datasets; the options below are unchanged from 2.x.

ETE3 (3.1.3) is the last ete3 release and can be fragile to install on Python ≥3.12 (pins old numpy/PyQt5). If t.render() fails, fall back to writing the Newick tree and viewing it in FigTree/iTOL, or use the maintained successor ete4 (note: ete4 changed the TreeStyle/render API, so the snippets below are ete3-specific).

When to Use This Skill

Use phylogenetics when:

  • Evolutionary relationships: Which organism/gene is most closely related to my sequence?
  • Viral phylodynamics: Trace outbreak spread and estimate transmission dates
  • Protein family analysis: Infer evolutionary relationships within a gene family
  • Horizontal gene transfer detection: Identify genes with discordant species/gene trees
  • Ancestral sequence reconstruction: Infer ancestral protein sequences
  • Molecular clock analysis: Estimate divergence dates using temporal sampling
  • GWAS companion: Place variants in evolutionary context (e.g., SARS-CoV-2 variants)
  • Microbiology: Species phylogeny from 16S rRNA or core genome phylogeny

Does NOT Trigger

Scenario Use Instead
Manipulating or comparing an existing Newick tree (prune, root, Robinson-Foulds, duplication/speciation) alterlab-etetoolkit
Plain sequence parsing, translation, or format conversion alterlab-biopython
Homology search to find the sequences before aligning them alterlab-blast
Diversity/ordination on a community feature table (UniFrac, PCoA, PERMANOVA) alterlab-scikit-bio
Running the 16S amplicon pipeline that produces the ASVs and tree alterlab-qiime2-amplicon

Standard Workflow

1. Multiple Sequence Alignment with MAFFT

import subprocess
import os

def run_mafft(input_fasta: str, output_fasta: str, method: str = "auto",
               n_threads: int = 4) -> str:
    """
    Align sequences with MAFFT.

    Args:
        input_fasta: Path to unaligned FASTA file
        output_fasta: Path for aligned output
        method: 'auto' (auto-select), 'einsi' (accurate), 'linsi' (accurate, slow),
                'fftnsi' (medium), 'fftns' (fast), 'retree2' (fast)
        n_threads: Number of CPU threads

    Returns:
        Path to aligned FASTA file
    """
    methods = {
        "auto": ["mafft", "--auto"],
        "einsi": ["mafft", "--genafpair", "--maxiterate", "1000"],
        "linsi": ["mafft", "--localpair", "--maxiterate", "1000"],
        "fftnsi": ["mafft", "--retree", "2", "--maxiterate", "2"],
        "fftns": ["mafft", "--retree", "2", "--maxiterate", "0"],
        "retree2": ["mafft", "--retree", "2"],
    }

    cmd = methods.get(method, methods["auto"])
    cmd += ["--thread", str(n_threads), "--inputorder", input_fasta]

    with open(output_fasta, 'w') as out:
        result = subprocess.run(cmd, stdout=out, stderr=subprocess.PIPE, text=True)

    if result.returncode != 0:
        raise RuntimeError(f"MAFFT failed:\n{result.stderr}")

    # Count aligned sequences
    with open(output_fasta) as f:
        n_seqs = sum(1 for line in f if line.startswith('>'))
    print(f"MAFFT: aligned {n_seqs} sequences → {output_fasta}")

    return output_fasta

# MAFFT method selection guide:
# Few sequences (<200), accurate: linsi or einsi
# Many sequences (<1000), moderate: fftnsi
# Large datasets (>1000): fftns or auto
# Ultra-fast (>10000): mafft --retree 1

2. Trim Alignment (Optional but Recommended)

def trim_alignment_trimal(aligned_fasta: str, output_fasta: str,
                            method: str = "automated1") -> str:
    """
    Trim poorly aligned columns with TrimAl.

    Methods:
    - 'automated1': Automatic heuristic (recommended)
    - 'gappyout': Remove gappy columns
    - 'strict': Strict gap threshold
    """
    cmd = ["trimal", f"-{method}", "-in", aligned_fasta, "-out", output_fasta, "-fasta"]
    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0:
        print(f"TrimAl warning: {result.stderr}")
        # Fall back to using the untrimmed alignment
        import shutil
        shutil.copy(aligned_fasta, output_fasta)
    return output_fasta

3. IQ-TREE 3 — Maximum Likelihood Tree

def run_iqtree(aligned_fasta: str, output_prefix: str,
                model: str = "TEST", bootstrap: int = 1000,
                n_threads: int = 4, extra_args: list = None) -> dict:
    """
    Build a maximum likelihood tree with IQ-TREE 3.

    Args:
        aligned_fasta: Aligned FASTA file
        output_prefix: Prefix for output files
        model: 'TEST' for automatic model selection, or specify (e.g., 'GTR+G' for DNA,
               'LG+G4' for proteins, 'JTT+G' for proteins)
        bootstrap: Number of ultrafast bootstrap replicates (1000 recommended)
        n_threads: Number of threads ('AUTO' to auto-detect)
        extra_args: Additional IQ-TREE arguments

    Returns:
        Dict with paths to output files
    """
    cmd = [
        "iqtree",
        "-s", aligned_fasta,
        "--prefix", output_prefix,
        "-m", model,
        "-B", str(bootstrap),   # Ultrafast bootstrap
        "-T", str(n_threads),
        "--redo"                # Overwrite existing results
    ]

    if extra_args:
        cmd.extend(extra_args)

    result = subprocess.run(cmd, capture_output=True, text=True)

    if result.returncode != 0:
        raise RuntimeError(f"IQ-TREE failed:\n{result.stderr}")

    # Print model selection result
    log_file = f"{output_prefix}.log"
    if os.path.exists(log_file):
        with open(log_file) as f:
            for line in f:
                if "Best-fit model" in line:
                    print(f"IQ-TREE: {line.strip()}")

    output_files = {
        "tree": f"{output_prefix}.treefile",
        "log": f"{output_prefix}.log",
        "iqtree": f"{output_prefix}.iqtree",  # Full report
        "model": f"{output_prefix}.model.gz",
    }

    print(f"IQ-TREE: Tree saved to {output_files['tree']}")
    return output_files

# Model selection: ModelFinder Plus (-m MFP) is the DEFAULT — omitting -m runs it.
# -m TEST is the narrower jModelTest/ProtTest-style search (no FreeRate models);
# use it only when you need to match those tools.
# Typical winners — DNA: GTR+F+I+G4, HKY+F+G4; Protein: LG+F+G4, Q.pfam+G4;
# Codon: MG+F3X4.

# For temporal (molecular clock) analysis, add:
# extra_args = ["--date", "dates.txt", "--clock-test", "--date-CI", "95"]

4. FastTree — Fast Approximate ML

For large datasets (>1000 sequences) where IQ-TREE is too slow:

def run_fasttree(aligned_fasta: str, output_tree: str,
                  sequence_type: str = "nt", model: str = "gtr",
                  n_threads: int = 4) -> str:
    """
    Build a fast approximate ML tree with FastTree.

    Args:
        sequence_type: 'nt' for nucleotide or 'aa' for amino acid
        model: For nt: 'gtr' (recommended) or 'jc'; for aa: 'lg', 'wag', 'jtt'
    """
    if sequence_type == "nt":
        cmd = ["FastTree", "-nt", "-gtr"]
    else:
        cmd = ["FastTree", f"-{model}"]

    cmd += [aligned_fasta]

    with open(output_tree, 'w') as out:
        result = subprocess.run(cmd, stdout=out, stderr=subprocess.PIPE, text=True)

    if result.returncode != 0:
        raise RuntimeError(f"FastTree failed:\n{result.stderr}")

    print(f"FastTree: Tree saved to {output_tree}")
    return output_tree

5. Tree Analysis and Visualization with ETE3

from ete3 import Tree, TreeStyle, NodeStyle, TextFace, PhyloTree
import matplotlib.pyplot as plt

def load_tree(tree_file: str) -> Tree:
    """Load a Newick tree file."""
    t = Tree(tree_file)
    print(f"Tree: {len(t)} leaves, {len(list(t.traverse()))} nodes")
    return t

def basic_tree_stats(t: Tree) -> dict:
    """Compute basic tree statistics."""
    leaves = t.get_leaves()
    distances = [t.get_distance(l1, l2) for l1 in leaves[:min(50, len(leaves))]
                 for l2 in leaves[:min(50, len(leaves))] if l1 != l2]

    stats = {
        "n_leaves": len(leaves),
        "n_internal_nodes": len(t) - len(leaves),
        "total_branch_length": sum(n.dist for n in t.traverse()),
        "max_leaf_distance": max(distances) if distances else 0,
        "mean_leaf_distance": sum(distances)/len(distances) if distances else 0,
    }
    return stats

def find_mrca(t: Tree, leaf_names: list) -> Tree:
    """Find the most recent common ancestor of a set of leaves."""
    return t.get_common_ancestor(*leaf_names)

def visualize_tree(t: Tree, output_file: str = "tree.png",
                    show_branch_support: bool = True,
                    color_groups: dict = None,
                    width: int = 800) -> None:
    """
    Render phylogenetic tree to image.

    Args:
        t: ETE3 Tree object
        color_groups: Dict mapping leaf_name → color (for coloring taxa)
        show_branch_support: Show bootstrap values
    """
    ts = TreeStyle()
    ts.show_leaf_name = True
    ts.show_branch_support = show_branch_support
    ts.mode = "r"  # 'r' = rectangular, 'c' = circular

    if color_groups:
        for node in t.traverse():
            if node.is_leaf() and node.name in color_groups:
                nstyle = NodeStyle()
                nstyle["fgcolor"] = color_groups[node.name]
                nstyle["size"] = 8
                node.set_style(nstyle)

    t.render(output_file, tree_style=ts, w=width, units="px")
    print(f"Tree saved to: {output_file}")

def midpoint_root(t: Tree) -> Tree:
    """Root tree at midpoint (use when outgroup unknown)."""
    t.set_outgroup(t.get_midpoint_outgroup())
    return t

def prune_tree(t: Tree, keep_leaves: list) -> Tree:
    """Prune tree to keep only specified leaves."""
    t.prune(keep_leaves, preserve_branch_length=True)
    return t

6. Complete Analysis Script

import subprocess, os
from ete3 import Tree

def full_phylogenetic_analysis(
    input_fasta: str,
    output_dir: str = "phylo_results",
    sequence_type: str = "nt",
    n_threads: int = 4,
    bootstrap: int = 1000,
    use_fasttree: bool = False
) -> dict:
    """
    Complete phylogenetic pipeline: align → trim → tree → visualize.

    Args:
        input_fasta: Unaligned FASTA
        sequence_type: 'nt' (nucleotide) or 'aa' (amino acid/protein)
        use_fasttree: Use FastTree instead of IQ-TREE (faster for large datasets)
    """
    os.makedirs(output_dir, exist_ok=True)
    prefix = os.path.join(output_dir, "phylo")

    print("=" * 50)
    print("Step 1: Multiple Sequence Alignment (MAFFT)")
    aligned = run_mafft(input_fasta, f"{prefix}_aligned.fasta",
                         method="auto", n_threads=n_threads)

    print("\nStep 2: Tree Inference")
    if use_fasttree:
        tree_file = run_fasttree(
            aligned, f"{prefix}.tree",
            sequence_type=sequence_type,
            model="gtr" if sequence_type == "nt" else "lg"
        )
    else:
        # -m TEST auto-detects the alphabet (nt vs aa) and selects the best model.
        iqtree_files = run_iqtree(
            aligned, prefix,
            model="TEST",
            bootstrap=bootstrap,
            n_threads=n_threads
        )
        tree_file = iqtree_files["tree"]

    print("\nStep 3: Tree Analysis")
    t = Tree(tree_file)
    t = midpoint_root(t)

    stats = basic_tree_stats(t)
    print(f"Tree statistics: {stats}")

    print("\nStep 4: Visualization")
    visualize_tree(t, f"{prefix}_tree.png", show_branch_support=True)

    # Save rooted tree
    rooted_tree_file = f"{prefix}_rooted.nwk"
    t.write(format=1, outfile=rooted_tree_file)

    results = {
        "aligned_fasta": aligned,
        "tree_file": tree_file,
        "rooted_tree": rooted_tree_file,
        "visualization": f"{prefix}_tree.png",
        "stats": stats
    }

    print("\n" + "=" * 50)
    print("Phylogenetic analysis complete!")
    print(f"Results in: {output_dir}/")
    return results

IQ-TREE Model Guide

DNA Models

Model Description Use case
GTR+G4 General Time Reversible + Gamma Most flexible DNA model
HKY+G4 Hasegawa-Kishino-Yano + Gamma Two-rate model (common)
TrN+G4 Tamura-Nei Unequal transitions
JC Jukes-Cantor Simplest; all rates equal

Protein Models

Model Description Use case
LG+G4 Le-Gascuel + Gamma Best average protein model
WAG+G4 Whelan-Goldman Widely used
JTT+G4 Jones-Taylor-Thornton Classical model
Q.pfam+G4 Pfam-trained (QMaker) General protein families
Q.bird+G4 Bird clade-specific (QMaker) Bird proteins; siblings: Q.mammal, Q.insect, Q.yeast, Q.plant

Tip: IQ-TREE selects the model automatically by default (-m MFP); pass an explicit model only when you need to fix it.

Best Practices

  • Alignment quality first: Poor alignment → unreliable trees; check alignment manually
  • Use linsi for small (<200 seq), fftns or auto for large alignments
  • Model selection: ModelFinder Plus is IQ-TREE's default — just omit -m (or pass -m MFP). -m TEST restricts the search to jModelTest/ProtTest-style models, so use it only to match those tools
  • Bootstrap: Use ≥1000 ultrafast bootstraps (-B 1000) for branch support
  • Root the tree: Unrooted trees can be misleading; use outgroup or midpoint rooting
  • FastTree for >5000 sequences: IQ-TREE becomes slow; FastTree is 10–100× faster
  • Trim long alignments: TrimAl removes unreliable columns; improves tree accuracy
  • Check for recombination in viral/bacterial sequences before building trees (RDP4, GARD)

Additional Resources

Files (alterlab-academic-skills)
  • evals
    • evals.json 4.7 KB
      {
        "skill": "alterlab-phylogenetics",
        "evals": [
          {
            "id": "full-ml-tree-from-fasta",
            "prompt": "I have a FASTA of about 150 unaligned protein sequences from a gene family. Align them, trim the bad columns, infer a maximum-likelihood tree with automatic model selection and 1000 bootstraps, then root it and give me a figure.",
            "expected_output": "Invokes alterlab-phylogenetics: runs MAFFT (linsi/einsi for ~150 seqs) to align, TrimAl to trim, IQ-TREE 2 with -m TEST for model selection and -B 1000 ultrafast bootstraps, then midpoint-roots and visualizes with ETE3. Full align -> trim -> ML tree -> visualize pipeline.",
            "assertions": [
              { "type": "should_trigger", "value": true },
              { "type": "output_contains", "value": "IQ-TREE" },
              { "type": "behavior", "value": "Sequences MAFFT alignment, optional TrimAl, then IQ-TREE 2 with model selection (-m TEST) and -B 1000 bootstraps." }
            ]
          },
          {
            "id": "fasttree-large-dataset",
            "prompt": "I've got an alignment of around 8000 nucleotide sequences and IQ-TREE is taking forever. What's the fast way to get an approximate ML tree out of this?",
            "expected_output": "Invokes alterlab-phylogenetics: recommends FastTree for the large dataset (FastTree -nt -gtr for nucleotides), noting it is 10-100x faster than IQ-TREE for thousands of sequences, as the appropriate fast approximate-ML choice.",
            "assertions": [
              { "type": "should_trigger", "value": true },
              { "type": "output_contains", "value": "FastTree" },
              { "type": "behavior", "value": "Recommends FastTree (with -nt -gtr) for the large alignment as the fast approximate-ML alternative to IQ-TREE." }
            ]
          },
          {
            "id": "mafft-method-selection",
            "prompt": "I'm aligning around 60 closely related sequences and accuracy matters more than speed. Which MAFFT mode should I use and how do I run it?",
            "expected_output": "Invokes alterlab-phylogenetics: recommends an accurate MAFFT mode for the small set (linsi via --localpair --maxiterate 1000, or einsi via --genafpair) since accuracy is preferred for <200 sequences, and shows the run command.",
            "assertions": [
              { "type": "should_trigger", "value": true },
              { "type": "output_contains", "value": "MAFFT" },
              { "type": "behavior", "value": "Selects an accurate MAFFT mode (linsi/einsi) for the small sequence set per the method-selection guidance." }
            ]
          },
          {
            "id": "viral-molecular-clock-recombination",
            "prompt": "I'm doing phylodynamics on a set of dated SARS-CoV-2 genomes and want to estimate divergence dates. Anything I should check before building the tree, and how do I add the temporal calibration?",
            "expected_output": "Invokes alterlab-phylogenetics: warns to check for recombination first (RDP4/GARD) in viral sequences, then builds the tree and adds molecular-clock / temporal calibration via IQ-TREE 2 --date with dates.txt (and --date-CI). Phylodynamics/molecular-clock workflow.",
            "assertions": [
              { "type": "should_trigger", "value": true },
              { "type": "behavior", "value": "Advises a recombination check (RDP4/GARD) before tree-building and uses IQ-TREE --date for temporal/molecular-clock calibration." }
            ]
          },
          {
            "id": "near-miss-etetoolkit",
            "prompt": "I already have a finished Newick tree. I just want to prune it to a subset of taxa keeping branch lengths, detect duplication versus speciation events on the gene tree, and compute the Robinson-Foulds distance to a second tree.",
            "expected_output": "Should NOT trigger alterlab-phylogenetics. Manipulating an existing tree (prune, duplication/speciation event detection via get_descendant_evol_events, robinson_foulds comparison) is alterlab-etetoolkit territory. phylogenetics is the upstream align-and-infer pipeline and defers pure tree manipulation/comparison to the ETE Toolkit skill.",
            "assertions": [
              { "type": "should_not_trigger", "value": true },
              { "type": "output_contains", "value": "etetoolkit" }
            ]
          },
          {
            "id": "near-miss-biopython",
            "prompt": "I just need to parse a multi-record FASTA file, reverse-complement each sequence, translate it to protein, and write the results back out to a new FASTA.",
            "expected_output": "Should NOT trigger alterlab-phylogenetics. Basic sequence parsing, reverse-complement, translation, and FASTA I/O is general sequence handling for alterlab-biopython, with no alignment or tree inference involved. phylogenetics is specifically for reconstructing evolutionary trees.",
            "assertions": [
              { "type": "should_not_trigger", "value": true },
              { "type": "output_contains", "value": "biopython" }
            ]
          }
        ]
      }
      
  • references
    • iqtree_inference.md 5.5 KB
      # IQ-TREE Phylogenetic Inference Reference
      
      > Commands below call `iqtree`. bioconda's `iqtree` package is IQ-TREE 3 (3.1.x) and installs
      > `iqtree` and `iqtree3`; the older 2.x package installed `iqtree2`. The options here are the
      > same on both.
      
      ## Basic Command Syntax
      
      ```bash
      iqtree -s alignment.fasta --prefix output -B 1000 -T AUTO --redo   # -m MFP is the default
      ```
      
      ## Key Parameters
      
      | Flag | Description | Default |
      |------|-------------|---------|
      | `-s` | Input alignment file | Required |
      | `--prefix` | Output file prefix | alignment name |
      | `-m` | Substitution model (or TEST) | GTR+G |
      | `-B` | Ultrafast bootstrap replicates | Off |
      | `-b` | Standard bootstrap replicates (slow) | Off |
      | `-T` | Number of threads (or AUTO) | 1 |
      | `-o` | Outgroup taxa name(s) | None (unrooted) |
      | `--redo` | Overwrite existing results | Off |
      | `-alrt` | SH-aLRT test replicates | Off |
      
      ## Model Selection
      
      ```bash
      # Full model testing (automatically selects best model)
      iqtree -s alignment.fasta -m TEST --prefix test_run -B 1000 -T 4
      
      # Specify model explicitly
      iqtree -s alignment.fasta -m GTR+G4 --prefix gtr_run -B 1000
      
      # Protein sequences
      iqtree -s protein.fasta -m TEST --prefix prot_tree -B 1000
      
      # Codon-based analysis
      iqtree -s codon.fasta -m GY --prefix codon_tree -B 1000
      ```
      
      ## Bootstrapping Methods
      
      ### Ultrafast Bootstrap (UFBoot, recommended)
      ```bash
      iqtree -s alignment.fasta -B 1000  # 1000 replicates
      # Values ≥95 are reliable
      # ~10× faster than standard bootstrap
      ```
      
      ### Standard Bootstrap
      ```bash
      iqtree -s alignment.fasta -b 100  # 100 replicates (very slow)
      ```
      
      ### SH-aLRT Test (fast alternative)
      ```bash
      iqtree -s alignment.fasta -alrt 1000 -B 1000  # Both SH-aLRT and UFBoot
      # SH-aLRT ≥80 AND UFBoot ≥95 = well-supported branch
      ```
      
      ## Branch Support Interpretation
      
      | Bootstrap Value | Interpretation |
      |----------------|----------------|
      | ≥ 95 | Well-supported (strongly supported) |
      | 70–94 | Moderately supported |
      | 50–69 | Weakly supported |
      | < 50 | Unreliable (not supported) |
      
      ## Output Files
      
      | File | Description |
      |------|-------------|
      | `{prefix}.treefile` | Best ML tree in Newick format |
      | `{prefix}.iqtree` | Full analysis report |
      | `{prefix}.log` | Computation log |
      | `{prefix}.contree` | Consensus tree from bootstrap |
      | `{prefix}.splits.nex` | Network splits |
      | `{prefix}.bionj` | BioNJ starting tree |
      | `{prefix}.model.gz` | Saved model parameters |
      
      ## Advanced Analyses
      
      ### Molecular Clock (Dating)
      
      ```bash
      # Temporal analysis with sampling dates
      iqtree -s alignment.fasta -m GTR+G \
              --date dates.tsv \           # Tab-separated: taxon_name  YYYY-MM-DD
              --clock-test \               # Test for clock-like evolution
              --date-CI 95 \              # 95% CI for node dates
              --prefix dated_tree
      ```
      
      ### Concordance Factors
      
      ```bash
      # Gene concordance factor (gCF) - requires multiple gene alignments
      iqtree --gcf gene_trees.nwk \
              --tree main_tree.treefile \
              --cf-verbose \
              --prefix cf_analysis
      ```
      
      ### Ancestral Sequence Reconstruction
      
      ```bash
      iqtree -s alignment.fasta -m LG+G4 \
              -asr \                      # Marginal ancestral state reconstruction
              --prefix anc_tree
      # Output: {prefix}.state (ancestral sequences per node)
      ```
      
      ### Partition Model (Multi-Gene)
      
      ```bash
      # Create partition file (partitions.txt):
      # DNA, gene1 = 1-500
      # DNA, gene2 = 501-1000
      
      iqtree -s concat_alignment.fasta \
              -p partitions.txt \
              -m TEST \
              -B 1000 \
              --prefix partition_tree
      ```
      
      ## IQ-TREE Log Parsing
      
      ```python
      def parse_iqtree_log(log_file: str) -> dict:
          """Extract key results from IQ-TREE log file."""
          results = {}
          with open(log_file) as f:
              for line in f:
                  if "Best-fit model" in line:
                      results["best_model"] = line.split(":")[1].strip()
                  elif "Log-likelihood of the tree:" in line:
                      results["log_likelihood"] = float(line.split(":")[1].strip())
                  elif "Number of free parameters" in line:
                      results["free_params"] = int(line.split(":")[1].strip())
                  elif "Akaike information criterion" in line:
                      results["AIC"] = float(line.split(":")[1].strip())
                  elif "Bayesian information criterion" in line:
                      results["BIC"] = float(line.split(":")[1].strip())
                  elif "Total CPU time used" in line:
                      results["cpu_time"] = line.split(":")[1].strip()
          return results
      
      # Example:
      # results = parse_iqtree_log("output.log")
      # print(f"Best model: {results['best_model']}")
      # print(f"Log-likelihood: {results['log_likelihood']:.2f}")
      ```
      
      ## Common Issues and Solutions
      
      | Issue | Likely Cause | Solution |
      |-------|-------------|---------|
      | All bootstrap values = 0 | Too few taxa | Need ≥4 taxa for bootstrap |
      | Very long branches | Alignment artifacts | Re-trim alignment; check for outliers |
      | Memory error | Too many sequences | Use FastTree; or reduce `-T` to 1 |
      | Poor model fit | Wrong alphabet | Check nucleotide vs. protein specification |
      | Identical sequences | Duplicate sequences | Remove duplicates before alignment |
      
      ## MAFFT Alignment Guide
      
      ```bash
      # Accurate (< 200 sequences)
      mafft --localpair --maxiterate 1000 input.fasta > aligned.fasta
      
      # Medium (200-1000 sequences)
      mafft --auto input.fasta > aligned.fasta
      
      # Fast (> 1000 sequences)
      mafft --fftns input.fasta > aligned.fasta
      
      # Very large (> 10000 sequences)
      mafft --retree 1 input.fasta > aligned.fasta
      
      # Using multiple threads
      mafft --thread 8 --auto input.fasta > aligned.fasta
      ```
      
  • scripts
    • phylogenetic_analysis.py 9.9 KB
      """
      Phylogenetic Analysis Pipeline
      ===============================
      Complete workflow: MAFFT alignment → IQ-TREE 3 tree → ETE3 visualization.
      
      Requirements (CLI binaries are NOT on PyPI):
          conda install -c bioconda mafft iqtree fasttree   # or Homebrew: brew install mafft fasttree; brew install brewsci/bio/iqtree
          uv pip install "ete3==3.1.3"                       # visualization only (optional)
      
      Usage:
          python phylogenetic_analysis.py sequences.fasta --type nt --threads 4
          python phylogenetic_analysis.py proteins.fasta --type aa --fasttree
      """
      
      import argparse
      import os
      import subprocess
      import sys
      from pathlib import Path
      from shutil import which
      
      # bioconda's `iqtree` package is IQ-TREE 3 and ships `iqtree` + `iqtree3`; the 2.x
      # package shipped `iqtree2`. Resolve whichever is on PATH instead of hardcoding one.
      IQTREE_CANDIDATES = ("iqtree", "iqtree3", "iqtree2")
      
      
      def find_iqtree():
          """Return the first IQ-TREE executable on PATH, or None."""
          for name in IQTREE_CANDIDATES:
              if which(name) is not None:
                  return name
          return None
      
      
      def check_dependencies(use_fasttree: bool = False):
          """Check that the required CLI tools are on PATH (binaries are not pip-installable)."""
          tools = {"mafft": "conda install -c bioconda mafft  (or: brew install mafft)"}
          if use_fasttree:
              tools["FastTree"] = "conda install -c bioconda fasttree  (or: brew install fasttree)"
      
          missing = [f"  {tool}: {hint}" for tool, hint in tools.items() if which(tool) is None]
          if not use_fasttree and find_iqtree() is None:
              missing.append(
                  "  iqtree (or iqtree3/iqtree2): "
                  "conda install -c bioconda iqtree  (or: brew install brewsci/bio/iqtree)"
              )
          if missing:
              print("Missing dependencies:")
              for m in missing:
                  print(m)
              sys.exit(1)
          print("All dependencies found.")
      
      
      def count_sequences(fasta_file: str) -> int:
          """Count sequences in a FASTA file."""
          with open(fasta_file) as f:
              return sum(1 for line in f if line.startswith('>'))
      
      
      def run_mafft(input_fasta: str, output_fasta: str, n_threads: int = 4,
                     method: str = "auto") -> str:
          """Run MAFFT multiple sequence alignment."""
          n_seqs = count_sequences(input_fasta)
          print(f"MAFFT: Aligning {n_seqs} sequences...")
      
          # Auto-select method based on dataset size
          if method == "auto":
              if n_seqs <= 200:
                  cmd = ["mafft", "--localpair", "--maxiterate", "1000",
                         "--thread", str(n_threads), "--inputorder", input_fasta]
              elif n_seqs <= 1000:
                  cmd = ["mafft", "--auto", "--thread", str(n_threads),
                         "--inputorder", input_fasta]
              else:
                  cmd = ["mafft", "--fftns", "--thread", str(n_threads),
                         "--inputorder", input_fasta]
          else:
              cmd = ["mafft", f"--{method}", "--thread", str(n_threads),
                     "--inputorder", input_fasta]
      
          with open(output_fasta, 'w') as out:
              result = subprocess.run(cmd, stdout=out, stderr=subprocess.PIPE, text=True)
      
          if result.returncode != 0:
              raise RuntimeError(f"MAFFT failed:\n{result.stderr[:500]}")
      
          print(f"  Alignment complete → {output_fasta}")
          return output_fasta
      
      
      def run_iqtree(aligned_fasta: str, prefix: str, seq_type: str = "nt",
                      bootstrap: int = 1000, n_threads: int = 4,
                      outgroup: str = None) -> str:
          """Run IQ-TREE phylogenetic inference (IQ-TREE 2 or 3, whichever is installed)."""
          exe = find_iqtree()
          if exe is None:
              raise RuntimeError(
                  "No IQ-TREE executable found on PATH "
                  f"(looked for {', '.join(IQTREE_CANDIDATES)}). "
                  "Install with: conda install -c bioconda iqtree"
              )
          print(f"{exe}: Building maximum likelihood tree...")
      
          cmd = [
              exe,
              "-s", aligned_fasta,
              "--prefix", prefix,
              "-m", "MFP",            # ModelFinder Plus (IQ-TREE's default model search)
              "-B", str(bootstrap),   # Ultrafast bootstrap
              "-T", str(n_threads),
              "--redo",
              "-alrt", "1000",        # SH-aLRT test
          ]
      
          if outgroup:
              cmd += ["-o", outgroup]
      
          result = subprocess.run(cmd, capture_output=True, text=True)
      
          if result.returncode != 0:
              raise RuntimeError(f"IQ-TREE failed:\n{result.stderr[:500]}")
      
          tree_file = f"{prefix}.treefile"
      
          # Extract best model from log
          log_file = f"{prefix}.log"
          if os.path.exists(log_file):
              with open(log_file) as f:
                  for line in f:
                      if "Best-fit model" in line:
                          print(f"  {line.strip()}")
      
          print(f"  Tree saved → {tree_file}")
          return tree_file
      
      
      def run_fasttree(aligned_fasta: str, output_tree: str, seq_type: str = "nt") -> str:
          """Run FastTree (faster alternative for large datasets)."""
          print("FastTree: Building approximate ML tree (faster)...")
      
          if seq_type == "nt":
              cmd = ["FastTree", "-nt", "-gtr", "-gamma", aligned_fasta]
          else:
              cmd = ["FastTree", "-lg", "-gamma", aligned_fasta]
      
          with open(output_tree, 'w') as out:
              result = subprocess.run(cmd, stdout=out, stderr=subprocess.PIPE, text=True)
      
          if result.returncode != 0:
              raise RuntimeError(f"FastTree failed:\n{result.stderr[:500]}")
      
          print(f"  Tree saved → {output_tree}")
          return output_tree
      
      
      def visualize_tree(tree_file: str, output_png: str, outgroup: str = None) -> None:
          """Visualize the phylogenetic tree with ETE3."""
          try:
              from ete3 import Tree, TreeStyle
          except ImportError:
              print("ETE3 not installed. Skipping visualization.")
              print('  Install: uv pip install "ete3==3.1.3"')
              return
      
          t = Tree(tree_file)
      
          # Root the tree
          if outgroup and outgroup in [leaf.name for leaf in t.get_leaves()]:
              t.set_outgroup(outgroup)
              print(f"  Rooted at outgroup: {outgroup}")
          else:
              # Midpoint rooting
              t.set_outgroup(t.get_midpoint_outgroup())
              print("  Applied midpoint rooting")
      
          # Style
          ts = TreeStyle()
          ts.show_leaf_name = True
          ts.show_branch_support = True
          ts.mode = "r"  # rectangular
      
          try:
              t.render(output_png, tree_style=ts, w=800, units="px")
              print(f"  Visualization saved → {output_png}")
          except Exception as e:
              print(f"  Visualization failed (display issue?): {e}")
              # Save tree in Newick format as fallback
              rooted_nwk = output_png.replace(".png", "_rooted.nwk")
              t.write(format=1, outfile=rooted_nwk)
              print(f"  Rooted tree saved → {rooted_nwk}")
      
      
      def tree_summary(tree_file: str) -> dict:
          """Print summary statistics for the tree."""
          try:
              from ete3 import Tree
              t = Tree(tree_file)
              t.set_outgroup(t.get_midpoint_outgroup())
      
              leaves = t.get_leaves()
              branch_lengths = [n.dist for n in t.traverse() if n.dist > 0]
      
              stats = {
                  "n_taxa": len(leaves),
                  "total_branch_length": sum(branch_lengths),
                  "mean_branch_length": sum(branch_lengths) / len(branch_lengths) if branch_lengths else 0,
                  "max_branch_length": max(branch_lengths) if branch_lengths else 0,
              }
      
              print("\nTree Summary:")
              for k, v in stats.items():
                  if isinstance(v, float):
                      print(f"  {k}: {v:.6f}")
                  else:
                      print(f"  {k}: {v}")
      
              return stats
          except Exception as e:
              print(f"Could not compute tree stats: {e}")
              return {}
      
      
      def main():
          parser = argparse.ArgumentParser(description="Phylogenetic analysis pipeline")
          parser.add_argument("input", help="Input FASTA file (unaligned)")
          parser.add_argument("--type", choices=["nt", "aa"], default="nt",
                              help="Sequence type: nt (nucleotide) or aa (amino acid)")
          parser.add_argument("--threads", type=int, default=4, help="Number of threads")
          parser.add_argument("--bootstrap", type=int, default=1000,
                              help="Bootstrap replicates for IQ-TREE")
          parser.add_argument("--fasttree", action="store_true",
                              help="Use FastTree instead of IQ-TREE (faster, less accurate)")
          parser.add_argument("--outgroup", help="Outgroup taxon name for rooting")
          parser.add_argument("--mafft-method", default="auto",
                              choices=["auto", "linsi", "einsi", "fftnsi", "fftns"],
                              help="MAFFT alignment method")
          parser.add_argument("--output-dir", default="phylo_results",
                              help="Output directory")
      
          args = parser.parse_args()
      
          check_dependencies(use_fasttree=args.fasttree)
      
          # Setup
          os.makedirs(args.output_dir, exist_ok=True)
          prefix = os.path.join(args.output_dir, Path(args.input).stem)
      
          print("=" * 60)
          print("Phylogenetic Analysis Pipeline")
          print("=" * 60)
          print(f"Input: {args.input}")
          print(f"Sequence type: {args.type}")
          print(f"Output dir: {args.output_dir}")
      
          # Step 1: Multiple Sequence Alignment
          print("\n[Step 1/3] Multiple Sequence Alignment (MAFFT)")
          aligned = run_mafft(
              args.input,
              f"{prefix}_aligned.fasta",
              n_threads=args.threads,
              method=args.mafft_method
          )
      
          # Step 2: Tree Inference
          print("\n[Step 2/3] Tree Inference")
          if args.fasttree:
              tree_file = run_fasttree(aligned, f"{prefix}.tree", seq_type=args.type)
          else:
              tree_file = run_iqtree(
                  aligned, prefix,
                  seq_type=args.type,
                  bootstrap=args.bootstrap,
                  n_threads=args.threads,
                  outgroup=args.outgroup
              )
      
          # Step 3: Visualization
          print("\n[Step 3/3] Visualization (ETE3)")
          visualize_tree(tree_file, f"{prefix}_tree.png", outgroup=args.outgroup)
          tree_summary(tree_file)
      
          print("\n" + "=" * 60)
          print("Analysis complete!")
          print(f"Key outputs:")
          print(f"  Aligned sequences: {aligned}")
          print(f"  Tree file: {tree_file}")
          print(f"  Visualization: {prefix}_tree.png")
      
      
      if __name__ == "__main__":
          main()
      
  • SKILL.md 16.6 KB
    ---
    name: alterlab-phylogenetics
    description: Build phylogenetic trees end-to-end from raw sequences — MAFFT multiple sequence alignment, optional TrimAl trimming, IQ-TREE 3 maximum-likelihood inference with model selection and bootstraps, FastTree for large datasets, then visualize with ETE3 or FigTree. Use when reconstructing trees from sequences (FASTA) for evolutionary analysis, microbial genomics, viral phylodynamics, protein-family studies, or molecular-clock dating. For manipulating/comparing an EXISTING Newick tree (prune, root, Robinson-Foulds, duplication/speciation events) use alterlab-etetoolkit; for plain sequence parsing/translation use alterlab-biopython. Part of the AlterLab Academic Skills suite.
    license: MIT
    allowed-tools: Read Write Edit Bash(python:*) Bash(uv:*)
    compatibility: "Needs external CLI tools (MAFFT, IQ-TREE 3, FastTree; optionally TrimAl) on PATH — install via bioconda or Homebrew, NOT pip/uv. Python parts (ETE3 for visualization) run under `uv run python`. No API key or account required."
    metadata:
        skill-author: AlterLab
        version: "1.1.0"
        last_updated: "2026-09-23"
    ---
    
    # Phylogenetics
    
    ## Overview
    
    Phylogenetic analysis reconstructs the evolutionary history of biological sequences (genes, proteins, genomes) by inferring the branching pattern of descent. This skill covers the standard pipeline:
    
    1. **MAFFT** — Multiple sequence alignment
    2. **IQ-TREE 3** — Maximum likelihood tree inference with model selection
    3. **FastTree** — Fast approximate maximum likelihood (for large datasets)
    4. **ETE3** — Python library for tree manipulation and visualization
    
    **Installation:** The aligners and tree builders are compiled CLI tools (not on PyPI). Install the binaries via bioconda or Homebrew; install the Python visualization layer with uv.
    ```bash
    # CLI binaries — bioconda (cross-platform) ...
    conda install -c bioconda mafft iqtree fasttree trimal   # iqtree is now IQ-TREE 3
    # ... or Homebrew on macOS (Apple Silicon): IQ-TREE/TrimAl live in the brewsci/bio tap
    brew install mafft fasttree
    brew tap brewsci/bio && brew install brewsci/bio/iqtree brewsci/bio/trimal
    
    # Python visualization layer
    uv pip install "ete3==3.1.3"   # also needs numpy<2 and PyQt5 for rendering
    ```
    > **Binary name:** bioconda's `iqtree` package is now **IQ-TREE 3** (3.1.x) and installs the
    > executables `iqtree` and `iqtree3` — there is **no `iqtree2`** in it. Call `iqtree`, which
    > works on both the 2.x and 3.x packages; only reach for a versioned name when two releases
    > coexist on PATH. IQ-TREE 3 adds MixtureFinder, multi-tree mixture models (MAST), gene/site
    > concordance factors, and CMAPLE for very large pathogen datasets; the options below are
    > unchanged from 2.x.
    >
    > ETE3 (3.1.3) is the last ete3 release and can be fragile to install on Python ≥3.12 (pins old numpy/PyQt5). If `t.render()` fails, fall back to writing the Newick tree and viewing it in FigTree/iTOL, or use the maintained successor `ete4` (note: ete4 changed the `TreeStyle`/`render` API, so the snippets below are ete3-specific).
    
    ## When to Use This Skill
    
    Use phylogenetics when:
    
    - **Evolutionary relationships**: Which organism/gene is most closely related to my sequence?
    - **Viral phylodynamics**: Trace outbreak spread and estimate transmission dates
    - **Protein family analysis**: Infer evolutionary relationships within a gene family
    - **Horizontal gene transfer detection**: Identify genes with discordant species/gene trees
    - **Ancestral sequence reconstruction**: Infer ancestral protein sequences
    - **Molecular clock analysis**: Estimate divergence dates using temporal sampling
    - **GWAS companion**: Place variants in evolutionary context (e.g., SARS-CoV-2 variants)
    - **Microbiology**: Species phylogeny from 16S rRNA or core genome phylogeny
    
    ### Does NOT Trigger
    
    | Scenario | Use Instead |
    |----------|-------------|
    | Manipulating or comparing an **existing** Newick tree (prune, root, Robinson-Foulds, duplication/speciation) | `alterlab-etetoolkit` |
    | Plain sequence parsing, translation, or format conversion | `alterlab-biopython` |
    | Homology search to *find* the sequences before aligning them | `alterlab-blast` |
    | Diversity/ordination on a community feature table (UniFrac, PCoA, PERMANOVA) | `alterlab-scikit-bio` |
    | Running the 16S amplicon pipeline that produces the ASVs and tree | `alterlab-qiime2-amplicon` |
    
    ## Standard Workflow
    
    ### 1. Multiple Sequence Alignment with MAFFT
    
    ```python
    import subprocess
    import os
    
    def run_mafft(input_fasta: str, output_fasta: str, method: str = "auto",
                   n_threads: int = 4) -> str:
        """
        Align sequences with MAFFT.
    
        Args:
            input_fasta: Path to unaligned FASTA file
            output_fasta: Path for aligned output
            method: 'auto' (auto-select), 'einsi' (accurate), 'linsi' (accurate, slow),
                    'fftnsi' (medium), 'fftns' (fast), 'retree2' (fast)
            n_threads: Number of CPU threads
    
        Returns:
            Path to aligned FASTA file
        """
        methods = {
            "auto": ["mafft", "--auto"],
            "einsi": ["mafft", "--genafpair", "--maxiterate", "1000"],
            "linsi": ["mafft", "--localpair", "--maxiterate", "1000"],
            "fftnsi": ["mafft", "--retree", "2", "--maxiterate", "2"],
            "fftns": ["mafft", "--retree", "2", "--maxiterate", "0"],
            "retree2": ["mafft", "--retree", "2"],
        }
    
        cmd = methods.get(method, methods["auto"])
        cmd += ["--thread", str(n_threads), "--inputorder", input_fasta]
    
        with open(output_fasta, 'w') as out:
            result = subprocess.run(cmd, stdout=out, stderr=subprocess.PIPE, text=True)
    
        if result.returncode != 0:
            raise RuntimeError(f"MAFFT failed:\n{result.stderr}")
    
        # Count aligned sequences
        with open(output_fasta) as f:
            n_seqs = sum(1 for line in f if line.startswith('>'))
        print(f"MAFFT: aligned {n_seqs} sequences → {output_fasta}")
    
        return output_fasta
    
    # MAFFT method selection guide:
    # Few sequences (<200), accurate: linsi or einsi
    # Many sequences (<1000), moderate: fftnsi
    # Large datasets (>1000): fftns or auto
    # Ultra-fast (>10000): mafft --retree 1
    ```
    
    ### 2. Trim Alignment (Optional but Recommended)
    
    ```python
    def trim_alignment_trimal(aligned_fasta: str, output_fasta: str,
                                method: str = "automated1") -> str:
        """
        Trim poorly aligned columns with TrimAl.
    
        Methods:
        - 'automated1': Automatic heuristic (recommended)
        - 'gappyout': Remove gappy columns
        - 'strict': Strict gap threshold
        """
        cmd = ["trimal", f"-{method}", "-in", aligned_fasta, "-out", output_fasta, "-fasta"]
        result = subprocess.run(cmd, capture_output=True, text=True)
        if result.returncode != 0:
            print(f"TrimAl warning: {result.stderr}")
            # Fall back to using the untrimmed alignment
            import shutil
            shutil.copy(aligned_fasta, output_fasta)
        return output_fasta
    ```
    
    ### 3. IQ-TREE 3 — Maximum Likelihood Tree
    
    ```python
    def run_iqtree(aligned_fasta: str, output_prefix: str,
                    model: str = "TEST", bootstrap: int = 1000,
                    n_threads: int = 4, extra_args: list = None) -> dict:
        """
        Build a maximum likelihood tree with IQ-TREE 3.
    
        Args:
            aligned_fasta: Aligned FASTA file
            output_prefix: Prefix for output files
            model: 'TEST' for automatic model selection, or specify (e.g., 'GTR+G' for DNA,
                   'LG+G4' for proteins, 'JTT+G' for proteins)
            bootstrap: Number of ultrafast bootstrap replicates (1000 recommended)
            n_threads: Number of threads ('AUTO' to auto-detect)
            extra_args: Additional IQ-TREE arguments
    
        Returns:
            Dict with paths to output files
        """
        cmd = [
            "iqtree",
            "-s", aligned_fasta,
            "--prefix", output_prefix,
            "-m", model,
            "-B", str(bootstrap),   # Ultrafast bootstrap
            "-T", str(n_threads),
            "--redo"                # Overwrite existing results
        ]
    
        if extra_args:
            cmd.extend(extra_args)
    
        result = subprocess.run(cmd, capture_output=True, text=True)
    
        if result.returncode != 0:
            raise RuntimeError(f"IQ-TREE failed:\n{result.stderr}")
    
        # Print model selection result
        log_file = f"{output_prefix}.log"
        if os.path.exists(log_file):
            with open(log_file) as f:
                for line in f:
                    if "Best-fit model" in line:
                        print(f"IQ-TREE: {line.strip()}")
    
        output_files = {
            "tree": f"{output_prefix}.treefile",
            "log": f"{output_prefix}.log",
            "iqtree": f"{output_prefix}.iqtree",  # Full report
            "model": f"{output_prefix}.model.gz",
        }
    
        print(f"IQ-TREE: Tree saved to {output_files['tree']}")
        return output_files
    
    # Model selection: ModelFinder Plus (-m MFP) is the DEFAULT — omitting -m runs it.
    # -m TEST is the narrower jModelTest/ProtTest-style search (no FreeRate models);
    # use it only when you need to match those tools.
    # Typical winners — DNA: GTR+F+I+G4, HKY+F+G4; Protein: LG+F+G4, Q.pfam+G4;
    # Codon: MG+F3X4.
    
    # For temporal (molecular clock) analysis, add:
    # extra_args = ["--date", "dates.txt", "--clock-test", "--date-CI", "95"]
    ```
    
    ### 4. FastTree — Fast Approximate ML
    
    For large datasets (>1000 sequences) where IQ-TREE is too slow:
    
    ```python
    def run_fasttree(aligned_fasta: str, output_tree: str,
                      sequence_type: str = "nt", model: str = "gtr",
                      n_threads: int = 4) -> str:
        """
        Build a fast approximate ML tree with FastTree.
    
        Args:
            sequence_type: 'nt' for nucleotide or 'aa' for amino acid
            model: For nt: 'gtr' (recommended) or 'jc'; for aa: 'lg', 'wag', 'jtt'
        """
        if sequence_type == "nt":
            cmd = ["FastTree", "-nt", "-gtr"]
        else:
            cmd = ["FastTree", f"-{model}"]
    
        cmd += [aligned_fasta]
    
        with open(output_tree, 'w') as out:
            result = subprocess.run(cmd, stdout=out, stderr=subprocess.PIPE, text=True)
    
        if result.returncode != 0:
            raise RuntimeError(f"FastTree failed:\n{result.stderr}")
    
        print(f"FastTree: Tree saved to {output_tree}")
        return output_tree
    ```
    
    ### 5. Tree Analysis and Visualization with ETE3
    
    ```python
    from ete3 import Tree, TreeStyle, NodeStyle, TextFace, PhyloTree
    import matplotlib.pyplot as plt
    
    def load_tree(tree_file: str) -> Tree:
        """Load a Newick tree file."""
        t = Tree(tree_file)
        print(f"Tree: {len(t)} leaves, {len(list(t.traverse()))} nodes")
        return t
    
    def basic_tree_stats(t: Tree) -> dict:
        """Compute basic tree statistics."""
        leaves = t.get_leaves()
        distances = [t.get_distance(l1, l2) for l1 in leaves[:min(50, len(leaves))]
                     for l2 in leaves[:min(50, len(leaves))] if l1 != l2]
    
        stats = {
            "n_leaves": len(leaves),
            "n_internal_nodes": len(t) - len(leaves),
            "total_branch_length": sum(n.dist for n in t.traverse()),
            "max_leaf_distance": max(distances) if distances else 0,
            "mean_leaf_distance": sum(distances)/len(distances) if distances else 0,
        }
        return stats
    
    def find_mrca(t: Tree, leaf_names: list) -> Tree:
        """Find the most recent common ancestor of a set of leaves."""
        return t.get_common_ancestor(*leaf_names)
    
    def visualize_tree(t: Tree, output_file: str = "tree.png",
                        show_branch_support: bool = True,
                        color_groups: dict = None,
                        width: int = 800) -> None:
        """
        Render phylogenetic tree to image.
    
        Args:
            t: ETE3 Tree object
            color_groups: Dict mapping leaf_name → color (for coloring taxa)
            show_branch_support: Show bootstrap values
        """
        ts = TreeStyle()
        ts.show_leaf_name = True
        ts.show_branch_support = show_branch_support
        ts.mode = "r"  # 'r' = rectangular, 'c' = circular
    
        if color_groups:
            for node in t.traverse():
                if node.is_leaf() and node.name in color_groups:
                    nstyle = NodeStyle()
                    nstyle["fgcolor"] = color_groups[node.name]
                    nstyle["size"] = 8
                    node.set_style(nstyle)
    
        t.render(output_file, tree_style=ts, w=width, units="px")
        print(f"Tree saved to: {output_file}")
    
    def midpoint_root(t: Tree) -> Tree:
        """Root tree at midpoint (use when outgroup unknown)."""
        t.set_outgroup(t.get_midpoint_outgroup())
        return t
    
    def prune_tree(t: Tree, keep_leaves: list) -> Tree:
        """Prune tree to keep only specified leaves."""
        t.prune(keep_leaves, preserve_branch_length=True)
        return t
    ```
    
    ### 6. Complete Analysis Script
    
    ```python
    import subprocess, os
    from ete3 import Tree
    
    def full_phylogenetic_analysis(
        input_fasta: str,
        output_dir: str = "phylo_results",
        sequence_type: str = "nt",
        n_threads: int = 4,
        bootstrap: int = 1000,
        use_fasttree: bool = False
    ) -> dict:
        """
        Complete phylogenetic pipeline: align → trim → tree → visualize.
    
        Args:
            input_fasta: Unaligned FASTA
            sequence_type: 'nt' (nucleotide) or 'aa' (amino acid/protein)
            use_fasttree: Use FastTree instead of IQ-TREE (faster for large datasets)
        """
        os.makedirs(output_dir, exist_ok=True)
        prefix = os.path.join(output_dir, "phylo")
    
        print("=" * 50)
        print("Step 1: Multiple Sequence Alignment (MAFFT)")
        aligned = run_mafft(input_fasta, f"{prefix}_aligned.fasta",
                             method="auto", n_threads=n_threads)
    
        print("\nStep 2: Tree Inference")
        if use_fasttree:
            tree_file = run_fasttree(
                aligned, f"{prefix}.tree",
                sequence_type=sequence_type,
                model="gtr" if sequence_type == "nt" else "lg"
            )
        else:
            # -m TEST auto-detects the alphabet (nt vs aa) and selects the best model.
            iqtree_files = run_iqtree(
                aligned, prefix,
                model="TEST",
                bootstrap=bootstrap,
                n_threads=n_threads
            )
            tree_file = iqtree_files["tree"]
    
        print("\nStep 3: Tree Analysis")
        t = Tree(tree_file)
        t = midpoint_root(t)
    
        stats = basic_tree_stats(t)
        print(f"Tree statistics: {stats}")
    
        print("\nStep 4: Visualization")
        visualize_tree(t, f"{prefix}_tree.png", show_branch_support=True)
    
        # Save rooted tree
        rooted_tree_file = f"{prefix}_rooted.nwk"
        t.write(format=1, outfile=rooted_tree_file)
    
        results = {
            "aligned_fasta": aligned,
            "tree_file": tree_file,
            "rooted_tree": rooted_tree_file,
            "visualization": f"{prefix}_tree.png",
            "stats": stats
        }
    
        print("\n" + "=" * 50)
        print("Phylogenetic analysis complete!")
        print(f"Results in: {output_dir}/")
        return results
    ```
    
    ## IQ-TREE Model Guide
    
    ### DNA Models
    
    | Model | Description | Use case |
    |-------|-------------|---------|
    | `GTR+G4` | General Time Reversible + Gamma | Most flexible DNA model |
    | `HKY+G4` | Hasegawa-Kishino-Yano + Gamma | Two-rate model (common) |
    | `TrN+G4` | Tamura-Nei | Unequal transitions |
    | `JC` | Jukes-Cantor | Simplest; all rates equal |
    
    ### Protein Models
    
    | Model | Description | Use case |
    |-------|-------------|---------|
    | `LG+G4` | Le-Gascuel + Gamma | Best average protein model |
    | `WAG+G4` | Whelan-Goldman | Widely used |
    | `JTT+G4` | Jones-Taylor-Thornton | Classical model |
    | `Q.pfam+G4` | Pfam-trained (QMaker) | General protein families |
    | `Q.bird+G4` | Bird clade-specific (QMaker) | Bird proteins; siblings: Q.mammal, Q.insect, Q.yeast, Q.plant |
    
    **Tip:** IQ-TREE selects the model automatically by default (`-m MFP`); pass an explicit model
    only when you need to fix it.
    
    ## Best Practices
    
    - **Alignment quality first**: Poor alignment → unreliable trees; check alignment manually
    - **Use `linsi` for small (<200 seq), `fftns` or `auto` for large alignments**
    - **Model selection**: ModelFinder Plus is IQ-TREE's default — just omit `-m` (or pass `-m MFP`).
      `-m TEST` restricts the search to jModelTest/ProtTest-style models, so use it only to match
      those tools
    - **Bootstrap**: Use ≥1000 ultrafast bootstraps (`-B 1000`) for branch support
    - **Root the tree**: Unrooted trees can be misleading; use outgroup or midpoint rooting
    - **FastTree for >5000 sequences**: IQ-TREE becomes slow; FastTree is 10–100× faster
    - **Trim long alignments**: TrimAl removes unreliable columns; improves tree accuracy
    - **Check for recombination** in viral/bacterial sequences before building trees (`RDP4`, `GARD`)
    
    ## Additional Resources
    
    - **MAFFT**: https://mafft.cbrc.jp/alignment/software/
    - **IQ-TREE 3**: http://www.iqtree.org/ | Tutorial: https://www.iqtree.org/workshop/molevol2022
    - **FastTree**: http://www.microbesonline.org/fasttree/
    - **ETE3**: http://etetoolkit.org/
    - **FigTree** (GUI visualization): https://tree.bio.ed.ac.uk/software/figtree/
    - **iTOL** (web visualization): https://itol.embl.de/
    - **MUSCLE** (alternative aligner): https://www.drive5.com/muscle/
    - **TrimAl** (alignment trimming): https://vicfero.github.io/trimal/
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related