This repository provides a Snakemake workflow for processing AnchorWave MAFs directly into per-contig site outputs. The workflow emits all-sites VCFs, variant-only VCFs, and BED masks from the alignments. Written with the aid of Codex and Claude. Note that v1.0 was a major rewrite from v0.4, and no longer uses Tassel or GATK.
If you use this please cite:
Ross-Ibarra, J. 2026. ARGprep: A pipeline to prepare pairwise whole-genome alignments for ancestral recombination graph estimation. doi: 10.5281/zenodo.19655050
If your use case is pairwise variant discovery (SNPs, large indels, inversions) rather than ARG-ready all-sites output, wgatools is a potential alternative. See WGATOOLS_COMPARISON.md for a detailed comparison of the two approaches.
Which version to use: for production runs, check out the most recent stable release (
git checkout v1.10).v1.11-beta.1is a pre-release that adds themaf_qcalignment-QC stage for testing; feedback is welcome via GitHub issues. See changelog.md for a per-version breakdown of changes.
- Conda
- The environment defined in
argprep.yml
conda env create -f argprep.yml
conda activate argprepA Docker/Singularity setup option is also available. See container setup
Create or edit a config file such as options.yaml.
The workflow requires --configfile and will fail if it is omitted.
Required keys:
maf_dir: directory containing*.mafor*.maf.gzreference_fasta: reference FASTA path
Optional path keys:
results_dir: output directory (default:results)
Optional controls (defaults shown):
max_missing_count- no default; see missingness thresholds belowmax_missing_fraction- no default; see missingness thresholds belowmask_indel_adjacent_snps: false- whentrue, mask SNPs immediately flanking an indel in any sample (see NOTES.md for exact semantics)allow_multiallelic_snps: true- retain sites with more than two allelesadd_ref: false- append a syntheticREFsample (genotype0) to both VCFsemit_argweaver_sites: false- whentrue, also emit a per-contig ARGweaver.sitesfile (see Preparing inputs for ARG inference)summary_window_bp: 100000- window size in bp for binned per-contig plots insummary.html(this does not affect the per-MAF tables)quality_bed_dir/quality_min- no default; optional per-sample assembly-quality masking (see below)
SLURM resource overrides (for the direct_maf_sites rule):
maf_mem_mb: 48000maf_time: "24:00:00"
SLURM profile keys (required when using --profile profiles/slurm):
slurm_accountslurm_partition
Advanced override:
contigs: restrict the run to specific contigs instead of using the automatic shared-contig behaviorsamples: restrict the run to specific sample basenames instead of using all*.maf/*.maf.gzfiles inmaf_dir
Contig and sample selection behavior:
- If
samplesis omitted, samples are auto-discovered from*.mafand*.maf.gzdirectly inmaf_dir(non-recursive — nested subdirectories are ignored). Pointmaf_dirat a directory containing only your input MAFs; avoid parent directories that also hold example data or other.maffiles. - If both
<sample>.mafand<sample>.maf.gzexist,<sample>.mafis used. - If
contigsis omitted, the workflow uses the intersection of contigs present in all selected MAFs. - Requested contigs are matched to reference
.faicontigs with normalization (for examplechr01can map to1when unambiguous). - Contig names are normalized before matching, so MAFs (or reference FASTAs) that differ only in casing, a leading
chr, or an assembly/genome prefix are treated as the same contig. An assembly prefix is stripped only when it precedes achrtoken, sochr2,Zm-B73v5.chr2, andZx-TIL25.chr2all resolve to contig2. Accession-style names such asNC_050096.1(where.1is a version suffix, not a prefix) are left intact. All MAFs must still be aligned to the same reference genome — normalization only reconciles cosmetic naming differences, it does not make coordinates from different references comparable. - Every explicitly configured
contigsentry must match exactly or resolve through one unambiguous normalized alias; otherwise the workflow fails and lists the unresolved names. - During automatic discovery, unmatched or ambiguous contigs are skipped with a warning, and the workflow fails if no contigs remain.
Example CLI override:
snakemake -j 8 --configfile options.yaml --config samples='["sampleA","sampleB"]' contigs='["chr1","chr2"]'Missingness thresholds:
max_missing_countis an absolute number of missing samples allowed at a retained site.max_missing_fractionis a fraction of samples allowed to be missing.- If both are set, the workflow uses the stricter threshold.
- The fraction is converted to a count with downward truncation. For example, with 10 samples,
0.15allows1missing sample. - If neither is set, the default is 0 - any site where even one sample is unaligned or missing is masked. Set one of these options explicitly if you want to retain sites with partial coverage.
- A sample counts as missing at a site if it has no alignment block covering that position, carries a gap (
-), anN, or any other non-ACGT character. To drop every site overlapped by a deletion in any sample, setmax_missing_count: 0(the default) — gaps always contribute to the missing-sample count.
Repeat masking and sequence case:
- Soft masking is ignored. Reference and query sequences are upper-cased before calling, so lowercase repeat-masked bases are called exactly like uppercase ones. A soft-masked reference (the default distribution format for most released genomes, including B73 v5) will therefore have its repeat content genotyped, not excluded.
- Hard masking is honored, because it is indistinguishable from missing data:
Nis non-ACGT and counts toward the missing-sample thresholds above. - If you want repeats excluded from ARG inference, ARGprep will not do it for you as a side effect of soft masking. Either supply a hard-masked reference, or pass repeat intervals through
quality_bed_dir/quality_min.
Adjacent-SNP masking is documented in detail in NOTES.md.
Per-sample assembly-quality masking:
- Disabled by default. Enable it by setting both
quality_bed_dirandquality_min; setting only one is an error. quality_bed_diris a directory of per-sample BED files named<sample>.bedor<sample>.bed.gz, in each sample's own genome coordinates (not reference coordinates). Samples without a matching file are simply left unmasked.- Each BED row is
chrom start end score(whitespace-separated, 0-based half-open,scorea 0-1 quality value).track/browser/comment lines and rows with fewer than four fields are ignored. Only intervals you want to flag need to be listed; positions not covered by any row are treated as passing. chrommust match the sample's own sequence names as they appear in that sample's MAF query rows. Both+and-strand alignments are handled (BED coordinates are always forward-strand).- Any aligned base whose score is below
quality_minis treated as missing, exactly as if it were anN. It therefore counts toward themax_missing_*thresholds and appears in that sample's*.missing.bed.
Reference-sample behavior:
add_ref: trueappends a syntheticREFsample (genotype0at every retained site) to both final VCFs.
Local:
snakemake -j 8 --configfile options.yamlSLURM (recommended — submit the controller as its own job):
mkdir -p logs/slurm
sbatch profiles/slurm/run-controller.sbatch options.yaml
# watch progress:
tail -f logs/slurm/controller-<jobid>.outThis runs the long-lived Snakemake controller as a SLURM job on a
non-preemptable partition and lets it submit the per-rule jobs. The
controller must outlive every rule job, so it should never run on a preemptable
queue. The wrapper always passes --rerun-incomplete.
Submit from the activated argprep conda environment, or set
ARGPREP_CONDA_ENV=argprep so the wrapper can activate it for the controller
job.
The two cluster-specific values in run-controller.sbatch are
--partition=high and --account=jrigrp (the UCD farm defaults). Override them
for another cluster without editing the file — sbatch CLI flags win over the
#SBATCH lines:
sbatch --partition=<your-nonpreemptable> --account=<your-acct> \
profiles/slurm/run-controller.sbatch options.yamlSet slurm_partition: low (or your cluster's preemptable partition) in your
config file to send the per-rule jobs to the cheap queue. Preemption is handled
safely: profiles/slurm/config.yaml wires in profiles/slurm/status-sacct.sh,
which maps a PREEMPTED job to running rather than failed. A preempted job
is auto-requeued by SLURM (requires PreemptMode=REQUEUE, the farm low
default) and reruns the rule from scratch; the controller waits for it instead
of aborting the run. The same status command also makes genuinely failed jobs
(TIMEOUT, OOM, NODE_FAIL, scancel) fail cleanly instead of hanging.
You can still launch the controller directly (e.g. on the head node for a quick run), but it is then vulnerable to being killed:
snakemake --profile profiles/slurm --configfile options.yaml --rerun-incompleteWhen using the SLURM profile, set slurm_account and slurm_partition in your config file. Slurm defaults for other resources are defined in profiles/slurm/config.yaml. Parsing the MAFs is the most computationally expensive step in the pipeline, and direct-maf rule resources can be overridden in options.yaml (maf_mem_mb, maf_time). Site calling is single-threaded, so each per-contig job requests one core.
Beta (v1.11-beta.1): metric definitions, flag thresholds, the report layout and output columns may change after user feedback.
Before the expensive per-contig run, check each input MAF with the QC-only target:
sbatch profiles/slurm/run-controller.sbatch options.yaml maf_qc
# inspect results/maf_stats/maf_stats.html, then start the main run separately:
sbatch profiles/slurm/run-controller.sbatch options.yamlmaf_qc runs one SLURM job per sample (scripts/maf_stats.py), then one small job that builds the cross-sample report (scripts/maf_stats_report.py). It is not part of rule all. The main workflow neither runs nor waits for it, and finishing QC never starts the main analysis. Building the maf_qc DAG does not read the MAFs. The main workflow's automatic contig discovery does (see #36).
Each sample's MAF is read once. QC keeps alignment quality and assembly quality apart. Alignment metrics come from the MAF alone. Assembly metrics need the query assembly FASTA (query_fasta_dir); without it they are NA, and the MAF-only view is used.
Alignment metrics (always):
- Aligned bases. Reference (and query) bases opposite a base in the other row, as bp and as % of the genome. This is the headline coverage figure. Block span (the union of block intervals) is reported too, but for AnchorWave it overstates alignment. Its blocks run between collinear anchors and can span tens of Mb, mostly as long indels. In a test on two teosinte genomes against B73 v5, blocks spanned about 94% of the reference, but only about 36% of reference bases were aligned base-to-base.
- Identity. Matches ÷ columns where both bases are A/C/G/T, case-insensitive.
- Insertion and deletion columns. Query base opposite a reference gap, and reference base opposite a query gap, each as % of alignment columns.
- Blocks and overlap. Block count,
overlapping_reference_bpandoverlapping_query_bp(bases covered by more than one block; 0 for one-to-one alignments). - Nested blocks. A block whose query interval lies inside a larger block's: the same query sequence aligned in two places, often far apart on the reference. Seen in NAM vs B73 MAFs. Nested blocks are listed separately (
<sample>.nested_blocks.tsv) and kept out of the breakpoint walk; otherwise each one would pair with its container as two fake strand flips. The report checks them for recurrence too: a nested block shared across samples at the same reference coordinates may reflect structure specific to the reference. - Structural breakpoints along each query contig. Strand flips, reference-contig jumps and out-of-order blocks are each counted on their own, plus the number of adjacencies with any of the three, per Gb aligned. Each breakpoint also gets a location:
contig_end(withinmaf_stats_breakpoint_context_bp, default 1 Mb, of a query contig end; points to assembly or scaffolding),n_gap(next to an N run; needs the FASTA), orinterior(real structure or misalignment; telling those apart needs read data). The report also checks each breakpoint for recurrence: whether other samples have a breakpoint at matching reference coordinates. Shared breakpoints are more likely biological, such as a known inversion; private ones are more likely artifacts. - MAF-only contiguity. Number and N50 of query contigs with at least one alignment. Contigs with no alignment don't appear in the MAF, so this is a lower bound on fragmentation.
- Dotplots.
Assembly metrics (with the FASTA):
- Size and contiguity. Total length and sequence count. Scaffold N50/L50 is over FASTA records. Contig N50/L50 is over the pieces left after splitting at N gaps, and is the real contiguity measure: a chromosome-scale scaffold can contain hundreds of gaps.
- Ns. N bases, and the number of N gaps (runs of at least 10 N; shorter runs are ambiguous bases).
- GC and soft-masking. Soft-masked % is
NAwhen the FASTA has no lowercase at all, i.e. it was never soft-masked. - Telomeres. A sequence end is telomeric when at least half of its terminal 1 kb is plant telomere repeat (
TTTAGGG/CCCTAAA). Ends are counted on the major sequences (those at least 10% as long as the longest one: for a chromosome-level assembly, the chromosomes, however much small unplaced sequence there is), reported as e.g. "14 / 20". Small scaffolds carrying telomere repeat are counted separately. - Unaligned sequences. Size and N/soft-masked content of sequences with no alignment. The FASTA's lengths also become the query-coverage denominator.
Identity, aligned bases, and insertion/deletion counts are summed over columns, so overlapping or secondary blocks are counted more than once. Block span is union-based. overlapping_reference_bp shows how much duplication there is.
The parser is strict. Each of the following stops the job with the file and block number in the error:
- a block without exactly one reference row and one query row;
- a size field that disagrees with the ungapped sequence;
- coordinates outside
srcSize; - inconsistent
srcSizevalues; - an unknown or ambiguous reference contig (matched exactly, or by the pipeline's contig-name normalization when that gives a single match);
- a minus-strand reference row;
- an empty MAF.
Outputs, under results/maf_stats/:
maf_stats.html— the report. It has a flagged alignment table, an assembly table, a cross-sample breakpoint table with recurrence, a strip plot of each flagged metric, per-sample drill-down (reference contigs, query contigs, breakpoints, dotplots), and metric definitions.maf_stats.tsv— one row per sample with every genome-wide metric, the counts of recurrent and private breakpoints, and aflagscolumn.maf_stats.breakpoints.tsv— every breakpoint from every sample, with its location and the number of other samples sharing it.maf_stats.nested_blocks.tsv— every nested block from every sample, with the number of other samples sharing it.<sample>.maf_stats.tsv,<sample>.by_reference_contig.tsv,<sample>.by_query_contig.tsv,<sample>.breakpoints.tsv,<sample>.nested_blocks.tsv— per-sample detail. With the FASTA, the query-contig table lists every assembly contig, aligned or not.dotplots/<sample>/<reference_contig>.png— 800×800 dotplots. Forward blocks are blue and reverse blocks red. Query contigs are stacked in labelled bands.
How samples are flagged:
- Relative flags. A sample is flagged when its robust z-score (median/MAD) passes 3.5 in the bad direction. Bad when low: aligned reference and query %, identity, assembly contig N50. Bad when high: reference and query overlap, breakpoints per Gb aligned, reference-contig jumps, private breakpoints, assembly N %. Each metric's scale has a floor, so near-constant cohorts aren't flagged for trivial differences. Relative flags need at least 5 samples with a value for that metric, so assembly metrics are flagged only across the samples that have a FASTA.
- Absolute thresholds. These are optional and off by default, because expected identity and fragmentation depend on divergence.
QC config keys (all optional):
| Key | Default | Meaning |
|---|---|---|
query_fasta_dir |
unset | Directory of query assembly FASTAs named <sample>.fa, .fasta, .fna (optionally .gz). Turns on the assembly metrics and the n_gap breakpoint class, and supplies query lengths. When set, every sample needs exactly one. |
query_fai_dir |
unset | Lengths only, for when the FASTAs aren't available but .fai files are. Mutually exclusive with query_fasta_dir. Directory of query .fai files named <sample>.fai, <sample>.fa.fai, <sample>.fasta.fai, <sample>.fna.fai, <sample>.fa.gz.fai or <sample>.fasta.gz.fai. When set, every sample needs exactly one. With neither option, query coverage uses only query contigs present in the MAF, which overstates it; the report marks this with †. |
maf_stats_min_block_bp |
0 |
Blocks shorter than this (reference span) are ignored when counting breakpoints only. |
maf_stats_overlap_tolerance_bp |
0 |
Reference overlap allowed between adjacent same-strand blocks before they count as out of order. |
maf_stats_breakpoint_context_bp |
1000000 |
A breakpoint this close to a query contig end (or N gap) is classed contig_end (or n_gap). |
maf_stats_recurrence_window_bp |
500000 |
Breakpoints in two samples are the same when both junction ends fall within this distance on the same reference contigs. |
maf_stats_dotplots |
flagged |
flagged (reference contigs with breakpoints, densest first), all, or false. |
maf_stats_dotplot_max |
20 |
Cap on flagged plots per sample; 0 = no cap. |
maf_stats_flag_min_aligned_reference, maf_stats_flag_min_identity, maf_stats_flag_max_breakpoints_per_gb |
unset | Absolute flag thresholds (aligned reference %, identity %, breakpoints per Gb aligned). |
maf_stats_threads, maf_stats_mem_mb, maf_stats_time |
1, 16000, 12:00:00 |
Per-sample job resources. Provisional: a 7.7 GB AnchorWave MAF took about 1 minute and peaked near 10 GB before block comparisons were chunked. Not yet re-measured, and not measured with a FASTA. |
maf_stats_report_mem_mb, maf_stats_report_time |
4000, 00:30:00 |
Report job resources. |
scripts/maf_stats.py also runs on its own, one sample at a time; --fasta is optional:
python scripts/maf_stats.py --maf S.maf.gz --reference-fai ref.fa.fai --sample S \
--out-dir qc [--fasta S.fa.gz]scripts/maf_dotplot.py also works on its own for quick plots: python scripts/maf_dotplot.py --maf S.maf.gz --reference-fai ref.fa.fai --out-dir plots. It exits non-zero if any input fails.
example_data/ ships with a small simulated dataset (example.maf/, example.reference.fa) and a matching options.yaml. From the repo root:
snakemake -j 4 --configfile example_data/options.yamlOutputs land in example_results/. To regenerate the example data from scratch, see the Simulation Helper section.
Outputs are written under results/ by default (or under results_dir if provided):
sites/combined.<contig>.all_sites.vcf— every retained site (invariant + variant) that passed all filters;INFO=SC=invariant|variantdistinguishes the twosites/combined.<contig>.vcf— variant-only subset ofall_sites.vcfsites/combined.<contig>.mask.bed— merged BED intervals for masked positionssites/combined.<contig>.site_summary.tsv— per-contig counts (see table below)sites/combined.<contig>.report_stats.tsv— compact window and per-sample counters used to buildsummary.html; the report does not rescanall_sites.vcfsites/combined.<contig>.sites— ARGweaver-format sites file (variant sites only; one real base per pseudo-haploid sample,Nfor missing). Emitted only whenemit_argweaver_sites: truesites/combined.<contig>.<sample>.missing.bed— per-sample missing regions used by per-MAF summary stats; 4-column BED (chrom,start,end,sample)sites/combined.<contig>.sample.mask.bed— concatenation of all per-sample missing BEDs for the contig, in configured sample ordersummary.html— genome-wide overview plus per-MAF tables and per-contig per-MAF breakdowns. Each contig gets two binned plots: invariant and missing on a fixed 0–100% axis, and variable sites on their own auto-scaled axis (variable sites are usually a fraction of a percent and are unreadable at 0–100%). The variable axis bound is computed once across all contigs, so contigs remain directly comparable to each other.maf_by_contig/<sample>/<contig>.maf.gz— intermediate per-contig MAF chunks produced by thesplit_sample_mafstage (each per-sample MAF is partitioned by reference contig so site calling reads only the relevant slice, and chunks are gzip-compressed to avoid duplicating the alignment corpus uncompressed); these are regenerable intermediates, not final outputs
Both VCFs share the same header and use a single haploid GT per sample (0 for the REF allele, 1/2/... for ALTs in ALT order, . for missing). INFO carries NS (non-missing samples), MS (missing samples), and SC (invariant or variant). All retained sites are emitted with FILTER=PASS; filtered-out positions appear in the BED mask, not the VCFs.
all_sites.vcf remains a required scientific output containing invariant as well as variant retained sites. Report statistics are accumulated during the same calling pass and written separately so generating summary.html does not reread the large VCF.
Optional .sites files are generated only when enabled. Two caveats apply when toggling emit_argweaver_sites inside a results directory you have already run:
- Enabling it: run the default target (plain
snakemake --configfile options.yaml). If you ask only forresults/summary.html, nothing in that part of the DAG forces the site-calling rule to re-run, so no.sitesfile is written. A fresh results directory produces.siteseither way. - Disabling it: stale
.sitesfiles from a previous run are left in place rather than deleted. Start from a clean results directory if that distinction matters.
The site_summary.tsv contains one metric per row with columns metric and value:
| metric | description |
|---|---|
contig |
contig name |
contig_length |
contig length in bp |
samples |
number of samples |
allowed_missing |
effective missing-sample threshold used |
all_sites |
retained sites (invariant + variant) |
variants |
retained variant sites |
invariant |
retained invariant sites |
masked_total |
total masked positions |
masked_intervals |
number of merged BED intervals in the mask |
masked_missingness |
positions masked due to too many missing samples |
masked_indel_adjacent |
SNPs masked because they immediately flank an indel |
masked_multiallelic |
positions masked due to more than two alleles |
masked_no_alignment |
positions masked because at least one sample had no alignment |
masked_ref_non_acgt |
reference positions with non-ACGT bases (always masked) |
The pipeline still validates that retained sites plus the mask span each contig exactly, but that check is now internal and is no longer written as a separate coverage.txt file.
The variant-only VCF (sites/combined.<contig>.vcf) is a standard VCF and can be fed to downstream ARG-inference tools directly or after a light conversion. ARGprep does not need to produce a genetic/recombination map — that is an independent biological input the inference tool takes separately (a scalar rate for ARGweaver, a --map file for Relate).
Relate. Convert the variant VCF to Relate's haps/sample format with the RelateFileFormats helper that ships with Relate:
RelateFileFormats \
--mode ConvertFromVcf \
--haps combined.<contig>.haps \
--sample combined.<contig>.sample \
-i sites/combined.<contig> # reads sites/combined.<contig>.vcfYou then supply Relate itself with a genetic map (--map) and mutation/recombination rates at run time; a flat constant-rate map is fine if you have no measured map. See the Relate documentation for the full pipeline (PrepareInputFiles, ancestral-allele polarization, and mask handling — the per-contig mask.bed can seed the accessibility mask).
Note: ARGprep emits a single-allele haploid
GTper sample. Relate is designed around phased haplotype data, so verify the converted.hapstreats each sample as one haplotype as you expect before running a large analysis.
ARGweaver. Set emit_argweaver_sites: true to have the pipeline write a native .sites file per contig directly — no conversion step needed. Each sites/combined.<contig>.sites is:
NAMES <sample1> <sample2> ...
REGION <contig> 1 <contig_length>
<pos> <one real base per sample> # variant sites only; missing calls are N
Because ARGprep is pseudo-haploid, each sample contributes exactly one character per site (no phasing needed). Only variant sites are listed — the set matches sites/combined.<contig>.vcf. When add_ref: true, the synthetic REF haplotype is appended as the final column (always the reference base). Supply ARGweaver's recombination rate on its command line (arg-sample -r ...); ARGprep does not emit a recombination map.
SINGER. No conversion needed — SINGER reads a VCF directly (singer_master -vcf sites/combined.<contig> ..., without the .vcf suffix). Supply the mutation/recombination rates on SINGER's command line.
pytest -qThe repository includes scripts/simulate_msprime_indels.py for generating haploid test datasets with msprime SNP variation plus branch-based indels on the tree sequence. Note that these simulations are not intended to be evolutionarily accurate, but simply to give a reasonable example data.
Example:
python scripts/simulate_msprime_indels.py \
--sequence-length 1000000 \
--num-samples 8 \
--theta 0.01 \
--rho 0.01 \
--ne 10000 \
--indel-rate 1e-8 \
--indel-lambda 0.001 \
--seed 8675309 \
--out-prefix example_data/exampleOutputs:
<prefix>.reference.fa<prefix>.samples.fa<prefix>.indels.tsv<prefix>.summary.tsv<prefix>.maf/
Summary fields include:
seedsequence_lengthreference_bp_with_indel_in_ge1_sampletotal_snpssnps_without_overlapping_indel
These are standalone utilities that are not part of the Snakemake workflow. No rule
invokes them, they are not run by snakemake, and their outputs are not part of the
pipeline's deliverables. Run them by hand against a completed run's outputs.
scripts/window_to_fasta.py builds a reference-anchored multi-FASTA for a single window, so you can eyeball a region as sequence rather than as a variant table:
python scripts/window_to_fasta.py \
--vcf results/sites/combined.<contig>.all_sites.vcf \
--bed-dir results/sites \
--reference /path/to/reference.fa \
--contig <contig> --start <start> --end <end> \
--out window.fa--start/--end are 1-based inclusive. Output is the reference sequence followed by one
sequence per sample. It builds each sample by starting from the reference, overwriting
positions where that sample's GT names a different allele, then overwriting every
interval in combined.<contig>.<sample>.missing.bed with N.
It requires all_sites.vcf rather than the variant-only VCF: starting from the reference
and patching in differences is only sound if every invariant position was positively
called as matching reference. The variant-only VCF cannot distinguish "called, matches
reference" from "never assessed".
Indels are not represented, in either direction. Every output sequence is exactly the window length and contains no gap characters, because the whole pipeline works in reference coordinates:
- An insertion in a sample consumes no reference position, so the inserted bases are dropped and leave no trace in the FASTA.
- A deletion in a sample is recorded as missing, not as a gap, so it appears as
N.
An N is therefore ambiguous: it may mean the sample never aligned there, that the
sample carries a deletion, that the base was N/ambiguous in the MAF, or that it fell
below quality_min. The per-sample .missing.bed merges those same cases and cannot
separate them either; recovering that distinction means going back to the MAF. Treat the
output as a reference-anchored substitution view, not as a true alignment.
The window is held in memory once per sample, so this is intended for kilobase-scale windows, not whole chromosomes.
scripts/maf_to_fasta.py builds a gapped multiple alignment
for a window by going back to the MAFs, so indels survive: a deletion becomes a gap
column and an insertion adds columns that every other sample pads with gaps. This is the
thing window_to_fasta.py deliberately is not. It reads the per-contig MAF chunks the
workflow already writes under results/maf_by_contig/, so it does not need the
all_sites.vcf:
python scripts/maf_to_fasta.py \
--maf-chunk-root results/maf_by_contig \
--reference-fasta /path/to/reference.fa \
--contig <contig> --start <start> --end <end> \
--out window.fa--start/--end are 1-based inclusive and default to the whole contig. Pass
--maf-dir instead of --maf-chunk-root to read whole-genome per-sample MAFs directly
(<sample>.maf / .maf.gz), and --quality-bed-dir / --quality-min to apply the same
per-sample quality masking the workflow uses. --max-columns caps the alignment width.
The merge is reference-anchored: each reference base gets one anchor column, and the pairwise alignments are related to each other only through it. Four consequences are worth stating outright.
- Insertion homology is asserted, not established. Insertions are left-anchored to the preceding reference base and left-aligned within that anchor's columns, padded on the right to the widest insertion any sample carries there. Two samples inserting at the same anchor therefore share columns, but the pairwise MAFs cannot show that they are the same event. An insertion preceding the window's first base anchors outside the window and is dropped. Leading insertions at a block boundary are retained when their preceding reference coordinate lies inside the window.
- Conflicting blocks degrade to
N. Where two alignment blocks cover one reference position with different calls, the anchor becomesN, matching the?thatmaf_to_sites.pyassigns. The analogous rule for insertions is that conflicting sequences at one anchor become a run ofNas long as the longer of the two. This includes an insertion conflicting with explicit absence in another block spanning that slot. A block ending or starting at the slot does not establish absence. - By default it does not agree with the VCF. It applies no missingness threshold and
does not know which sites the workflow masked, so it can show a base where
mask.bedmasked the site. Pass--mask-bed results/sites/combined.<contig>.mask.bedto reconcile them: masked reference positions render asNfor every sample, and insertions anchored there are dropped. - Width can blow up. An insertion-rich region across many samples can make a 10 kb
window far wider than 10 kb, and memory is columns × samples.
--max-columns(default 5,000,000) rejects reference spans above the cap before loading sample alignments, and checks the insertion-expanded width before rendering rows. It is a width limit, not a memory guarantee: input parsing, stored projections, and sample count still consume memory. Use an explicit small window for large genomes.
Characters mean: A/C/G/T a called base; - a gap, meaning either a deletion in
that sample or padding beside another sample's insertion; N no information — the sample
never aligned there, the MAF base was ambiguous, it fell below --quality-min, blocks
conflicted, or --mask-bed masked the position. Unlike window_to_fasta.py, N no
longer conflates deletions with the rest.
