Hop atlas home · Overview · Changelog · Architecture · Adding_Data · Introgression · Ribbon · Dosage · Phylogeny · Bundle · Ingest · Ingest_Gbs · Ingest_Wgs · Ingest_Grasdi · Ingest_Rnaseq · Ingest_Assembly

Rendered from hop_atlas/components/INTROGRESSION.md (2026-10-13)

Introgression atlas: how to update it (pedigree, subspecies / lineage layers, new cohorts)

Status: living document, started 2026-10-08. All products it describes are tentative. Served atlas: scratch_plots/introgression_atlas, on port 8900 (http://10.112.218.205:8900/introgression_atlas/). Other atlases: the ribbon (structural) atlas and the dosage atlas have their own guides, RIBBON.md and DOSAGE.md. All three share the generic stage / build / test / deploy tool hop_atlas/tools/atlas_deploy.py (profiles introgression, ribbon, dosage). For this atlas, atlas_layers.py (below) adds the layer-specific build steps. Companion: annevo_eval/docs/INTROGRESSION_ATLAS_TECHNICAL.md describes how the base atlas is computed (catalogue, NA/EU calls, phasing, sex chromosomes, front end). Its snapshot is dated 2026-10-03. This document covers what was added later and how to update it.

Overview: what is served today (2026-10-13)

The atlas is one page (index.html) drawing every row on a canvas; one row = one sample library, one tile = one calling bin (5 Mb / 2 Mb / 1 Mb / 500 kb). Everything is static JSON; the page grew by anchored patches (table below).

Rows. 2,299 sample rows drawn by default, from 12 cohorts: diversity panel 199, USDA 173, Zenith x 21058M 130, Cascade x LB_male 182, Saaz x HL1012 257, Wye Target x BL2/1 144, Apollo x Pub_740M 186, KR (cordifolius) 1, OSU 826, Wye RNA lane 13, Pilgrim (DArT) 166, Carey 2026 WGS 22 (live since 2026-10-09). Opt-in: Zhang 2017 HapMap, 4,453 rows (5 Mb and 2 Mb only). Counts from multires/atlas_samples_5Mb*.json.

Haplotype-resolution levels (drop-down; each level is conditioned on the one above it):

Level Key Covers Notes
NA/EU (atlas, served) (base) every cohort the base clade calls (multires/)
NA/EU (BAM re-call) L0 cohorts with reads SNP layer, population-validated A/B sites (lineage/)
Subspecies L1 cohorts with reads lupulus / lupuloides / neomexicanus, ApeKI tag space; ChrX sex-aware
Lineage (European) + American subspecies L2 cohorts with reads (WGS / GRAS-Di limited) European lineages on lupulus copies
Subspecies + cordifolius (experimental) L1c cohorts with reads 4th group from KR + Carey CORDI 2/4/7/8; overlays called cordifolius bins (§8b)
WGS subspecies (variants) Lw the 29 WGS rows of the 30-sample joint call calls lupulus / American / cordifolius / U; the lupuloides / neomexicanus / pubescens guess is faded and off by default (§8d)

Pilgrim (DArT) and Zhang 2017 (genotype calls) have no reads, so they stay on the base NA/EU lane in every level.

Controls and URL state. - Reference (?ref=<key>, default 21110M): only references with real calls are listed (today 21110M + Saaz hap1, 10-12 hap1, Apollo hap1, Apollo hap2, Cascade, for Zenith x 21058M). On another reference only the plants with calls there are drawn, in its own bins, and 21110M-only layers are greyed. The lifted ("liftover") display frames and the universal uMb option were removed on 2026-10-13 (§8e). - Datasets drawer (?ds=a,b,..., remembered per browser): which cohorts are drawn; Zhang 2017 is off by default and its files load only when selected. Canvas guard: past the ~32,000 px browser canvas limit the largest open sections are folded (or the canvas clamped) with a message, never a blank canvas. - Presets (?preset=<id>): saved queries that switch on the datasets they need, e.g. zhang2017, wild_wgs ("Wild WGS (Carey 2026 + KR)", 90 rows), not_19058m, pilgrim_wye. - ?sample=<key or name> opens a row (used by Phylogeny tip links); ?ped= opens the pedigree lookup. The sample search has Clear and Esc. - Badges and evidence pages. Row-label badges (sex disagreement, trace Y, OSU tags, graded paternity father≠19058m · strong|moderate|possible) open a popover with See evidence, linking to the Badges page evidence/index.html or the OSU paternity page (§7). "Badges" is in the nav bar. - Evidence detail (click a row): per-bin read evidence; in the Subspecies / Lineage levels the hover box continues with the subspecies evidence (§8). - Labels: the Havill cross is displayed as "Zenith x 21058M" (cohort key havill unchanged; tools/relabel_havill.py, 2026-10-12).

Patch markers

Each feature added after the base page is an anchored, idempotent patch: the script asserts every anchor occurs exactly once, skips hooks already present, and writes a marker comment so the hook can be found. Found in the served index.html and *.js on 2026-10-13 (grep -o '/\*[a-z0-9-]*' index.html *.js); tools/ = hop_atlas/tools/. The packager (tools/package_atlas_bundle.py) re-scans the markers each time it runs and warns about any it cannot map to a script.

Marker Where Script What it adds
/*vsex*/, /*vsex2*/ index.html sync/2026-10-04/verified_sex_update/scripts/v03_atlas_verified_sex.py verified sex: filter tokens (vsex:, sex:disagree, tag:, group:), sex badges and hover box, sex presets
/*o08-preset*/ index.html sync/2026-10-02/osu_introgression_atlas/scripts/o08_preset_patch.py OSU sections and presets
/*o18*/ index.html sync/2026-10-02/osu_introgression_atlas/scripts/o18_y_hover.py ChrY lineage line in the sex hover
/*ev2*/ index.html sync/2026-10-03/atlas_evidence_rebuild/viewer_patch/e06_patch_index.py evidence tracks (haplotype rows, AIM, block catalogue; evidence_v2.js)
/*pilgrim*/ index.html sync/2026-10-07/pilgrim_in_atlases/scripts/p05_atlas_export.py Pilgrim DArT cohort sections, pilgrim_wye preset
/* lin */, // lin, <!-- lin --> index.html sync/2026-10-07/lineage_refinement_v2/viewer/patch_index_lineage.py (via tools/atlas_layers.py patch) haplotype-resolution levels: lineage.js hooks in tiles, detail panel, ChrY, frequency tracks
<!-- ped --> index.html …/viewer/patch_index_pedigree.py (via atlas_layers.py patch) pedigree lookup (pedigree.js, ?ped=)
/*zhang2017*/ index.html sync/2026-10-08/zhang2017_hapmap_project/ancestral_haplotypes/scripts/zhang2017_atlas_export.py, tools/zhang_regroup.py Zhang 2017 opt-in dataset: sections, genotype-only detail panel
/*zhfold*/ index.html tools/patch_datasets_guard.py Datasets drawer, lazy loading of opt-in datasets, canvas guard
/*evbadge:1..5*/ index.html tools/patch_evbadge.py graded paternity badge, popover with See evidence (evbadge.js), legend link
/*sclear*/ index.html tools/patch_sample_clear.py Clear / Esc for the sample search
/*subev:1..4*/ index.html tools/patch_subspecies_evidence.py subspecies evidence in the Evidence-detail hover (subev.js)
/*c26ds*/, /*c26wild*/ index.html hop_atlas/cohorts/carey2026/pipeline/c26_23_patch_index.py Carey 2026 on by default in the drawer; wild_wgs preset
/*urlsample*/ index.html tools/patch_url_sample.py ?sample=
/*cordi:1..9*/, /*cordi:s1..s8*/ lineage.js, subev.js tools/patch_cordifolius_level.py level Subspecies + cordifolius
/*hphase:1..6*/ lineage.js tools/patch_halves_phase.py display order of tile halves
/*wgsv:1..16*/ index.html, lineage.js tools/patch_wgsv_level.py level WGS subspecies (variants) (wgsv.js)
/*wfill*/ lineage.js tools/patch_wgsv_fill_default.py WGS-variants guess hidden by default
/*frames*/ index.html tools/patch_frames.py Reference selector, per-reference calls (frames.js), liftover removed, lifted geometry stripped

Order after a re-copy. If a cohort script rewrites index.html or atlas_layers.py patch re-copies lineage.js, re-apply on the stage in the order they were first deployed, which reproduces the anchors each script was written against: atlas_layers.py patch, patch_datasets_guard.py, patch_evbadge.py, patch_sample_clear.py, patch_subspecies_evidence.py, c26_23_patch_index.py, patch_cordifolius_level.py, patch_halves_phase.py, patch_wgsv_level.py, patch_url_sample.py, patch_wgsv_fill_default.py, relabel_havill.py, patch_frames.py. Each --check lists its hooks; a script whose anchor is missing aborts without writing.

How haplotypes are assigned (plain words)

Common recipe. For every level: (1) pick sites where reference groups of plants or assemblies carry different bases; (2) count each sample's reads carrying each base at those sites; (3) for each bin, compute how likely the reads are under each pair state (AA / AB / BB, or a pair of subspecies); (4) call the bin only if the best state has posterior ≥ 0.95 with ≥ 3 informative sites, otherwise leave it white. Bins are called independently (no smoothing between bins). A tile is an unordered pair; its two halves are ordered for display only (§8c).

Autosomes, NA/EU. - Clades are defined by assemblies only: in each 5 Mb bin, each of the 13 panel haplotypes is called clade A (European-like, close to drHumLupu, Saaz, HL1012) or clade B (American-like, close to the two wild American plants MN1421 and MN586). - Diagnostic sites: positions where ≥ 3 clade-A haplotypes all share one base and ≥ 3 clade-B haplotypes all share a different base, about 29 M sites on 21110M, thinned to one per 150 bp grid cell. - Counts: reads aligned to 21110M, piled up at those sites (route B: BAM pileup; MAPQ ≥ 20, base quality ≥ 20; GBS duplicates kept, WGS duplicates dropped). - Model: per cohort, a two-state EM model fitted once at 5 Mb and held fixed at finer resolutions. Besides a read-error term it has an outlier term (paralogous reads), separate dropout terms for a het that looks AA or BB (GBS restriction-site and library effects), and a B-read efficiency w that absorbs reads carrying the non-reference base mapping less well.

Subspecies and lineage. - Subspecies (L1): reference groups of plants (e.g. 28 lupuloides and 35 neomexicanus GBS plants, plus lupulus), each plant scored leave-one-out; counted in alignment-free ApeKI cut-site tag space (64-mers), so it does not depend on the 21110M alignment. Conditioned on the NA/EU call (an AA bin can only be lupulus / lupulus). - Lineage (L2): European lineages on the lupulus copies from the SNP layer, conditioned on L1; American copies stay at subspecies level. WGS and GRAS-Di cohorts get subspecies only, because the GBS-trained lineage calls do not transfer. - Cordifolius (L1c, experimental): a 4th group (5 plants) allowed under any NA/EU call. - WGS variants (Lw): five groups built from WGS plants only (incl. pubescens and cordifolius), scored at joint-call SNPs. Synthetic F1 controls could not separate lupuloides, neomexicanus and pubescens per bin, so only lupulus / American / cordifolius / none is called; the finer guess is unvalidated and hidden by default.

Sex chromosomes. - ChrX: sex-aware. Males carry one X copy in the X-specific region (below ~234 Mb on 21110M ChrX), so only homozygous (haploid) states are possible there; 234–240.8 Mb depends on the Y lineage; the pseudo-autosomal region is diploid, with reads summed over X and Y. - ChrY: the male-specific region is called European-type (L) or American-type (Am) from sites where 21110M and the MN1421/MN586 Y differ. - Sex itself comes from male-specific-Y read depth (mother-masked, within a cross), cross-checked by X dosage and Y k-mers; disagreements are badged, not overridden. - On a non-21110M reference, ChrX uses one diploid model for every plant and is labelled "not sex-aware".

Reference-bias mitigations. - Read counts come from BAM pileups (reference-only reads counted), not variants-only VCFs, for every cross moved to route B; VCF cohorts hid reference-allele evidence and gave "depleted" all-AB bins. - The EM's dropout terms and B-read efficiency absorb systematic under-counting of the non-reference allele. - Subspecies calls use alignment-free tag space. - Leave-one-out scoring of reference plants; must-fail controls (shuffled groups, swapped labels) for each new level. - Per-reference calls (§8e; annevo_eval/methods/per_frame_ancestry/): the whole call (catalogue, counts, EM) re-made on other assemblies, so a call can be compared across references instead of trusting 21110M alone.

Biases that remain. - All served calls are made on one reference (21110M, a male European-type composite); per-reference calls exist so far for one cohort only. - Ascertainment: the diagnostic sites come from 13 assembly haplotypes; clade B rests on two wild American plants, and none of the assemblies is pubescens or cordifolius, so the two-state NA/EU fit is not established for those (Carey section headers and KR say so). - Small wild reference groups for the subspecies levels. - Zenith x 21058M, Saaz, divpanel and USDA base calls still come from variants-only VCFs (route V), which over-call AB (PI decision D2, ingest/README.md). - The catalogue is 5 Mb-derived; inside fine bins whose call switches, site density drops (~0.2×). - Genotype-only cohorts (Pilgrim, Zhang 2017) have no read depth: hets are under-called and no finer level is possible.


Short answer: can a new dataset go from SRA to the atlas by following written instructions?

No, not end to end yet. - Read alignment is generic and runnable: cluster/lepton_linear/ (README, linear_job.sh, per-population config/<pop>_<ref>.env). - The add-on layers have one tool: pedigree, subspecies / lineage, sex-aware ChrX labels and deployment use hop_atlas/tools/atlas_layers.py (§2–§5). - Everything else is cohort-specific: the steps between alignment and the served atlas JSON (allele counting, NA/EU calls, phasing, sex chromosomes, detail files, viewer patch) exist only as dated, copy-and-edit scripts. OSU used o01…o18, Pilgrim used p01…p05, Wye used m01…m07. - Download and read QC exist only as one-off records.

§6 maps every stage to the best existing script and marks each gap.


1. What is served, and where it comes from

Served path What it is Produced by
index.html the viewer page; the lineage (32) and pedigree (4) hooks plus every later patch (table "Patch markers" above) base page from the cohort scripts; lineage / pedigree hooks from viewer/patch_index_lineage.py and viewer/patch_index_pedigree.py; later hooks from hop_atlas/tools/patch_*.py
lineage.js, pedigree.js the haplotype-resolution and pedigree layers sync/2026-10-07/lineage_refinement_v2/viewer/
lineage/ → symlink to /data/hop_data/lineage_refinement/viewer_lineage* subspecies / lineage strings per sample (atlas_lineage_<res>.json), plus per-bin evidence (detail/<res>/<cohort>__<sample>.json.gz) scripts/e03_viewer_export.py, scripts/e04_detail_export.py
pedigree/atlas_pedigree.json the pedigree graph and its pedigree-name → sample matches scripts/p01_pedigree_build.py
phase/atlas_{phase,concordance}_<res>.json parental lanes and a/b/c labels; ChrX recomputed sex-aware base: p01_phase_concordance.py / o01_phase_osu.py; ChrX: scripts/x11_sexaware_x_concordance.py
multires/, sexchrom/, detail_mr/, atlas_*.json the base atlas cohort scripts (see the technical doc, and §6 here)

scripts/ and viewer/ above mean sync/2026-10-07/lineage_refinement_v2/scripts/ and …/viewer/ in the main checkout.

2. The rule: stage → change → deploy (tests on request)

Tests run only when the PI asks (PI, 2026-10-08). They are kept and documented, but not run after every change.

Other sessions edit the served atlas too. On 2026-10-07 a Pilgrim cohort was added while the lineage layer was being staged. Never copy an old staged page over the served one. Always start from the current served copy:

T=hop_atlas/tools/atlas_layers.py
python3 $T stage --name my_change          # copies :8900 to /data/hop_data/lineage_refinement/stages/my_change/ + checksum snapshot
STG=/data/hop_data/lineage_refinement/stages/my_change
# ... make changes on the stage (sections 3 to 5) ...
python3 $T serve $STG --port 8907          # review at http://10.112.218.205:8907/introgression_atlas/
python3 $T test  $STG --port 8907          # ONLY when the PI asks: every viewer test -> $STG/checks.txt (~5 min)
python3 $T deploy $STG                     # refuses if :8900 changed since `stage`; lists files; asks; backs up *.bak_<date>_<name>
# python3 $T test /workspace/annevo-monkeyflowers/scratch_plots/introgression_atlas --port 8900   # only when the PI asks

3. Updating the pedigree

Source: /data/hop_data/pedigree.txt, from doi:10.13020/02k6-6995. - Format: tab-separated, columns Selection Mother Father, with one header line. Lines starting with # are comments. - Conventions (kept as distinct plants, never merged with the base cultivar):

Notation Meaning
# unknown or open-pollinated parent
T / 4N suffix induced tetraploid of the base cultivar (NuggetT)
.p, .sib, X× intermediate seedlings
(A×B) or A×B an unnamed seedling of that cross; its parents are parsed from the name

To add or correct a pedigree: edit pedigree.txt (keep a dated backup), then rebuild on a stage:

python3 $T pedigree $STG      # writes $STG/pedigree/atlas_pedigree.json and the review tables

Review the matches before deploying: - tables/P01_pedigree_matches.tsv lists every pedigree-name → sample match with its basis. - tables/P01_unmatched_nodes.tsv lists the entries without samples.

How names are matched (all in scripts/p01_pedigree_build.py):

Basis Rule Where to edit
exact same name after normalising case, punctuation, USDA_ prefix, Mount/Mt., ×/x, and bracketed notes; parent rows like "Apollo mother (Apollo)" use the name in brackets norm(), clean()
variant atlas name = pedigree name + a clone or replicate suffix (Fuggle_N., Zeus_1, Saazer_38). A male marker (19005_M) is never treated as a suffix SUF0 / _Suf
synonym curated spelling equivalents (Saazer = Saaz, Hallertauer Mittelfruh = Mittelfrueh, Vanguard = "Vangaurd", Target = Wye Target, …) SYN
approximate near-equivalents, flagged (Swiss Tettnanger ≈ Tettnanger) APPROX
unverified PI notes we have not verified (USDA 9058_M = 19058M) UNVER
same plant, RNA Wye RNA parent libraries RNA
excluded known mislabels or ungenotyped samples (divpanel USDA_19058m_3; Pilgrim parents) EXCL (and the Pilgrim rule)
mapping-population progeny each cross's progeny attached as an offspring node of its two parents CROSS

4. Updating the subspecies / lineage layers

4.1 What the layers are built from

Layer (viewer drop-down) Calls read Built by
NA/EU (BAM re-call) /data/hop_data/lineage_refinement/calls/<cohort>_v6/<sample>.npz (SNP layer, population-validated A/B sites) scripts/c01_call_levels_v6.py (c01_run_all_v6.sh)
Subspecies /data/hop_data/lineage_refinement/tagspace/calls/<cohort>_t4/<sample>.npz (cut-site tag alleles; ChrX sex-aware) scripts/c01t_call_levels.py (c01t_run.sh)
Lineage (European) + American subspecies calls/<cohort>_v6 (L2) combined with the subspecies call the composite rules in scripts/e03_viewer_export.py
ChrY class tables/Y01_am_y_split.tsv scripts/y01_am_y_split.py
Group definitions sync/2026-10-06/lineage_refinement/tables/C00_groups.tsv v1 lineage work (C00 / C02 separability tests)

4.2 Re-export after calls change for existing cohorts

python3 $T lineage $STG    # E03 strings + E04 per-bin evidence into a NEW /data/hop_data/lineage_refinement/viewer_lineage_<stamp>/
                           # and points $STG/lineage at it (the served data directory is untouched until deploy)
python3 $T test $STG --port 8907

l23_lineage_v3_test.py re-derives every exported string from the call files independently, so a bad export fails that test.

4.3 Add a cohort to the subspecies / lineage layers

These steps are recorded in sync/2026-10-07/lineage_refinement_v2/PIPELINE.sh. That file is documentation only (paths hard-coded), and each step is run per cohort. 1. BAMs. Mapped to 21110M, with no duplicate marking for GBS (see annevo_eval/docs/ASSAYS.md). 2. SNP layer. Allele counts at the L01 catalogue sites: sync/2026-10-06/lineage_refinement/scripts/l02_count_one.sh (l02_run_list.sh; WGS via l02_make_wgs_jobs.py / l02_run_wgs.sh on the 500 bp thinned target list), then l03_build_sparse.py. GBS keeps duplicates. Then c01_call_levels_v6.py → calls/<cohort>_v6. 3. Tag space. Extract the cut-site 64-mers with kt (samtools view -F 0x900 <bam> | kt --mode gbs5scan for GBS; --mode scan for WGS and GRAS-Di). Then t03_run.sh <cohort> (sparse counts) and EXTRA='--minref 1' c01t_run.sh _t4 <groups> <cohort> with --l0-from the v6 calls → tagspace/calls/<cohort>_t4. 4. Register the cohort in the export. Add it to the hard-coded COH list in e03_viewer_export.py, e04_detail_export.py and l23_lineage_v3_test.py. If the cohort's GBS-trained lineage calls do not transfer (WGS, GRAS-Di), add it to LIMITED_COH (§3 of the dossier explains why). 5. Export and test. atlas_layers.py lineage, then test.

Cohorts with no reads (Pilgrim, DArT genotypes) cannot enter these layers. They stay on the NA/EU atlas lane in every view.

4.4 Changing the groups or the catalogue

Changing the groups (C00_groups.tsv) or the haplotype catalogue invalidates all calls. Re-run steps 2–5 for every cohort, plus the separability tests (c02t_separability.py) and the population-validated A/B filter (n03_ab_site_filter.py, n06_holdout.sh). This is a research step, not routine maintenance.

4.5 Sex-aware ChrX labels

After phase/ or sexchrom/ change (a new cross, re-phased parents, new sex calls):

python3 $T xconc $STG     # X11: ChrX a/b/c + parental ChrX lanes; keeps the originals as *.atlas_orig.json; skips crosses without
                          # progeny in the concordance file or without sex-aware parent calls (e.g. Pilgrim)

5. Viewer code

Test Covers
l27_pedigree_test.py pedigree search, tree, sections vs an independent walk, ?ped= links, hover, Clear
l26_pilgrim_smoke.py the Pilgrim (DArT) cohort in every view
l23_lineage_v3_test.py every exported lineage string vs the call files; pixels; ChrY; 1 Mb
l24_detail_view_test.py detail panel follows the drop-down; ChrY band; sex-aware X labels; X/Y hovers
l25_freq_tracks_test.py per-group frequency tracks vs an independent computation; mixture rows; Havill-47 removed
p12_label_integrity_no47.py, p11_pixel_test.py the base atlas's own label and pixel regression tests

6. Ingesting a new cohort end to end: what exists today

Stage Best existing instructions Runnable and generic? Gap
1 Download (SRA/ENA, or PBSS) /data/hop_data/osu_PRJNA1424574/scripts/DOWNLOAD_COMMANDS.sh (+ fetch_all.sh, fetch_one.sh, build_manifest.py); CLAUDE.md data rules (PBSS first on EOS) no, one cohort's record ("not meant to be re-executed blindly") no generic downloader or manifest step
2 Read QC / cleaning cluster/lepton_linear/linear_job.sh (fastp, DO_FASTP); Havill needs cleaning first (memory havill-fastq-must-be-cleaned-before-use) partly (inside the alignment job) no standalone QC doc
3 Alignment cluster/lepton_linear/README.md + linear_job.sh + config/<pop>_<ref>.env + submit_linear.py; the lepton, eos-slurm and parabricks skills yes, population- and reference-agnostic none for the linear route
4 BAM rules cluster/lepton_linear/README.md ("Five things"), annevo_eval/docs/ASSAYS.md yes (rules) none
5 NA/EU (clade A/B) calls into atlas bins annevo_eval/docs/INTROGRESSION_ATLAS_TECHNICAL.md §1.2 (routes V/B/K), §3–4, §10.2 (how OSU, KR and wyerna were added); scripts sync/2026-09-30/multires_clade/ (m01 count, m03/m04 fit and call) no, copy-and-edit; job tables and sample lists are [manual] no generator; no cohort manifest
6 Phasing, concordance, sex chromosomes sync/2026-09-30/phased_parents_switch_concordance/, sync/2026-09-30/sex_chromosome_lineages/; OSU forks o01_phase_osu.py, o11_sexchrom_osu.py; then atlas_layers.py xconc no, cohort-specific (parents hard-coded) no parameterised version
7 Base viewer JSON (multires/, detail_mr/, presets, index.html entries) OSU o07_viewer_export.py, o08_preset_patch.py, o17_detail_osu.py; Pilgrim p05_atlas_export.py no, one script set per cohort "no generator for the served viewer" (technical doc §11.2)
8 Subspecies / lineage layers §4.3 here per-cohort steps written down; export and deploy are tooled COH lists are hard-coded
9 Pedigree §3 here yes (atlas_layers.py pedigree) name matching needs review per new cohort
10 Deploy and test §2 here yes (atlas_layers.py) none

Proposed next step (not yet done): a cohort manifest (accessions, assay, enzyme, parents, roles, truth plants) and one parameterised script per stage 1–8 that reads it. Stage 7 would then regenerate every served JSON from the manifests instead of patching. The technical doc §10.2 makes the same proposal.

7. Evidence pages and row-label badges ("See evidence")

What it is. Row-label badges (sex disagreement sex F, trace Y trY, OSU anomaly tags, and the graded paternity badge father≠19058m · strong|moderate, father≠19058m? · possible) open a popover on hover or keyboard focus. The popover shows the badge's evidence summary and a See evidence button: paternity → evidence/osu_paternity.html?s=<seedling>; every other badge → evidence/index.html?s=<key>#badge-<type> (the badge page). The atlas legend links to the badge page. Deployed to :8900 on 2026-10-08 23:47 (re-applied on top of the Zhang deploy); "Badges" is in the nav bar.

Served path What it is Produced by
evidence/index.html the badge page: hop_atlas/evidence/BADGES.md + a generated badge catalogue (anchors badge-<type>, badge-tag-<group>) tools/build_evidence_pages.py
evidence/osu_paternity.html hop_atlas/evidence/osu_paternity.md with interactive figures and the seedling card tools/build_evidence_pages.py
evidence/osu_paternity_data.json, osu_paternity_blocks.json per-seedling statistics (369 rows) and per-5-Mb-block counts + reference median/range build_evidence_pages.py from sync/2026-10-08/osu_paternity_uncertainty/tables/*.tsv and /data/hop_data/osu_paternity_uncertainty/work/blocks_deep.npz
evidence/evidence.{js,css} plain JS/SVG figures (no library), light/dark copied from tools/evidence_assets/
sexchrom/atlas_sexchrom.json field vpg graded paternity per OSU seedling (72 flagged + 2022005-164 possible); vpat/vtag untouched tools/build_paternity_badges.py
index.html hooks /*evbadge:1..5*/, evbadge.js the graded badge, search tokens paternity:<grade>, hover line, legend link, popover script tools/patch_evbadge.py

Prose tokens (each on its own line in the .md; the paragraph after a {{FIG:…}} token is its caption): {{SEEDLING_CARD}}, {{FIG:oh_ranked}}, {{FIG:flip_ranked}}, {{FIG:oh_hist}}, {{FIG:flip_hist}}, {{FIG:evidence_strip}}, {{FIG:atlas_vs_snp}}, and {{BADGE_TABLE}} (badge page; appended at the end when absent). A missing .md is built as a placeholder, so the build always runs. ?s=<seedling> highlights that seedling in every figure and fills the card; without it the page shows cohort-level views.

Build / rebuild on a stage (all idempotent; all take the atlas dir, so they re-apply to a re-staged page, e.g. after another session's cohort patch):

T=hop_atlas/tools; STG=/data/hop_data/lineage_refinement/stages/<name>/introgression_atlas
python3 $T/build_paternity_badges.py $STG     # vpg field (removes and rewrites all vpg; one-time backup *.bak_pre_evbadge)
python3 $T/patch_evbadge.py $STG             # hooks; aborts unless every anchor occurs exactly once; --check lists hooks
python3 $T/build_evidence_pages.py $STG      # pages + data; re-run whenever hop_atlas/evidence/*.md or the audit tables change
python3 $T/test_evidence_badges.py http://localhost:<port>/introgression_atlas/ $STG   # feature test (28 checks, incl. must-fail)

Adding a badge type: add it to BADGES in build_evidence_pages.py (catalogue row + anchor), to SELQ/typeOf in evidence_assets/evbadge.js, and describe it in hop_atlas/evidence/BADGES.md. A new evidence page = a new .md in hop_atlas/evidence/ (rendered as <name>.html); point the badge's popover link at it in evbadge.js.

Nav bar: "Badges" was added to www/nav.js on 2026-10-08 (www/nav.js is served live, so editing it changes :8900 immediately).

8. Subspecies / lineage evidence in the Evidence-detail hover boxes

What it is (PI request 2026-10-09). In the Evidence-detail panel (click a sample row), hovering a calling bin shows the NA/EU box (A only / B only / both, observed vs expected per state). When the haplotype-resolution selector is Subspecies (L1) or Lineage (L2), the same box now continues with the subspecies evidence for that bin, laid out the same way. Lineage view: the subspecies block, then the European-lineage block. At the NA/EU levels nothing changes. Deployed to :8900 on 2026-10-09 18:21 (stage /data/hop_data/lineage_refinement/stages/subspecies_evidence3; backups *.bak_2026-10-09_1821_subspecies_evidence3).

Box line Meaning From
call line most likely pair, posterior, informative loci, called / not called (posterior ≥ 0.95 and ≥ 3 loci); or forced (the parent call allows one state, e.g. NA/EU AA → lupulus / lupulus) the stored call file (tagspace/calls/<coh>_t4, calls/<coh>_v6)
candidates the states allowed by the parent layer's call (L1: NA/EU v6 call, or the atlas sex-aware X code on ChrX; L2: the SNP-layer subspecies call), or "all states" when the parent is not called caller nesting rule
observed / expected table informative loci with REF reads only / ALT reads only / both; expected counts of each class and the posterior for every candidate state (top 6; the rest summed) re-derived (below)
group table per group: mean ALT-allele frequency in the reference plants at these loci (plant left out, shrunk to the clade mean = what the caller used), reference plants per locus, loci whose reads show only that group's majority allele re-derived
forced bins the unconstrained evaluation of all states, labelled "for reference only (not used for the call)" re-derived
light red / faded note L1: "American, subspecies unresolved" = NA/EU has a B copy but the subspecies call is below threshold; L2: faded = subspecies shown where the lineage is unresolved E03 composite rule
ChrX L1: sex-aware; males haploid before their PAR boundary (only homozygous states possible; heterozygous expected = "impossible (haploid X)"); L2: lineages are not evaluated on ChrX caller
cohorts without reads Zhang 2017 and Pilgrim: row hover and detail panel say "no subspecies calls for this cohort (no reads)" subev.js

How the numbers are made. hop_atlas/tools/build_subspecies_evidence.py <atlas_dir> re-runs the callers' scoring (c01t_call_levels.py for L1, c01_call_levels_v6.py for L2) from the same sparse allele counts, catalogue, donor groups and settings (_run.json), and exports the intermediate quantities the calls were made from. It makes no new calls: every exported bin is compared with the stored call (state, pmax within 0.002, informative count), and the build writes checks_summary.tsv / checks_disagree.tsv next to the data. 2026-10-09 build: 35,504,311 bins, 2,468 disagree. All are uncalled L2 bins (posterior ≤ 0.469) with the same posterior and locus count, where states tie on posterior and the export (argsort kind stable) breaks the tie differently from the caller (default argsort). Two inputs changed after the calls were made; the builder uses the versions in effect on 2026-10-07: the plant-identity table (--identity, default sample_identity.tsv.bak_2026-10-08_saazsex) and the sex-chromosome JSON (the stage's served copy, which matches for these fields).

Path What
subev/ → /data/hop_data/lineage_refinement/viewer_subspecies_evidence_<stamp>/ <res>/<cohort>__<sample>.json.gz per sample and resolution; build.json (groups, states, run settings, check summary)
subev.js the box code (hop_atlas/tools/subev_assets/subev.js); wraps LIN.tip and LIN.tipPix for the no-reads row line
index.html hooks /*subev:1..4*/ script tag; detail hover; Zhang / Pilgrim detail-panel line (hop_atlas/tools/patch_subspecies_evidence.py, anchors asserted exactly once; idempotent; --check lists hooks)
T=hop_atlas/tools; STG=/data/hop_data/lineage_refinement/stages/<name>/introgression_atlas
python3 $T/build_subspecies_evidence.py $STG --nproc 4    # ~15 min for 9 cohorts; NEW data dir + $STG/subev symlink
python3 $T/patch_subspecies_evidence.py $STG              # hooks + subev.js
# re-staged page, same lineage/ data: reuse a finished export instead of rebuilding
python3 $T/build_subspecies_evidence.py $STG --link-existing /data/hop_data/lineage_refinement/viewer_subspecies_evidence_<stamp>

When to rebuild: after the call files change (re-calling, or a new cohort registered in the export: add it to COH in build_subspecies_evidence.py as for E03/E04), or after atlas_layers.py lineage changes the bin layout. Feature check (with must-fail checks): sync/2026-10-09/subspecies_evidence/check_subspecies_evidence.py <url> <atlas_dir> <figdir>.

8b. Experimental level "Subspecies + cordifolius" (L1c, 2026-10-09)

What it is. A 4th L1 reference group, cordifolius, made of 5 plants: kr|KR and carey2026 CORDI 2/4/7/8. - Groups table: sync/2026-10-09/cordifolius_lineage/tables/C00c_groups_cordifolius.tsv. C00 is untouched. - Wildcard parent *: cordifolius is allowed under any NA/EU call, its donors count at every bin, and it is its own clade for shrinkage. - Re-call: all 10 read cohorts into tagspace/calls/<coh>_t4c, using the caller fork sync/2026-10-09/cordifolius_lineage/scripts/c01tc_call_levels.py. With the original C00 groups the fork reproduces _t4: 0 / 307,034 bins differ. - Viewer: shows the existing Subspecies tile, overlaid with every CALLED cordifolius-containing state.

Rebuild on a stage (after lineage or the calls change):

S=sync/2026-10-09/cordifolius_lineage/scripts
python3 $S/e05_cordi_viewer_export.py <current lineage data dir> <NEW dir>; ln -sfn <NEW dir> $STG/lineage
python3 hop_atlas/tools/build_subspecies_evidence.py $STG --modes tagc --link-name subevc --out <NEW evidence dir>
python3 hop_atlas/tools/patch_cordifolius_level.py $STG    # lineage.js /*cordi:1..9*/, subev.js /*cordi:s1..s8*/

Re-apply after a re-copy. Re-run the patch whenever atlas_layers.py patch or patch_subspecies_evidence.py re-copies lineage.js or subev.js: the hooks live in the atlas copies, not in their sources.

When a new cohort is added to E03/E04, also add it to COH in build_subspecies_evidence.py and re-run e05 and the tagc build.

8c. Display order of tile halves (/*hphase*/, 2026-10-09)

What it is. A haplotype-resolution call is an unordered pair state, stored with its groups in alphabetical order. The viewer now orders the two halves for display only (no call or data file changes): 1. Cross parents: where the parent's phase lanes (phase/atlas_phase_<res>.json) are A/B with confidence a/l/m and the pair is one clade-A group (lupulus, European lineages) plus one clade-B group (lupuloides, neomexicanus, American unresolved), h1's group is drawn on top. 2. Everything else and the fallback (AA/BB lanes, unlinked u, no call, same-clade pairs, cordifolius): each group keeps the half it held in the previous called heterozygous bin of the chromosome (greedy left to right, deterministic).

Hover text gives the unordered pair; the legend notes the display ordering.

python3 hop_atlas/tools/patch_halves_phase.py $STG     # lineage.js /*hphase:1..6*/; anchors asserted exactly once; idempotent; --check

Re-apply after a re-copy. As with /*cordi*/, the hooks live in the atlas copy of lineage.js: re-run the patch whenever atlas_layers.py patch re-copies it. Order relative to patch_cordifolius_level.py does not matter (no shared anchors).

8d. Level "WGS subspecies (variants)" (Lw, /*wgsv*/, 2026-10-10)

What it is (PI request 2026-10-10). A haplotype-resolution option for WGS rows only, built from the genome-wide joint call of 30 WGS samples (/data/hop_data/wgs_subspecies_variants/joint/, record sync/2026-10-10/wgs_subspecies_variants/). Five reference groups built from WGS plants only (lupulus, lupuloides, neomexicanus, pubescens, cordifolius) + U; the CALL is the coarse pair state lupulus / American / cordifolius / U, and the most likely American subspecies is an unvalidated, faded guess shown only with "fill" ticked. Rows outside the joint call are pale grey ("WGS-only level"). Nothing in lineage/, subev/ or subevc/ changes.

Served path What Produced by
wgsv/ → /data2/hop_data/wgs_subspecies_variants/viewer_wgsv_<stamp>/ atlas_wgsv_<res>.json (level, strings samples / samples_fill, row-hover arrays ev), detail/<res>/<cohort>__<sample>.json.gz, calls/, export_summary.tsv sync/2026-10-10/wgs_subspecies_variants/scripts/b06_export.py
wgsv.js merge into the lineage JSON at load (levels.Lw, samples[k].Lw/Lwf), pale tiles, hovers, legend note hop_atlas/tools/wgsv_assets/wgsv.js
hooks /*wgsv:1..16*/ index.html script tag; lineage.js selector, decode (+ faded guess), str (fill), tile / pixel / hover / legend / detail / half-order / freq-track entries hop_atlas/tools/patch_wgsv_level.py (anchors asserted exactly once; idempotent; --check)
STG=/data/hop_data/lineage_refinement/stages/<name>/introgression_atlas
ln -sfn /data2/hop_data/wgs_subspecies_variants/viewer_wgsv_<stamp> $STG/wgsv
python3 hop_atlas/tools/patch_wgsv_level.py $STG       # re-apply after any re-copy of lineage.js / index.html

Rebuild after re-scoring (b03) or a new call rule: b06_export.py <model dir> <NEW dir>, re-point wgsv/, deploy. Codes are Greek letters (capitals = called coarse states, small = guesses), so they cannot collide with the ASCII codes of the other levels. Hook gotcha (2026-10-10): a hook marker inside a template literal is rendered as text (the first deploy showed "/wgsv:9/" in the legend); put markers inside a ${} expression. Also: never write /*marker*/ inside a JS block comment — it closes the comment.

8e. References and per-reference calls (/*frames*/, 2026-10-13)

What it is (PI request 2026-10-13). "Deprecate the old 'liftover' bins ... When we switch references, only display plants which have been aligned against that reference ... don't display references that have no bins/alignments/data." - The "Display coordinates" control is now Reference (?ref=<key>, default 21110M). It lists only references with calls on them, grouped by assembly (21110M · Saaz · 10-12 (HL1012) · Apollo · Cascade). Today 6: 21110M (served calls, all cohorts), Saaz hap1, 10-12 hap1, Apollo hap1, Apollo hap2, Cascade (126 Zenith x 21058M plants each: 124 progeny + both parents; Z_034/035/041/057 have no 09-25 BAMs). - Removed: the "Universal (consensus uMb)" option, the 14 lifted display frames, their drawing paths (per-frame placement of native bins, uMb axes, universal-frame branches) and the "Liftover coverage per assembly" table. The page no longer reads lifted geometry, so it was stripped from the served data (atlas_native_<res>.json per-bin g; atlas_data.json len / utile / notes.frame of every frame except 21110M, ulen, coverage). The universal bins and their 21110M placement stay: they carry the 21110M-only evidence tracks (haplotype rows, AIM, blocks, F segments, depth-class boundaries, terminal zones), and their hover tables still list the bin's coordinates in each assembly. - On 21110M the view is unchanged (canvas byte-identical to the served 21110M frame before the change, 5 Mb and 500 kb). - On any other reference frames.js swaps the viewer's data for that reference's calls: only the plants with calls on it, in its own calling bins and chromosome lengths. Sections, presets, Datasets drawer ("no calls on "), filters, sample search (": no calls on . It has calls on: ...") and ?sample= follow it. Layers without per-reference data are disabled (greyed, one-line note "available on 21110M only") and never drawn from 21110M data: haplotype rows, AIM, imputation no-call, breakpoint marks, phased parental lanes, switch-point guides, concordance badges, crossover ticks, transmission tracks, evidence tracks, terminal zones, sex-aware ChrX + ChrY, haplotype-resolution levels, sex / paternity badges, mixture-evidence and concordance filters, similarity sorts. Kept: the stacked summary and the per-panel mixture track (both counted from the drawn rows). - Hover on a non-21110M reference: call, sites with reads, posterior, A / B reads in that bin. Evidence detail: the per-frame run kept per-bin numbers only, so the panel says site-level detail exists on 21110M only. - ChrX on non-21110M references is drawn, labelled "ChrX (not sex-aware)": the per-frame caller uses one diploid model for every plant, and the 21110M re-call of ChrX agrees with the served sex-aware X in only 0.80 of 5,816 plant-bins (record §4a).

Registry frames/frames.json (17 entries): 21110M male composite; the 13 v5 panel haplotypes; Saaz hap1 (principal, PRJDB17944) and 10-12 hap1 (principal, PRJDB17947, + X/Y) as their own entries, because the per-frame saaz_h1 / hl1012_h1 references are NOT the panel Saaz / HL1012 sequences (4.25 M / 2.68 M substitutions; the panel ones are the pseudo-haploid deposits PRJDB17943 / PRJDB17946); Cascade (HopBase Dovetail). Panel Saaz / HL1012 (pseudo-haploid) are marked deprecated and are never offered (PI: hap1 / hap2 only for hop). Per entry: display name, group, hap, chromosome lengths in its own coordinates, bins per resolution (calling bins where calls exist, else nominal), cohorts with calls (samples, EM parameters, record), ChrX note. Per-frame key → registry: m21 → 21110M (the served calls are drawn; the re-call is not), apollo_h1 → Apollo_h1, apollo_h2 → Apollo_h2 (same sequences as the panel haplotypes, 0 substitutions), saaz_h1 → saaz_hap1, hl1012_h1 → hl1012_hap1, cascade → Cascade.

Served path What Produced by
frames/ → /data/hop_data/per_frame_ancestry/viewer_frames_<stamp>/ frames.json + <ref>/calls_<res>.json (bins; per sample x codes, ns sites, pm posterior x 1000, ra / rb reads) hop_atlas/tools/build_frames.py from annevo_eval/methods/per_frame_ancestry calls
frames.js selector, data swap, disabled layers, notes / hovers / search messages hop_atlas/tools/frames_assets/frames.js
index.html hooks /*frames*/ (40) selector + liftover removal + hooks; data strip hop_atlas/tools/patch_frames.py (anchors asserted; idempotent; --check)
STG=/data/hop_data/lineage_refinement/stages/<name>/introgression_atlas
python3 hop_atlas/tools/build_frames.py $STG     # NEW data dir + $STG/frames symlink (reads panel lengths from atlas_data.json,
                                                 # or from the previous frames.json once the data were stripped)
python3 hop_atlas/tools/patch_frames.py $STG     # hooks + frames.js + strip lifted geometry (re-apply after any re-copy of index.html)

Adding a cohort's per-frame calls: run the per-frame program for the cohort (annevo_eval/methods/per_frame_ancestry/README.md "Extending to other cohorts"; output <work>/frames/<frame>/calls/<cohort>_<res>_calls.tsv.gz + <cohort>_params.json), add the cohort to COHORTS in build_frames.py (atlas cohort key, per-frame cohort name, sample-name → atlas key rule, record), re-run both tools on a stage, deploy. A reference gets offered as soon as one sample has calls on it. Adding a reference: an entry in REFS (key, display, group, hap, pf = per-frame frame key). A re-export that rewrites atlas_native_* / atlas_data.json brings the lifted geometry back (harmless; re-run patch_frames.py to strip it). The test suites that read lifted geometry (e.g. xytruth.py) need updating before they are next run.

9. Change log

Display label of the Havill cross (2026-10-12). It is shown as "Zenith x 21058M"; the cohort key stays havill. Upstream exporters (m07/u14/o07-style scripts) still write "Havill cross (Zenith x 21058M)", so run python3 hop_atlas/tools/relabel_havill.py <stage>/introgression_atlas after any re-export, before deploying.