This document tracks changes to the codebase. Each entry should include a brief description of the change, the files affected, and any relevant context or reasoning behind the change. This helps maintain a clear history of modifications and facilitates collaboration among developers.
2026-07-29 π harmonia.py (v1.5.3) β fix HDF5 build-mixing bug in assign_chrpos_from_hdf5 (--add-chrpos)
Root cause: assign_chrpos_from_hdf5 used a glob pattern (*.chr*.rsID_CHR_POS_mod10.h5) that matched both hg19 (GCF_000001405.25) and hg38 (GCF_000001405.40) HDF5 files. When both builds are present in the reference directory (the normal production setup), chr_to_h5 was populated by last-wins over an unsorted filesystem glob β so each chromosome could end up pointing to either the hg19 or hg38 file, non-deterministically. For hg19 input (MVP studies), chromosomes that accidentally used the hg38 HDF5 received hg38 positions. Liftover then treated those hg38 positions as hg19 and re-shifted them, producing doubly-offset coordinates.
Example (CAD_EUR_MVP_withmultiallelic, input hg19 β liftover to hg38):
| variant | hg38 HDF5 position | liftover re-applies hg19βhg38 offset | harmonia output | dbSNP hg38 |
|---|---|---|---|---|
| rs17293632 (chr15) | 67,150,258 | β292,338 | 66,857,920 β | 67,150,258 |
| rs10510432 (chr3) | 14,823,860 | β41,507 | 14,782,353 β | 14,823,860 |
| rs60388387 (chr3) | 14,824,824 | β41,507 | 14,783,317 β | 14,824,824 |
| rs2925345 (chr15) | 41,019,601 | β292,198 | 40,727,403 β | 41,019,601 |
chr13 variants (rs9549621, rs1317507) were correct only because chr13 happened to get the hg19 file on that filesystem.
Fix: assign_chrpos_from_hdf5 gains a build parameter ("19" or "38"). When scanning HDF5 files, build-matched files (GCF_000001405.25 for hg19, GCF_000001405.40 for hg38) are always preferred over non-matching files. Files are iterated in sorted order (deterministic). A warning is emitted if any chromosome falls back to a non-matching build file. The call site passes input_build (the fixed input build, never overwritten by liftover) so the correct files are always selected.
Impact: all studies with --add-chrpos and a reference directory containing both builds (CAD_EUR_MVP, CAD_EUR_MVP_withmultiallelic, CAD_PAN_MVP, CAD_PAN_MVP_withmultiallelic). All must be fully rerun.
Files: harmonia.py.
Two bugs in the HDF5 rsIDβCHR:POS lookup that can cause some variants to receive wrong positions or remain unmapped after --add-chrpos.
Bug 1 β zip misalignment when HDF5 contains duplicate rsn entries
- Root cause: Inside
_lookup(),ref.set_index("rsn")["POS"]can produce a Series with a duplicated index when the dbSNP VCF stores the same rsID as multiple bi-allelic records (one per allele). Callingref_indexed.loc[rsn_array]on such a Series expands each duplicated entry, returning more values thanrsn_arrayhas elements. The subsequentzip(common_rsn.index, ..., pos_vals.values)stops at the shorter length, silently pairing every variant that follows the duplicate in the batch with the wrong position. - Example: if rsn A has two HDF5 rows (same POS, two alleles), and rsn B follows A in the batch, rsn B gets rsn A's second position instead of its own; rsn B's real position is dropped entirely.
- Fix:
ref.drop_duplicates(subset="rsn").set_index("rsn")["POS"]β deduplicate on rsID before building the index, guaranteeing.locreturns exactly one value per requested rsn. Safe because duplicate rsn rows always share the same POS (same variant, different allele); no positional information is lost. - Files:
harmonia.py(_lookupinner function ofassign_chrpos_from_hdf5).
Bug 2 β CHR restriction excludes chromosomes when input has mixed CHR/POS completeness
- Root cause:
needs_fillcaptures two populations β Group A (CHR known, POS missing) and Group B (both CHR and POS missing). The chromosome search list was derived from Group A's CHR values alone. When Group A and Group B coexist,chrs_to_searchis restricted to Group A's chromosomes; Group B variants on any other chromosome are never looked up and remain unmapped. - Example: if chr3 variants have CHR=3 but POS=NA (Group A) and chr13/chr15 variants have both missing (Group B), only the chr3 HDF5 is opened β chr13/chr15 variants are silently skipped.
- Fix: check whether ALL variants needing fill already have CHR (
has_chr.all()). If yes, restrict to those chromosomes (the original optimisation, still valid for pure Group A input). If any CHR is missing, search all chromosome files. For pure rsID-only input (no CHR column at all, the MVP case),has_chris all-False and behaviour is unchanged. - Files:
harmonia.py(assign_chrpos_from_hdf5).
2026-07-13 π§ harmonia.py (v1.5.1) β add column aliases for MetaGWAS/METAL fixed-effects output format
- Added
coded_alleleto EA aliases (existingcodedallelelacked the underscore variant). - Added
noncoded_alleleandnoncodedalleleto NEA aliases (previously unrecognised). - Added
coded_allele_freqandcoded_allele_frequencyto EAF aliases. - Covers MetaGWAS/METAL output files with columns
CODED_ALLELE,NONCODED_ALLELE,CODED_ALLELE_FREQ; the remaining fixed-effects columns (BETA_FIXED,SE_FIXED,P_FIXED,N_EFF) were already recognised. - Files:
harmonia.py.
- Root cause:
assign_chrpos_from_hdf5()inner_lookup()function usedref.index(a plainRangeIndex(0, N)) instead of thersncolumn values to check membership and retrieve positions.gl.process_vcf_to_hfd5()storesrsnas a regular DataFrame column, not as the index, sogrp_data["rsn"].isin(ref.index)was testing whether the integer rsID number fell within[0, N-1](the row count). For any rsID whose numeric value was smaller than the number of rows in a chromosome's shard group, the check returned a false positive;.loc[rsn_value, "POS"]then used that integer as a positional row label, returning the POS of a completely unrelated variant. - Symptom: rsID-only input files processed with
--add-chrposhad variants assigned to wrong chromosomes. Confirmed examples fromCAD_EUR_MVP_withmultiallelicandCAD_PAN_MVP:rs9549621β chr12:23614837 (correct: chr13:113633579) andrs1317507β chr3:2809124 (correct: chr13:113631780). The HDF5 reference data itself was correct; only the lookup logic was broken. - Fix: replaced
ref.indexmembership test and positional.locwithref.set_index("rsn")["POS"]so that rsn values are matched against actual rsn column entries. - Impact: only studies with
--add-chrposin EXTRA_FLAGS are affected. In this repo that isCAD_EUR_MVP,CAD_EUR_MVP_withmultiallelic,CAD_PAN_MVP, andCAD_PAN_MVP_withmultiallelic. All four must be fully rerun (preprocess parquets carry the wrong coordinates and cannot be patched). Rerun configs:gwas_list_cadmvp_rerun.txt(hg38) andgwas_list_cadmvp_rerun_b37.txt(hg19). - Files:
harmonia.py.
The project is now named Harmonia, after the Greek goddess of harmony and concord. The rename reflects the tool's expanded scope: it does far more than produce COJO-format files β it is a full GWAS summary-statistics harmonisation suite, standardising alleles and variant notation, applying multi-tier QC, and writing outputs in COJO, LDSC, Parquet, TSV.GZ, and Pickle formats.
Core rename:
gwas_process.pyβharmonia.py(git mv; tracked by git history)VERSION_NAMEupdated to"harmonia"βVERSIONbumped to1.5.0;VERSION_DATEto2026-07-10
All script and config files renamed (git mv) and content updated:
| Old name | New name |
|---|---|
gwas_process.py |
harmonia.py |
gwas_process.submit.sh |
harmonia.submit.sh |
gwas_process.submit_staged.sh |
harmonia.submit_staged.sh |
gwas_process.submit_staged_b37.sh |
harmonia.submit_staged_b37.sh |
gwas_process.array_for_submit.sh |
harmonia.array_for_submit.sh |
gwas_process.array_for_submit_b37.sh |
harmonia.array_for_submit_b37.sh |
gwas_process.cleanup.sh |
harmonia.cleanup.sh |
gwas_process.check.py |
harmonia.check.py |
gwas_process.download_refs.py |
harmonia.download_refs.py |
gwas2cojo.conf.example |
harmonia.conf.example |
Content changes throughout:
- All internal
gwas_process.*/gwas2cojo.conf/GWAS2COJO_CONFreferences updated toharmonia.*/harmonia.conf/HARMONIA_CONFin every script, Python file, utility script, andREADME.md environment.yml: conda envname:βharmonia;.gitignore:gwas2cojo.confβharmonia.confharmonia.py: output log file name<GWAS>.gwas_process.logβ<GWAS>.harmonia.logharmonia.check.py/harmonia.download_refs.py:VERSION_NAMEupdated
What does NOT change:
-
gwas2cojo.pyandgwas2cojo-verify.pyβ the original lightweight COJO-aligner scripts; kept as-is (distinct tools with own version history) -
Action required for existing installations: rename your local
gwas2cojo.conftoharmonia.confand updatePYTHON_SCRIPTinside it to point toharmonia.py -
Files: all files in the table above plus
environment.yml,.gitignore,README.md,utility_scripts/resubmit_merge.sh,utility_scripts/make_chrpos_hdf5.sh,utility_scripts/make_chrpos_hdf5.py,utility_scripts/gwas_get_sample_sizes.sh,CHANGES.md.
- Added
--base <DIR>argument that overrides theOUT_BASEpath set bygwas2cojo.conf. Useful when studies are stored in a subdirectory (e.g.b37/) that differs from the default output base. The flag is parsed after the conf is sourced, so it takes precedence over the conf value without requiring a separate config file. - Files:
harmonia.cleanup.sh.
- Added an INFO pre-filter at the preprocess stage (after column standardisation, before per-chromosome splitting). When an
INFOcolumn is present and has at least one finite value, variants withINFO < --info-minare removed immediately. NaN INFO values (genotyped variants without imputation quality scores) are kept at this step. The--info-minthreshold (default 0.4) was already applied at the QC stage; the new pre-filter fires unconditionally at preprocess time so low-quality imputed variants are dropped before the expensive SLURM array stages, reducing I/O and compute. Updated--info-minhelp text to document both application points. - Files:
gwas_process.py(v1.4.51 β v1.4.52).
- After moving SLURM
*.out/*.errfiles into<study_dir>/logs/, the cleanup script now compresses the directory tologs.tar.gzand removes the originallogs/folder. The compression step runs unconditionally on any existinglogs/directory (including pre-existing ones from a previous partial run), so it also fires when--no-archive-logsis passed.--dry-runreports the would-be compression without creating the archive. - Files:
harmonia.cleanup.sh.
- SE derivation strategy 4 β OR + P only (
correct_columns()): added a fourth SE-derivation path for files that containodds_ratioandp_valuebut nobeta, nostandard_error(or all-NaN), and no CI columns.correct_columns()runs beforecheck_or_vs_beta()(line 3251 vs 3253), so at the time SE derivation executes OR has not yet been converted to BETA β strategy 3 (BETA+P) cannot fire. Fix: when strategies 1β3 all fail butor_col_rawandp_colare both present, derivebeta = ln(OR)thenSE = |beta| / |Z|whereZ = Ξ¦β»ΒΉ(P/2). If an all-NaN SE column is present it is dropped first. Applies toMigraine_PAN_Choquet2021(OR + P; standard_error=NA; no CI). - Files:
gwas_process.py(v1.4.50 β v1.4.51).
2026-07-02 π utility_scripts β CRLF bug in awk column-hash; fix_chip_kessler / fix_migraine_choquet / fix_t1d_mcgrail / fix_bc_michailidou refactored
- Root cause (CRLF in harmonised .h.tsv.gz files): GWAS Catalog harmonised files compressed with Windows-style CRLF line endings. In each fix script,
awkreads the last header field as e.g.rsid\rand storesh["rsid\r"]=N. The subsequent$h["rsid"]lookup findsh["rsid"]=0, so$0(the entire input record) is printed in that field position, producing dozens of spurious columns in every output row. Fix applied to all four scripts:gsub(/\r/, "", $i)on each header field inNR==1, andgsub(/\r$/, "")on each data record. fix_chip_kessler.sh: last column wasrsidβ CRLF triggered$0dump.rsiddropped (re-assigned by--dbsnp);standard_erroralso dropped (always NA; CIβSE path used instead). Output header renamedSNPIDβname. Column order updated to match downstream expectations:name chromosome base_pair_location effect_allele other_allele effect_allele_frequency odds_ratio ci_upper ci_lower p_value num_cases num_controls(12 columns, was 13). Rerunfix_chip_kessler.shon HPC before resubmittingCHIP_EUR_Kessler2022.fix_migraine_choquet.sh: last column wasrsidβ CRLF triggered$0dump on thersidprint.rsiddropped;odds_ratiois now passed through as-is (no pre-conversion) β SE is derived by gwas_process.py v1.4.51 strategy 4 (OR+P).standard_error(all NA) retained in output; strategy 4 detects the all-NaN SE, drops it, then derives SE from OR+P.variant_idmoved to first column. Output:variant_id chromosome base_pair_location effect_allele other_allele odds_ratio standard_error effect_allele_frequency p_value(9 columns, was 8). Rerunfix_migraine_choquet.shbefore resubmittingMigraine_PAN_Choquet2021.fix_t1d_mcgrail.sh: last column wasvariant_idβ CRLF triggered$0dump on thevariant_idprint.variant_iddropped (formatchr_pos_ref_alt, not needed);rsidmoved to first column. Output:rsid chromosome base_pair_location effect_allele other_allele beta standard_error effect_allele_frequency p_value n(10 columns, was 11). Rerunfix_t1d_mcgrail.shbefore resubmittingT1D_EUR_McGrail2026.fix_bc_michailidou.sh: CRLF strip added for robustness.var_name(source SNPID column,chr_pos_ref_altformat) renamed toSNPIDin the output header so gwas_process.py recognises it via thesnpidalias.chr,position_b37,a0,a1kept verbatim (already recognised:chrβCHR,position_b37βPOS,a0βNEA,a1βEA). Stat column headers renamed to standard recognised aliases (effect_allele_frequencyβEAF,betaβBETA,standard_errorβSE,p_valueβP) regardless of sub-analysis β the awk hash still extracts the correct per-analysis source column (bcac_onco_icogs_gwas_*,bcac_onco_icogs_gwas_erpos_*, orbcac_onco_icogs_gwas_erneg_*). Rerunfix_bc_michailidou.shbefore resubmittingBC_EUR_Michailidou2017,BC_EUR_ERpos_Michailidou2017,BC_EUR_ERneg_Michailidou2017.- Files:
utility_scripts/fix_chip_kessler.sh,utility_scripts/fix_migraine_choquet.sh,utility_scripts/fix_t1d_mcgrail.sh,utility_scripts/fix_bc_michailidou.sh.
- EA alias priority (
SUMSTATS_ALIASES["ea"]):effect_allelenow appears beforealtin the alias list. GWAS Catalog harmonised files (.h.tsv.gz) contain both aneffect_allelecolumn (the true effect allele) and analtcolumn (the VCF REF/ALT alternative, which may differ from the effect allele). The previous ordering mappedaltβEAfirst, overwriting the correcteffect_allele. With botheffect_alleleandaltpresent and identical (common for SNPs) the error is silent; when they differ (e.g.RA_EUR/PAN_Verma2024whereeffect_allele=Aandalt=C), EA and NEA were set to the same VCF allele, causing gwaslab to remove 19.6 M of 19.7 M variants as EA==NEA. Fix: reordered aliases sohm_effect_alleleandeffect_alleleare matched first;altretained as a last-resort fallback with an explanatory comment. check_and_fill_eaf()NaN guard: before convertinggrp["pos"].min()andgrp["pos"].max()toint, the values are now checked forpd.isna(). If either is NaN (chromosome group has no valid positions), the loop skips that chromosome withcontinue. FixesValueError: cannot convert float NaN to integerinSCZ_PAN_Trubetskoy2022, which has 8 variants with missing POS in a chromosome group with EAF-missing rows.- Affected studies:
RA_EUR_Verma2024,RA_PAN_Verma2024(must rerun from--stage preprocessβ parquet has wrong EA baked in);SCZ_PAN_Trubetskoy2022(rerun from--stage preprocess). - Files:
gwas_process.py(v1.4.49 β v1.4.50).
2026-07-02 π§ utility_scripts β fix_bc_michailidou.sh drops malformed rsID column; fix_chip_kessler.sh refactored
fix_bc_michailidou.sh: thephase3_1kg_idcolumn (rsID) is dropped from all three output files (bc_all,bc_erpos,bc_erneg). The source file stores malformed values: either an rsID with appended coordinates (rs376342519:10616:CCGCCGTTGCAAAGGCGCGCCG:C) or a bare coordinate string when no rsID exists (1:11008:C:G). Either format causes gwaslab's rsID parser to fail and resulted in completed pipeline runs with zero output variants. Output is now 9 columns:SNPID CHR POS NEA EA EAF BETA SE P. rsIDs are re-assigned downstream via--dbsnpduringprocess-assign-rsid. Action: rerunfix_bc_michailidou.shon HPC, then resubmitBC_EUR_Michailidou2017,BC_EUR_ERpos_Michailidou2017,BC_EUR_ERneg_Michailidou2017from--stage preprocess.fix_chip_kessler.sh: unnecessary single-iterationforloop removed; restructured as direct file processing, consistent withfix_t1d_mcgrail.shandfix_migraine_choquet.sh. Behaviour unchanged.- Files:
utility_scripts/fix_bc_michailidou.sh,utility_scripts/fix_chip_kessler.sh.
- Root cause: dbSNP b157 VCFs for GRCh38 (
GCF_000001405.40.gz) have no##contigheader lines. gwaslab'sprocess_vcf_to_hfd5()auto-detects chromosome notation by scanning the header; with no contig lines it cannot distinguish hg38 from hg19 RefSeq IDs and falls back to the hg19 mapping (NC_000001.10β 1, etc.). bcftools then queries forNC_000001.10but the GRCh38 data records useNC_000001.11β 0 rows for every autosome. Only the mitochondrion (NC_012920.1, shared between builds) had data (9,229 rows). The hg19 VCF (GCF_000001405.25.gz) was unaffected because its data records genuinely use hg19 accessions. - Fix: added
_REFSEQ_HG38dict (25 entries:NC_000001.11β"1"β¦NC_012920.1β"25") tomake_chrpos_hdf5.py. Whenbuild == "hg38", this dict is passed aschr_dicttogl.process_vcf_to_hfd5(), overriding auto-detection.chr_dict=Nonefor hg19 preserves the existing (working) auto-detection path. - Action required: delete the empty hg38 HDF5 files (
GCF_000001405.40.chr*.rsID_CHR_POS_mod10.h5, all 280 bytes) and resubmitsbatch make_chrpos_hdf5.sh --build hg38. - Files:
utility_scripts/make_chrpos_hdf5.py.
fix_migraine_choquet.sh(new): Choquet2021 Migraine PAN harmonised file (GCST90000016.h.tsv.gz, GRCh38) hasstandard_error=NAthroughout with no CI columns. The SE derivation ingwas_process.pystrategies 1 and 2 both fail; strategy 3 (SE from BETA+P) requires BETA, not OR. This script pre-convertsodds_ratio β beta = log(OR)in awk and writes 8 key columns toGCST90000016.parsed.txt.gz. The SE is then derived at runtime via strategy 3. EAF is omitted and filled by--fill-eaf.- Activated in
gwas_list.txt(17 studies):CAD_EUR_MVPandCAD_PAN_MVP(MVP NatMed2022;--add-chrpos; withmultiallelic variants remain commented)AAA_Roychowdhury2023_PAN,TAA_MVP2023_PAN,IA_Bakker2020_PAN(β preprocess TIMEOUT note preserved β large files; may need extendedTIME_PREPROCESS)MDD_PAN_PGC2025(PGC Adams2025; β preprocess TIMEOUT note preserved)OSA_PAN_Verma2024,OSA_EUR_Verma2024(already pointed to.parsed.txt.gz)RA_PAN_Verma2024,RA_EUR_Verma2024(OR-based; CIβSE path handles missing SE)PrCa_PAN_Wang2023,PrCa_EUR_Wang2023(already pointed to.parsed.txt.gz)Endometriosis_EUR_PujolGualdo2025(N values verified: 233257/19588/213669)AoM_PAN_Kentistou2024,AoM_EUR_Kentistou2024(meta_effect_allele/meta_other_allelealiases added in v1.4.48)Migraine_PAN_Choquet2021(path changed from.h.tsv.gzβ.parsed.txt.gz; runfix_migraine_choquet.shon HPC first)CytokineNetwork_EUR_Nath2019(ZIP format supported since v1.4.46)
- Files:
utility_scripts/fix_migraine_choquet.sh(new),gwas_list.txt.
- BOLT-LMM P priority:
resolve_column()iterates aliases in list order; first match wins. With aliases orderedp_bolt_lmm β p_bolt_lmm_inf β p_linreg,P_BOLT_LMMis preferred when multiple BOLT-LMM P columns coexist in a file. Unmatched P columns remain in the DataFrame but are ignored downstream. - SNPID synthesis from CHR:POS (
standardise_columns()): when SNPID is missing from the source file after alias resolution but CHR and POS are already standardised, gwas_process.py now synthesisesSNPID = CHR:POS. This fixesPregnancy_EUR_Backman2021(GCST90085228, no variant ID column) and any future studies with the same pattern.Pregnancy_EUR_Backman2021activated ingwas_list.txt. - PGC VCF parsing scripts: four new utility scripts for studies distributed in PGC VCF format (##-prefixed metadata +
#CHROM-prefixed header line). Each script strips the VCF header, extracts needed columns, and writes.parsed.txt.gz.gwas_list.txtupdated to use parsed files for all five studies:fix_pgc_ptsd_nievergelt.sh: PTSD EUR + PAN (Z-score format; CHROM/ID/POS/A1/A2/FREQ/NEFF/Z/P)fix_pgc_scz_trubetskoy.sh: SCZ EUR + PAN (BETA/SE format; FCON renamed to EAF; NEFFDIV2Γ2βNEFF for PAN)fix_pgc_an_watson.sh: AN EUR Watson2019 (BETA/SE format; REF/ALT alleles; NEFFDIV2Γ2βNEFF)fix_t1d_mcgrail.sh: T1D McGrail2026 (harmonised TSV; extracts 11 of 13 columns to prevent OOM; memory 256Gβ128G)
- Files:
gwas_process.py(v1.4.48 β v1.4.49),utility_scripts/fix_pgc_ptsd_nievergelt.sh(new),utility_scripts/fix_pgc_scz_trubetskoy.sh(new),utility_scripts/fix_pgc_an_watson.sh(new),utility_scripts/fix_t1d_mcgrail.sh(new),gwas_list.txt.
- Root cause: loading all 27 columns of
GCST90165267.h.tsv.gz(UKB-scale, harmonised) caused OOM in the preprocess stage.standard_errorisNAfor all variants (Firth regression REGENIE output);ci_upper/ci_lowerare always populated and the existing CIβSE path incorrect_columns()handles them. - Fix:
utility_scripts/fix_chip_kessler.sh(new) β same awk-based approach asfix_osa_verma.sh; extracts 14 columns (SNPID fromname, rsid, chromosome, base_pair_location, effect_allele, other_allele, odds_ratio, standard_error, ci_upper, ci_lower, effect_allele_frequency, p_value, num_cases, num_controls) and writes.parsed.txt.gz. Memory requirement reduced from 256G β 128G for the HEAVY tier. - Files:
utility_scripts/fix_chip_kessler.sh(new),gwas_list.txt.
2026-07-01 π gwas_process.py β BOLT-LMM P aliases, AoM EA/NEA aliases, column whitespace strip; BC parsing script (v1.4.48)
- BOLT-LMM P-value aliases (
SUMSTATS_ALIASES["p"]+ two inlineresolve_columncalls incorrect_columns()): addedp_bolt_lmm,p_bolt_lmm_inf, andp_linreg. BOLT-LMM outputs these column names rather than the standardP; without aliasing, the LOY_EUR_Thompson2019 study (and any other BOLT-LMM run) failed at the P-value resolution step. - AoM / Kentistou2024 EA and NEA aliases: added
meta_effect_alleleβEAandmeta_other_alleleβNEAtoSUMSTATS_ALIASES. The Menarche2024 / AoM meta-analysis file usesMeta_effect_alleleandMeta_other_alleleas allele column names. - Column whitespace stripping (
main(), afterpd.read_csv()): addedgwas_data.columns = [c.strip() for c in gwas_data.columns]immediately after loading. Fixes the' chr'leading-space issue inAD_EUR_Wightman2021(PGCALZ2 file), where the first column header is stored with a leading space that preventedresolve_column()from matching thechralias. - BC parsing script (
utility_scripts/fix_bc_michailidou.sh): new awk-based script that splits the single multi-analysisoncoarray_bcac_public_release_oct17.txt.gzinto three files (bc_all,bc_erpos,bc_erneg) with standard column names (SNPID, rsID, CHR, POS, NEA, EA, EAF, BETA, SE, P). Run before submitting BC studies.gwas_list.txtupdated to replace the commented-out single BC entry with 3 active entries pointing to the parsed files. - Files:
gwas_process.py(v1.4.47 β v1.4.48),utility_scripts/fix_bc_michailidou.sh(new),gwas_list.txt.
- New flag
--output-build {19,38}ingwas_process.py: controls the target coordinate build for all pipeline output. When set to19and input data is in GRCh38, performs a reverse liftover (hg38βhg19) usinghg38ToHg19.over.chain.gzfrom--ref. Runs after the existing forward liftover block so it works whether or not--liftoveris also passed.build_num(used for file stems, VCF/FASTA selection, and checkpoints) is pre-adjusted inmain()so all stages use the correct build from the start. harmonia.array_for_submit_b37.sh(new): SLURM worker script for hg19 output. Removes--liftover(no forward hg19βhg38 step) and adds--output-build 19. Output directory is${OUT_BASE}/b37/${GWAS_NAME}.harmonia.submit_staged_b37.sh(new): staged SLURM submit script that chains all 8 stages using the b37 worker. Submission log and all stage outputs land under${OUT_BASE}/b37/. Job names are prefixedb37_to distinguish them from the hg38 pipeline.gwas_list_b37.txt(new): 13 completed studies (7 BUILD=38, 6 BUILD=19) active; 7 in-progress studies (PD, PrCaΓ2, OSAΓ2, CAD-MVPΓ2) commented out pending completion of current runs or HDF5 setup.- Files:
gwas_process.py(v1.4.46 β v1.4.47),harmonia.array_for_submit_b37.sh(new),harmonia.submit_staged_b37.sh(new),gwas_list_b37.txt(new).
- Root cause:
OSA_EUR_Verma2024,OSA_PAN_Verma2024,PrCa_EUR_Wang2023, andPrCa_PAN_Wang2023completed the pipeline but with near-zero variant output (915, 1,254, 3,590, and 4,183 variants respectively from inputs of 20β40M). For OSA:standard_erroris#NAfor all variants in the source file; without SE, all variants fail QC. For PrCa: indels dominate the file, producing ~6% checkref match rate and near-total variant loss. Pre-processing scripts (fix_osa_verma.sh,fix_prca_wang.shinutility_scripts/) extract only the necessary columns; for OSA,ci_upper/ci_lowerare passed through sogwas_process.py's existing CIβSE path incorrect_columns()derives SE automatically. - Fix: updated
gwas_list.txtinput paths from.h.tsv.gzβ.parsed.txt.gzfor all four studies. The.parsed.txt.gzfiles are generated by running the respectiveutility_scripts/fix_*.shbefore submitting. - Note: OSA files on the HPC lack the
.h.harmonisation prefix (plain.tsv.gz);fix_osa_verma.shalready uses the correct filenames. - Files:
gwas_list.txt.
- Root cause:
PD_EUR_Nalls2019was listed asBUILD=38with the comment "hg38; excludes 23andMe". The file's GWAS-significant hits on chr4 cluster at 90.6β90.8 Mb, matching the SNCA locus in GRCh37 (~90,645,250) rather than GRCh38 (~89,724,099). The incorrect build caused a ~50% checkref match rate (expected >90%) because coordinates were compared against the GRCh38 reference FASTA. - Fix: corrected
BUILDfield from38β37and updated the inline comment to "hg19/GRCh37; excludes 23andMe". The--liftoverflag is already part ofWORKER_FLAGSinharmonia.submit_staged.shand applies globally β no per-study EXTRA_FLAGS entry is needed. - Action required: delete prior
PD_EUR_Nalls2019output and rerun from--stage preprocess. The pipeline will now liftover hg19βhg38 before checkref, restoring >90% match rate. - Files:
gwas_list.txt.
- Root cause:
CytokineNetwork_EUR_Nath2019input file isMultivariateGWAS_CytokineNetwork_SummaryStatistics_GWASCatalog.zip. Thedetect_separator()function only handled.gzand plain text β callingopen()on a zip raisedFileNotFoundError(or binary garbage).pd.read_csv()supports zip natively, so onlydetect_separator()needed fixing. - Fix: added a
.zipbranch todetect_separator()that opens the archive withzipfile.ZipFile, lists members, logs a warning if there are multiple files, then reads the header line of the first member to sniff the delimiter. The rest of the pipeline (pandasread_csv) handles zip decompression transparently. - gwas_list.txt: corrected
CytokineNetwork_EUR_Nath2019path from.csvβ.zip. - Note: the column header of the zip's inner file is unknown until first run; the pipeline will log all detected columns at preprocess time.
- Files:
gwas_process.py(v1.4.45 β v1.4.46),gwas_list.txt.
2026-06-30 π gwas_process.py β CHR:POS SNPID extraction; BETA/SE from Z+EAF+N; BOLT-LMM aliases (v1.4.45)
- CHR:POS extraction from SNPID (
_extract_chrpos_from_snpid()): when SNPID is inchr1:226621487or1:226621487format and CHR/POS columns are absent, automatically extracts them. Detects format by checking β₯90% of SNPID values against a regex; handleschr-prefix and maps X/Y/MT to numeric codes. Called in bothcorrect_columns()(new preprocess runs) andmake_sumstats_object()(fallback for old parquets). FixesKeyError: 'POS'inremove_dupforPD_EUR_Nalls2019. - BETA/SE from Z-score + EAF + N (in
run_merge()): for Z-scoreβonly meta-analyses (e.g.AD_EUR_Wightman2021) that have no BETA or SE in the source file, derives them after EAF has been filled from the reference VCF at the checkaf stage. Formula:SE = 1/sqrt(2Β·EAFΒ·(1βEAF)Β·N),BETA = ZΒ·SE. This is the standard GWAS meta-analysis approximation. Enables COJO for Z-score studies where EAF and N are available. - BOLT-LMM aliases: added
allele0/allele_0βNEAanda1freq/a1_freq/freq_a1βEAF. FixesValueError: Failed to fix dtypes for requested columns: EAincheck_refforLOY_EUR_Thompson2019(BOLT-LMM output usesALLELE0for the non-effect allele andA1FREQfor effect-allele frequency). - Files:
gwas_process.py(v1.4.44 β v1.4.45).
- Root cause (aliases): three studies use non-standard coordinate/allele column names that were absent from
SUMSTATS_ALIASES: (1)PosGRCh37βPOSandtestedAlleleβEAforAD_EUR_Wightman2021(PGC-ALZ Z-score meta-analysis); (2)position_b37βPOS(missing β onlypos_b37was present) anda0βNEAforBC_EUR_Michailidou2017; (3) Z-score column (z) not recognised at all β only BETA was in scope. Additionallytestedallele(no underscore, camelCase) would not matchtested_allele(with underscore) under case-insensitive lookup. - Root cause (make_sumstats_object):
make_sumstats_object()built gwaslab kwargs by checkingif standard_name in gwas_data.columns. Old preprocess parquets saved under a version that lacked these aliases still have non-standard column names (e.g.PosGRCh37,testedAllele). The standard name (POS,EA) was therefore not found β kwarg not passed β gwaslab treated those columns as unrecognised "other" columns βremove_dupsubsequently crashed withKeyError: 'POS'when it tried to sort by coordinate. - Fix (aliases): added
position_b37,posgrch37,pos_grch37,position_grch37to POS aliases;testedalleleto EA aliases;a0to NEA aliases; new"z"entry toSUMSTATS_ALIASEScoveringz,zscore,z_score,zs,z_stat,tstatetc. - Fix (make_sumstats_object): changed kwarg construction to fall back to alias resolution when the standard name is absent β handles old parquets without requiring preprocess rerun.
- Remaining issues: (1)
AD_EUR_Wightman2021is a Z-scoreβonly study (no BETA/SE/OR in the source file); COJO will be skipped because BETA+SE cannot be derived at preprocess time without EAF. LDSC will work (gwaslab can use Z directly). A future enhancement could derive BETA/SE from ZΓEAF after the fill-eaf step. (2)BC_EUR_Michailidou2017uses study-specific column names for BETA (bcac_onco_icogs_gwas_beta), SE, and P that are not matchable by generic aliases β requires per-study column configuration (not yet implemented). (3)PD_EUR_Nalls2019uses SNPID-as-CHR:POS (chr1:226621487) with no separate CHR/POS columns; gwaslab's SNPID check does not auto-extract them. - Files:
gwas_process.py(v1.4.43 β v1.4.44).
2026-06-30 π gwas_process.py β P=0 values crash normalize stage with FloatingPointError (v1.4.43)
- Root cause: gwaslab's
remove_dup()internally converts P-values to-log10(P)to sort duplicates. When a study contains exact P=0 values (e.g.AD_EUR_Wightman2021has 40 such variants),np.log10(0)raises a hardFloatingPointError: divide by zeroand the pipeline aborts. Affects any study where the source file encodes genome-wide-significant associations as P=0 rather than as very small floats. Observed in:AD_EUR_Wightman2021, likelyBC_EUR_Michailidou2017andPD_EUR_Nalls2019(same failure stage). - Fix: before each
remove_dup()call (two call sites:run_normalize()andrun_preprocess_normalize()), clamp anyP==0tonp.finfo(float).tiny(β 2.23e-308). Logs a WARNING with the count. This preserves the variants (they represent maximally significant associations) while preventing the log10 crash. - Files:
gwas_process.py(v1.4.42 β v1.4.43).
2026-06-30 π gwas_process.py / gwas_check_cojoldsc_output.sh β LDSC output file double-named .ldsc.ldsc.tsv.gz (v1.4.42)
- Root cause:
write_ldsc()builtout_pathas{stem}.qc.ldsc(manually appending.ldscto label the file type), then passed it to gwaslab'sto_format(fmt="ldsc"). gwaslab itself appends.ldsc.tsv.gzto whatever path it receives, producing{stem}.qc.ldsc.ldsc.tsv.gzβ a redundant double.ldsc. All studies in_finished_2026have this double-suffix. New runs after this fix will produce{stem}.qc.ldsc.tsv.gz. - Fix (gwas_process.py): removed the trailing
.ldscfromout_pathinwrite_ldsc()so gwaslab's own suffix is the sole source of the.ldsclabel. - Fix (gwas_check_cojoldsc_output.sh): the LDSC file glob was
*.qc.ldsc.tsv.gzwhich never matched the double-suffixed files (hence all LDSC counts showed MISS). Broadened to*.ldsc.tsv.gzso it matches both old (*.qc.ldsc.ldsc.tsv.gz) and new (*.qc.ldsc.tsv.gz) naming conventions. - Files:
gwas_process.py(v1.4.41 β v1.4.42),utility_scripts/gwas_check_cojoldsc_output.sh.
- Root cause: the CI-based SE derivation added in v1.4.40 only recognised a subset of common CI column names (
ci_upper,ci_lower,upper_ci,lower_ci,ci.upper,ci.lower,ci_95_upper,ci_95_lower,95%ci_upper,95%ci_lower). Common alternatives such ashighCI/lowCI,high_ci/low_ci,ci_high/ci_low,or_upper/or_lower,or_upper_95ci/or_lower_95ci, andconf_upper/conf_lowerwere absent. - Fix: added the missing aliases to both CI resolver calls in
correct_columns().resolve_column()is case-insensitive, sohighCI,HighCI, andHIGHCIall match the"highci"entry. - Files:
gwas_process.py(v1.4.40 β v1.4.41).
2026-06-30 π gwas_process.py β SE derived from 95% CI; OR-based studies now supported end-to-end (v1.4.40)
- Root cause: GWAS Catalog harmonised files for OR-based studies (e.g.
OSA_PAN_Verma2024,RA_EUR_Verma2024) provideodds_ratio,ci_upper,ci_lower, andstandard_error=NA. Three gaps prevented processing: (1)odds_rationot in any alias β never renamed toORβcheck_or_vs_beta()was a no-op; (2)standard_errorall-NA with no CI fallback β SE unavailable β COJO impossible; (3)num_cases/num_controlsnot in N-derivation alias lists β case/control N breakdown missed. - Fix 1: Added
"OR": ["odds_ratio", "or"]toOPTIONAL_OTHER_ALIASESsostandardise_columns()renamesodds_ratioβOR, whichcheck_or_vs_beta()then converts toBETA = ln(OR). - Fix 2: Added SE derivation from 95% CI as the priority fallback in
correct_columns()(before the existing beta+p back-calculation). Formula:SE(log OR) = (ln(ci_upper) β ln(ci_lower)) / 3.92when an OR column is detected;SE = (ci_upper β ci_lower) / 3.92for beta-scale CI. Verified against Verma2024 data: p-value reproduced to 4 d.p. - Fix 3: Added
num_cases/num_controlsto N-derivation alias lists incorrect_columns()and toOPTIONAL_OTHER_ALIASES. - Affected studies:
OSA_PAN_Verma2024,OSA_EUR_Verma2024,RA_PAN_Verma2024,RA_EUR_Verma2024; also covers any future OR-based GWAS Catalog harmonised study with CI columns. - Action required: uncomment studies in
gwas_list.txt, delete any prior output, and run from--stage preprocess. - Files:
gwas_process.py(v1.4.39 β v1.4.40).
2026-06-30 π gwas_process.py β COJO skipped for PGC daner-format studies: N dropped by gwaslab harmonise() (v1.4.39)
- Root cause:
correct_columns()at preprocess correctly derivesN = Nca + Ncoand saves it to preprocess.parquet. However, gwaslab'sharmonize()in the process-normalize stage drops theNcolumn while keepingN_casesandN_controlsas pass-through extra columns. All subsequent per-chromosome stages (checkref β inferstrand β assignrsid β checkaf) and the merge stage therefore have noN.write_cojo()guards on"N" not in df.columnsand silently returns β no COJO written. Confirmed onANX_EUR_Strom2026: normalize.pkl contained['SNPID', 'NEA', 'INFO', 'ngt', 'Direction', 'N_cases', 'N_controls', 'Neff_half']β N_cases and N_controls present, N absent. - Fix: in
run_merge(), immediately aftermake_sumstats_from_chrom_df()creates the merged gwaslab object, re-deriveN = N_cases + N_controlswhen N is absent but both components are present. This is a no-op when N survived normalisation (continuous-trait studies) or when--force-nwas used. - Affected studies: all PGC daner-format studies with per-variant Nca/Nco columns:
ANX_EUR_Strom2026,BIP_EUR_OConnell2025,BIP_PAN_OConnell2025, and any similar future study. - Action required: these studies do NOT need preprocess rerun β the per-chromosome checkaf parquets are intact and N_cases/N_controls are present. Resubmit only the merge stage:
--stage merge(or--stage processif merge isn't a standalone stage). - Files:
gwas_process.py(v1.4.38 β v1.4.39).
- Root cause:
COLUMN_ALIASES["beta"]andcorrect_columns()beta_colresolver did not includestdbetaorstd_beta. The Savage 2018 IQ GWAS (IQ_EUR_Savage2018) usesstdBeta(standardised beta in SD units) as its effect-size column. Without a matching alias, gwaslab standardisation and the COJO writer both failed to find a BETA column β COJO skipped. LDSC still worked because the pipeline filled EAF via VCF lookup and used theZscorecolumn directly for chi-square computation. - Fix: added
stdbetaandstd_betato both alias locations. - Affected study:
IQ_EUR_Savage2018; also covers any future study using standardised-beta nomenclature. - Action required: rerun from
--stage preprocessso the new alias is applied at load time. - Files:
gwas_process.py(v1.4.37 β v1.4.38).
2026-06-30 π gwas_process.py β fill_eaf missing aliases for GWAS Catalog harmonised column names (v1.4.37)
- Root cause:
check_and_fill_eaf()runs beforestandardise_columns(), so it sees raw source column names rather than standardised ones. Three alias gaps were found:- DIAMANTE T2D sumstat uses
chromosome(b37)/position(b37)β absent from chrom/pos alias lists βCHR=None, POS=Noneβ fill_eaf skipped entirely. - GWAS Catalog harmonised files (
.h.tsv.gz) usehm_chrom/hm_posas the authoritative harmonised coordinate columns β not in alias lists (resolved to the equivalentchromosome/base_pair_locationfallbacks by coincidence, buthm_chrom/hm_posshould be preferred). - Harmonised EAF column
hm_effect_allele_frequencynot in the localEAF_ALIASESlist β onlyeffect_allele_frequency(the un-harmonised original) was detected; for ALS/Asthma/RA/etc. both are all-NA, but ordering preference matters for future studies.
- DIAMANTE T2D sumstat uses
- Fix: added to
check_and_fill_eafalias lists:hm_chrom,hm_pos(first priority),chromosome(b37),chromosome(b38),position(b37),position(b38); addedhm_effect_allele_frequency(first priority inEAF_ALIASES). - Affected studies:
T2D_PAN_Mahajan2022(chromosome(b37) fix); all GWAS Catalog.h.tsv.gzstudies benefit fromhm_chrom/hm_pos/hm_effect_allele_frequencybeing explicitly recognised. - Action required: delete preprocess checkpoint and rerun
--stage preprocessfor affected studies. - Files:
gwas_process.py(v1.4.36 β v1.4.37).
2026-06-30 π gwas_process.py β fill_eaf returns 0 matches for hg38 studies due to chr-prefix mismatch (v1.4.36)
- Root cause:
check_and_fill_eaf()passed the chromosome string from the GWAS data directly totabix.fetch()without normalising it to match the VCF's contig naming convention. The 1KG 30x hg38 VCF useschr1,chr2, β¦ contig names (GRCh38 standard), but GWAS Catalog harmonised files (.h.tsv.gz) use bare numbers1,2, β¦. Everytabix.fetch("1", β¦)call raisedValueError(contig not found), which was silently caught withexcept ValueError: pass, so all chromosomes yielded 0 lookups. Studies that already hadhm_effect_allele_frequencyin the source were unaffected (they returned early before the fetch loop). - Fix: detect the VCF contig naming convention once from
tbx.contigs(vcf_uses_chr_prefix), then normalise each chromosome string before the tabix fetch β prependingchrwhen the VCF is prefixed and the data is not, or stripping it in the reverse case. - Affected studies: any study on hg38 where the source file lacks an EAF column:
ALS_PAN_Rheenen2021,Asthma_PAN_Demenais2017,CRP_EUR_Said2022,Psoriasis_EUR_Dand2025,RA_EUR_Ishigaki2022,RA_PAN_Ishigaki2022,UKBB_LPa_PAN_Sinnot-Armstrong2021. (8th studyT2D_PAN_Mahajan2022is hg19 and may have a separate issue.) - Action required: delete preprocess output and rerun
--stage preprocessfor each affected study so fill_eaf re-executes with the corrected chromosome normalisation. LDSC should then produce variant counts comparable to other studies. - Files:
gwas_process.py(v1.4.35 β v1.4.36).
2026-06-23 π gwas_process.py β SE back-calculation skipped when SE column exists but is all-NaN (v1.4.35)
- Root cause:
correct_columns()checkedif se_col is not Noneto decide whether to skip the SE back-calculation from beta + p-value. Studies such asMigraine_PAN_Choquet2021have astandard_errorcolumn present in the harmonised file but all values areNAβ sose_colis not None but the column is entirely useless. - Fix: added an
se_all_nanguard: ifse_colexists butgwas_data[se_col].isna().all(), the all-NaN column is dropped before back-calculatingSE = |Ξ²| / |Ξ¦β»ΒΉ(p/2)|. Dropping is necessary becausestandardise_columns()runs immediately after and would renamestandard_errorβSE, overwriting the back-calculated values. - Affected studies:
Migraine_PAN_Choquet2021(SE all-NaN in source); any harmonised GWAS Catalog file that includes astandard_errorcolumn with all-NA values. - Files:
gwas_process.py(v1.4.34 β v1.4.35).
- Root cause:
COLUMN_ALIASES["n"]and theresolve_columncall incorrect_columns()(merge-step N derivation) did not includen_total_sum(used by Wuttke 2019 eGFR files:EGFRcrea_PAN_Wuttke2019,EGFRcrea_EUR_Wuttke2019) orN_analyzed(used by Savage 2018 IQ:IQ_EUR_Savage2018). Both studies were processed without a sample-size column β--cojowas skipped at runtime. - Fix: added
n_total_sumandn_analyzedto both alias lists.Nca/Nco(ANX_EUR_Strom2026) were already present inOPTIONAL_OTHER_ALIASESvianca/ncoaliases and required no change. - Action required: rerun from
--stage preprocessfor the three affected studies once gwas_list.txt N values are confirmed so the new aliases are applied at load time. - Files:
gwas_process.py(v1.4.33 β v1.4.34).
- Root cause: the LDSC file glob was
*.ldsc.ldsc.tsv.gz(doubledldscinfix) but the actual output filename pattern written bywrite_ldsc()is*.qc.ldsc.tsv.gz. - Fix: corrected glob to
*.qc.ldsc.tsv.gz. - Files:
utility_scripts/gwas_check_cojoldsc_output.sh.
2026-06-11 π§ utility_scripts/download_dbsnp_vcfs.sh β SLURM job to download all dbSNP VCF reference files
- New script that downloads (or resumes) all three dbSNP VCF files needed by the pipeline: b157 hg38 (
GCF_000001405.40.gz), b151 hg38 (00-All.vcf.gz, the current fallback), and b157 hg19 (GCF_000001405.25.gz) - Uses
wget --continue --tries=10 --read-timeout=120to safely resume partial downloads (avoids the 20-second timeout that caused the original truncated downloads via gwaslab) - Verifies BGZF integrity with
bgzip -tafter each download; aborts on failure - Rebuilds
.tbiwithtabix -p vcf(never downloads the index β avoids the b156/b157 mismatch in gwaslab'sreference.json) - Sources
gwas2cojo.confautomatically; no hardcoded paths - Submit with:
sbatch utility_scripts/download_dbsnp_vcfs.sh - Files:
utility_scripts/download_dbsnp_vcfs.sh(new)
2026-06-11 π§ harmonia.submit_staged.sh β TIME_PREPROCESS/MEM_PREPROCESS now overridable via env var
MEM_PREPROCESSandTIME_PREPROCESSare now set via${VAR:-default}so they can be overridden from the environment without editing the script- Example:
TIME_PREPROCESS="08:00:00" bash harmonia.submit_staged.sh gwas_list_resubmit_preprocess_timeout.txt - Needed for large files (AAA_Roychowdhury2023_PAN, IA_Bakker2020_PAN, MDD_PAN_PGC2025) that exceed the default 4-hour preprocess time limit
- Files:
harmonia.submit_staged.sh
2026-06-11 π gwas_process.py β dbsnp_vcf_path: self-healing fallback for broken b157 dbSNP index (v1.4.33)
- Root cause:
dbsnp_vcf_path()returnedGCF_000001405.40.gz(dbSNP b157 hg38) for the rsID-assignment step (process-assign-rsid). Investigation revealed two compounding problems with that file on the HPC: (1) the download was truncated β only a fraction of chromosome 1 was downloaded (1.8 GB of an expected ~10β15 GB), confirmed bytabix -lreturning onlyNC_000001.11; (2) the.tbiindex was sourced from the b156 archive (a bug in gwaslab'sreference.json), so the block offsets do not match the b157 VCF. Every bcftools query via_extract_lookup_table_from_vcf_bcf()returned 0 rows. Consequence:sweep_mode=Trueinharmonize()assigned zero rsIDs to all studies that lacked pre-existing rsIDs in their SNPID column, capping LDSC variant counts at ~84k (only variants with rsIDs already present in the source file). - Additional context: gwaslab attempted to pre-process the GCF files into HDF5 lookup shards on 2026-04-04; those runs logged
Total rows processed: 0for the same reason. The HDF5 shards are empty but irrelevant to the CHR:POSβrsID assignment path used by this pipeline. - Fix:
dbsnp_vcf_path()now checks whether the b157.tbiis valid (size > 1 MB β the broken b156-sourced index was only 207 KB, a correct full-build index is several MB). If valid, b157 is used as before. If not, it falls back to00-All.vcf.gz(dbSNP b151 hg38, standard1/2/.../X/Y/MTchromosome names, correct 2.7 MB.tbi). This is self-healing: once a proper b157 VCF is downloaded and indexed withtabix -p vcf, the function automatically returns the b157 path without any further code change. b151 covers all HapMap3 / common SNPs; b157 additionally covers TOPMed-era rare variants (rsIDs added in builds b154βb157). - hg19:
GCF_000001405.25.gz(b157 hg19, 797 MB) has the same index mismatch and will yield 0 assignments until re-downloaded and re-indexed. No hg19 studies are currently in the active queue. - Action required: (1) download full b157 VCFs for hg38 and hg19 as a SLURM job (see below); (2) deploy v1.4.33 to HPC immediately β the fallback to b151 means the pipeline can run now; (3) rerun
process-assign-rsidβprocess-check-afβprocess-mergefor all 52 β LDSC studies using b151 (sufficient for LDSC); (4) after b157 downloads complete, rerun the same 52 studies again to get full b157 rsID coverage for COJO completeness. - Files:
gwas_process.py(v1.4.32 β v1.4.33).
2026-06-11 β¨ utility_scripts/gwas_check_cojoldsc_output.sh β new script to check COJO and LDSC output files
- New utility:
gwas_check_cojoldsc_output.shchecks the presence and variant counts of COJO (*.cojo/*.cojo.gz) and LDSC (*.ldsc.ldsc.tsv.gz) output files for one or more gwas2cojo studies. - Usage: accepts study names as positional arguments or via
--list FILE;--base DIRoverrides the default base directory (/hpc/dhl_ec/data/_gwas_datasets/gwas2cojo). - Output: aligned table with
COJO_NandLDSC_Nvariant counts per study; LDSC counts flagged β (< 100k, suspicious) or β (< 1k, critically low); summary footer with per-category totals. - Files:
utility_scripts/gwas_check_cojoldsc_output.sh(new).
2026-06-11 π gwas_process.py β EAF lookup: normalise allele case before reference VCF matching (v1.4.32)
- Root cause:
check_and_fill_eaf()built its(pos, ref, alt)lookup dict directly from the reference VCF (uppercase alleles: A/T/C/G) and then queried it using allele strings taken verbatim from the GWAS data. Older meta-analysis files (e.g. AholaOlli2017 cytokine GWAS) store alleles in lowercase (a/t/c/g). Python dict lookups are case-sensitive, so every query returnedNoneβ 0 out of ~9.9M EAF values were filled despite 9.8M rsIDs being present and the reference VCF chromosome names matching correctly (confirmed viatabix -l). - Diagnosis: cross-referencing the
check_and_fill_eaf()dict key construction (fields[3],fields[4]from the VCF β always uppercase) against the per-row lookup (str(row["ea"]),str(row["nea"])β lowercase in affected files) confirmed a complete case mismatch. Chromosome-name format was ruled out as a secondary cause because the same reference VCF works correctly for other studies. - Fix: two
.str.upper()calls immediately after column renaming, before the per-chromosome loop β so all allele comparisons are uppercase-normalised regardless of how the source file encodes them. - Affected studies: IL6_EUR_AholaOlli2017 (confirmed 0/9,901,590 EAF found); any study whose source file uses lowercase alleles will benefit from a rerun after this fix. Studies with uppercase alleles in source are unaffected.
- Files:
gwas_process.py(v1.4.31 β v1.4.32).
2026-04-05 π gwas_process.py β check_ref: prefer uncompressed FASTA to avoid pyfaidx OOM (v1.4.31)
- Root cause:
run_check_ref()always usedhg{build}.fa.gz(plain gzip) as the FASTA reference. pyfaidx cannot perform random-access on plain-gzip files β it decompresses and indexes the entire genome into RAM, which for hg38 (~3.2 billion bases as a Python in-memory structure) can exceed 100 GB regardless of variant count. Studies using hg38 as the target build (BUILD=38, or BUILD=19+liftover) hit this limit duringprocess-check-refeven with only hundreds of thousands of per-chromosome variants. - Fix: both the per-chromosome path (
run_check_ref()) and the legacy whole-genome path now preferhg{build}.fa(uncompressed) overhg{build}.fa.gz. With the uncompressed FASTA and its.faiindex, pyfaidx uses true O(1) random access with memory proportional only to the variants being processed. pyfaidx creates the.faiautomatically on first use if absent (one-time cost)..fa.gzis retained as a fallback with a warning. - Action required: ensure
samtools faidx hg38.fahas been run in REF_DIR so the.faiindex exists (or let gwaslab/pyfaidx build it on first run). The uncompressedhg38.fais already present in the reference directory alongsidehg38.fa.gz. - Affected studies: any study using the hg38 FASTA for check_ref, including all BUILD=19+liftover studies (IL6, CAD, HF, NICM, etc.) and BUILD=38 studies (CRP, Migraine, T1D).
- Files:
gwas_process.py(v1.4.30 β v1.4.31).
- Bug:
write_ldsc()appliedEAF > 0.01 & EAF < 0.99even when EAF was all-NaN, silently removing every variant and writing a useless 0-variant LDSC file. Same root cause as theapply_qc()EAF/DAF bug (v1.4.29) β NaN comparisons always return False in pandas. - Different fix from apply_qc(): for LDSC, EAF is genuinely required (it becomes the
Frqcolumn). Skipping the filter would produce an LDSC file with all-NaN frequencies, which is equally useless. Instead,write_ldsc()now detects all-NaN or absent EAF up-front, logs an ERROR explaining the cause and remediation (--fill-eaf), and returns without writing a file. - Studies affected:
HF_EUR_Aragam2018andNICM_EUR_Aragam2018used--no-fill-eafand have no EAF in the source β will now clearly log the reason instead of writing an empty LDSC file.T1D_EUR_Chiou2021had wrong BUILD (19 instead of 38 for a GWAS Catalog harmonised file) β double liftover corrupted coordinates β EAF fill failed β near-0 LDSC variants; fix is BUILD=38 + full rerun. - Files:
gwas_process.py(v1.4.29 β v1.4.30).
- Bug 1 (EAF):
build_qc_filter_expr()always included the EAF filter regardless of whether EAF had any non-NaN values. In pandas,NaN >= 0.005evaluates toFalse, so an all-NaN EAF column caused every variant to fail the filter. All other filter terms (BETA, SE, INFO, DAF) were already guarded withif col in cols else None; EAF was not. - Bug 2 (DAF):
apply_qc()passedelse 0(notelse None) when DAF column was absent, which would generate the impossible expressionDAF < 0 & DAF > 0had the column not existed. Now passesNone. - Root cause for CRP_EUR_Said2022: EAF is mostly/entirely NaN in the harmonised input file and EAF fill could not recover it. The all-NaN EAF caused both the EAF filter and the DAF filter (DAF is derived from EAF) to remove all 10.6M variants.
- Fix:
apply_qc()uses_col_usable(col)β column present AND has at least one non-NaN value β as the guard for every numeric filter (EAF, DAF, BETA, SE, INFO).build_qc_filter_expr()now acceptseaf: float | Noneand returnsNonewhen no usable filter criterion exists (all columns absent or all-NaN). - New: when
exprisNone(no usable filters), all variants are retained with a WARNING. When QC removes 100% of variants, an ERROR is logged explicitly pointing to EAF/DAF as the likely culprit. - Files:
gwas_process.py(v1.4.28 β v1.4.29).
2026-04-04 β¨ gwas_process.py β OR + 95% CI columns in TSV output for case/control studies (v1.4.28)
- New:
reformat_output()now detects case/control studies (N_cases present and non-zero) and appends three derived columns immediately after SE and P in the output TSV:OR= exp(Beta)OR_lower_95CI= exp(Beta β 1.96 Γ SE)OR_upper_95CI= exp(Beta + 1.96 Γ SE)
- Because ORβBETA conversion happens at preprocess time (v1.4.27),
Betain all output files is already ln(OR), so these back-transformations are exact. - Applies to both raw pre-QC (
.tsv.gz) and QC-filtered (.qc.tsv.gz) outputs. COJO and LDSC outputs are unaffected (they require Beta in linear scale by design). Parquet/pickle internal formats are also unaffected. - For quantitative traits (no N_cases) the output is unchanged.
- Files:
gwas_process.py(v1.4.27 β v1.4.28).
2026-04-04 π gwas_process.py β ORβBETA conversion at preprocess time (v1.4.27 supersedes v1.4.26)
- Root cause identified:
check_or_vs_beta()(called during preprocess) only handled the mislabelled OR case (negative values β rename OR to BETA). When OR values were genuinely positive it logged "OR column looks valid" and left the column asOR. gwaslab preservesORas-is through normalize β checkref β inferstrand β checkaf β merge, soBETAwas never populated, andwrite_cojo()rightly skipped output. - Fix (
check_or_vs_beta()): genuine positive OR is now converted toBETA = ln(OR)and theORcolumn is dropped immediately at preprocess time. This means BETA flows correctly through the entire pipeline (gwaslab'sflip_allele_stats()negates BETA, which is mathematically identical to taking ln(1/OR) = βln(OR)), and COJO/LDSC/raw outputs all work without special casing. - Safety net retained (
write_cojo()): the ORβBETA fallback added in v1.4.26 remains in place as a guard for edge cases where OR survives to merge (e.g. old checkpoints written before this fix). - Affected studies: any case/control study where the source file stores odds ratios (PGC iPSYCH ASD, PGC BIP OConnell2025, and likely ICH, AAA, TAA, IA, Migraine). Studies with
.withBETA_Nfiles (BIP_EUR_Stahl2019, CD/IBD/UC Liu2015) and UKBB Neale files (which publish log-OR as BETA) are unaffected. - Files:
gwas_process.py(v1.4.26 β v1.4.27).
- Bug:
write_cojo()required aBETAcolumn and silently skipped COJO output when the study used odds ratios (OR). Affected OR-based case/control studies (e.g. ASD_EUR_Grove2019, BIP_EUR) which had noBETAcolumn after gwaslab processing. - Fix: if
BETAis absent butORis present,write_cojo()now derivesBETA = ln(OR)on a working copy of the DataFrame before building the COJO table. Variants withOR β€ 0or non-finite OR are set toNaNand filtered out with a warning. - Safety: a non-finite filter (
np.isfinite(BETA) & np.isfinite(SE)) is applied to the COJO output regardless of whether ORβBETA conversion was performed, guarding against any upstream data issues. - Files:
gwas_process.py(v1.4.25 β v1.4.26).
- Bug:
run_infer_ancestry()used gwaslab's key-based file lookup (1kg_hm3_hg38_eaf) without first telling gwaslab where to look. gwaslab searches its own default cache; if the file was placed inREF_DIRmanually (or via our download helper rather than gwaslab's internal helper), the lookup fails withReference file '1kg_hm3_hg38_eaf' not foundeven when the file is physically present. - Fix:
run_infer_ancestry()now accepts an optionalref_dirkeyword argument. When provided and the directory exists,gwaslab.bd.bd_download.set_default_directory(ref_dir)is called beforeinfer_ancestry()so gwaslab resolves the HapMap3 EAF reference (PAN.hapmap3.hg38.EAF.tsv.gz/PAN.hapmap3.hg19.EAF.tsv.gz) from the correct location. Both call sites passref_dir=args.ref. - Files:
gwas_process.py(v1.4.24 β v1.4.25).
- Bug: SLURM copies the script to its spool directory before execution, making
BASH_SOURCE[0]resolve to/var/spool/slurmd/jobN/slurm_scriptrather than the original script location. The conf lookup${SCRIPT_DIR}/../gwas2cojo.conftherefore failed withERROR: gwas2cojo.conf not found. - Fix: conf resolution now follows a three-step fallback:
GWAS2COJO_CONFenv var β explicit override${SLURM_SUBMIT_DIR}/gwas2cojo.confβ SLURM always exportsSLURM_SUBMIT_DIRas the directory wheresbatchwas called; submitting from the gwas2cojo root just worksBASH_SOURCErelative path β fallback for direct local invocation
- New: after sourcing the conf,
ROOTDIR=$(dirname "${PYTHON_SCRIPT}")derives the installation root from the already-knownPYTHON_SCRIPTpath, so the Python helper is always called as${ROOTDIR}/utility_scripts/make_chrpos_hdf5.pyregardless of spool location. - File:
utility_scripts/make_chrpos_hdf5.sh.
- Fix: array stages (
checkref,inferstrand,assignrsid,checkaf) now use the chromosome count reported by the split stage (n_split_chr) as the expected total (eff_total) rather than counting SLURM log files. SLURM always arrays over all 26 tasks regardless of how many chromosomes are in the data; tasks for absent chromosomes exit 0 without a "done" marker, previously causing falseβ 23/26warnings for datasets with only autosomes + chrX. - Behaviour: if split reports N chromosomes and all N array tasks complete, status shows
β donewith metricN chromosomes complete. A warning is only raised whenn_done < n_split_chr(genuine missing or failed tasks). - Files:
harmonia.check.py(v1.2.6 β v1.2.7).
- New (
gwas_process.py):--add-chrposflag assigns CHR and POS from rsID at the preprocess stage for datasets that contain only rsIDs (e.g. MVP CAD files). Must be added per-study viaEXTRA_FLAGS(COL12) ingwas_list.txt; not set globally. - New (
gwas_process.py):assign_chrpos_from_hdf5()implements the lookup directly on the pandas DataFrame using pre-built per-chromosome HDF5 files in--ref. Uses the same modulo-10 group structure as gwaslab'srsid_to_chrpos2(). Parallel lookup viaThreadPoolExecutor. When CHR is absent, searches all chromosome files; when CHR is present, restricts to matching files. - New (
utility_scripts/make_chrpos_hdf5.py): one-time setup script wrappinggl.process_vcf_to_hfd5(). ReadsREF_DIRfromgwas2cojo.conf, discovers the best available dbSNP VCF (v157preferred,v151fallback) via gwaslab'sget_path(), and writes HDF5 files intoREF_DIR. Options:--ref-dir,--build hg19|hg38|all,--threads,--complevel,--overwrite. - Usage: run
make_chrpos_hdf5.py --build hg19once, then add--add-chrpostoEXTRA_FLAGSfor affected studies. Multiple flags inEXTRA_FLAGSare space-separated (e.g.--add-chrpos --keep-multiallelic). - Files:
gwas_process.py(v1.4.23 β v1.4.24),utility_scripts/make_chrpos_hdf5.py(new, v1.0.0).
2026-04-03 β‘ gwas_process.py β vectorise EAF fill + check.py EAF reporting (v1.4.23 / check v1.2.6)
- Fix (
gwas_process.py):check_and_fill_eaf()rewritten to fetch per chromosome via tabix rather than issuing one tabix query per variant. For a 3.5 M-variant file across 22 chromosomes this reduces I/O from ~3.5 M individual tabix calls to ~22, cutting runtime from hours to seconds and eliminating preprocess TIMELIMIT kills. - How: for each chromosome, one tabix range-fetch covers all positions in that contig; results are loaded into a
(pos, ref, alt) β AFdict; per-variant AF is resolved by dict lookup (O(1)) with automatic allele-flip when effect/other alleles are swapped. - New (
harmonia.check.py):_metrics_preprocess()now parses EAF fill log lines from the preprocess.outfile and surfaces them in the check summary row:EAF: completeβ EAF column present and fully populatedEAF: not filledβ--fill-eafnot set or suppressedEAF filled N from ref (M still missing)β partial or full fill from reference VCF
- Files:
gwas_process.py(v1.4.22 β v1.4.23),harmonia.check.py(v1.2.5 β v1.2.6).
- Rename: all pipeline scripts renamed from
gwaslab.process.<name>togwas_process.<name>for consistency and brevity:gwas_process.pyβgwas_process.pyharmonia.check.pyβharmonia.check.pyharmonia.cleanup.shβharmonia.cleanup.shharmonia.submit.shβharmonia.submit.shharmonia.submit_staged.shβharmonia.submit_staged.shharmonia.array_for_submit.shβharmonia.array_for_submit.sh
- Updated: all internal cross-references,
VERSION_NAME,prog=, log file suffix (.gwaslab_process.logβ.gwas_process.log), and submit-log filename prefix updated accordingly. - Files: all six scripts above +
CHANGES.md.
- Fix:
_parse_ancestry_check()now correctly handlesMatch: unknown β SKIPPEDlog lines (emitted when EAF is absent/all-NaN andinfer_ancestryis skipped). Previously theUNKNOWNmatch value fell through tostatus: "unknown", showingancestry: unknown (status unknown). Now mapped tostatus: "skipped"and displayed asancestry: not inferred β EAF unavailable (provided=POP). - New:
_metrics_outputs(text)extracts COJO and LDSC output variant counts from the merge stage log ([SAVE] COJO β ...and[SAVE] LDSC β ...patterns). - New: A
β outputssub-row is printed directly after the merge row when either COJO or LDSC (or both) outputs were written, showing variant counts (e.g.COJO 6,912,451 | LDSC 1,103,847). COJO count removed from the merge row itself. - Files:
harmonia.check.py(v1.2.3 β v1.2.4).
- New:
write_ldsc()function produces an LDSC-ready munged summary statistics file from the QC-filtered data. Applies the standard LDSC pre-filtering pipeline on an isolated deep copy (original QC object unchanged):filter_hapmap3()β HapMap3 variants onlyfilter_palindromic(mode="out")β all A/T and C/G SNPs removed (LDSC cannot handle strand ambiguity)exclude_hla()β HLA region excluded (chr6:25β34 Mb)filter_region_out(high_ld=True, build=output_build)β other high-LD regions excludedfilter_value('INFO > 0.9 & EAF > 0.01 & EAF < 0.99')β quality thresholds (INFO filter skipped if column absent)to_format(fmt="ldsc")β gwaslab ldsc format: SNP (rsID), A1, A2, Beta/OR, Frq, INFO, N, P, Z, CHR, POS
- New:
--ldscargparse flag (analogous to--cojo); enabled by default in both submission scripts. - Output:
{stem}.qc.ldsc.tsv.gzalongside the existing.qc.tsv.gzand.cojo.gz. - Robustness: each filter step wrapped in try/except β a missing reference file or failed step skips that step and logs a warning without aborting the pipeline.
- Files:
gwas_process.py(v1.4.20 β v1.4.21),harmonia.array_for_submit.sh,harmonia.submit_staged.sh.
- New (
gwas_process.py):run_infer_ancestry()callsgwas_obj.infer_ancestry()on the QC-filtered data, comparing the declared--populationagainst the Fst-inferred super-population from the HapMap3 pan-ancestry EAF reference (1kg_hm3_hg19/hg38_eaf). Run at the end of the QC block in both themergestage and the--stage allpath. - New (
gwas_process.py):--no-infer-ancestryflag skips the ancestry check (enabled by default). Logged under Toggles asinfer_ancestry=True/False. - Logging: emits a canonical
[ANCESTRY CHECK] Provided: X | Inferred: Y | Match: True/FALSEline (WARNING level on mismatch) parseable by the check script. - Output: result saved to
{stem}.ancestry_check.jsonin the output directory for archival. - New (
harmonia.check.pyv1.2.3):_parse_ancestry_check()parses the[ANCESTRY CHECK]log line from the merge or qc stage output. Result displayed in the study header line. Mismatches shown asβ MISMATCHin the header andβ ANCESTRY MISMATCH β re-check population labelin the overall summary.--errors-onlyalso surfaces ancestry mismatches. - Files:
gwas_process.py(v1.4.19 β v1.4.20),harmonia.check.py(v1.2.2 β v1.2.3).
- New:
_apply_status_filter()helper replaces the previous single digit_7 check with a comprehensive STATUS-based filter covering all problematic flag classes:- Build prefix 97/98:
UnknownGenome/UnmappedVariant(e.g. liftover failures) - Digit 4 in [5,6,7,8]: CHR or POS invalid/unknown (safety net; most handled by
basic_check(remove=True)) - Digit 5 in [5,6,7]: allele indistinguishable, invalid notation, or unknown
- Digit 6 = 8: not on reference genome (safety net; most removed by
check_refinternally) - Digit 7 in [7,8]:
infer_strand2indistinguishable (7) or no match/no info (8) β previously only 8 was filtered
- Build prefix 97/98:
- New:
--filter-palindromicflag callsfilter_palindromic(mode="out")to remove ALL A/T and C/G SNPs at QC. Disabled by default β the STATUS filter is more precise (resolved palindromics at asymmetric MAF are retained; only unresolvable ones removed via digit_7 [7,8]). Use for strict meta-analysis strand-safety. - Logging: STATUS filter reports per-class counts so the breakdown is visible in the log.
- Note: Digit 3 (SNPID/rsID format) issues are intentionally not filtered β they represent ID format problems only; CHR:POS and alleles are still valid and usable.
- Files:
gwas_process.py(v1.4.18 β v1.4.19).
- New:
apply_qc()now runs a STATUS digit-7 filter after the numeric threshold pass. Variants whereinfer_strand2could not resolve the strand (STATUS digit_7 == 8 β palindromic SNPs at MAF~0.5, or indel allele mismatches) are removed before saving QC output. - Background: gwaslab's
check_refalready internally removes variants with digit_6 == 8 (allele absent from FASTA reference).check_af2does not use STATUS β it populates the DAF column, which is covered by--daf-max. The only STATUS flag that survives to output without removal is digit_7 == 8 frominfer_strand2. Nofilter_status()method exists in this gwaslab version; the filter is implemented directly via integer arithmetic (STATUS % 10 == 8). - Logging: separate counts for numeric filter and STATUS filter; total after all QC filters logged at end.
- Files:
gwas_process.py(v1.4.17 β v1.4.18).
- New:
normalize_allele(threads=n_cores)inserted betweenbasic_check()andremove_dup()in bothrun_normalize()and the--stage allpath. This standardises indel notation (trim shared prefix/suffix, uppercase, left-align) before deduplication so that variants expressed differently across studies but representing the same position are correctly identified as duplicates. - Change:
basic_check()now called withremove=True(previously no arguments), so variants with invalid chromosome codes, positions, or allele strings are dropped at source rather than propagating through the pipeline. - Files:
gwas_process.py(v1.4.16 β v1.4.17).
- New: Added
_ANCESTRY_X_VCFSdict covering all 6 ancestries Γ 2 builds for the 1KG chrX VCFs (1kg_{eur,pan,afr,eas,amr,sas}_x_hg19/hg38). - New: Added
1kg_dbsnp151_hg19_xand1kg_dbsnp151_hg38_xchrX SNPIDβrsID conversion tables to_BUILD_FILES(alongside existing autosomal_autotables). - New:
--no-xflag to skip all chrX downloads (population VCFs + rsID tables); default behaviour is to include chrX. - New:
build_download_list()gainsinclude_xparameter;main()printsInclude chrXstatus line. - Files:
gwaslab.download_refs.py(v1.1.0 β v1.2.0).
- Bug:
[[ ! -f "${CONFIG_FILE}" ]]uses-fwhich only matches regular files; process substitution (<(...)) passes a named pipe (/dev/fd/N) which fails the test, producingERROR: config file not found: /dev/fd/63. - Fix: changed to
[[ ! -e "${CONFIG_FILE}" ]](-ematches any file type including pipes), so--config <(grep ...)now works as expected. - File:
harmonia.cleanup.sh
- Fix: added
"freq_a"to EAF aliases inSUMSTATS_ALIASESβ covers TAG consortium files (tag.*.tbl.withN.txt.gz) which useFRQ_Afor effect-allele frequency. Previously EAF was always NaN for these studies and had to be filled from the reference VCF. - Fix: added
"imp_qual"to INFO aliases β covers MVP PAD file (CLEANED.MVP.EUR.PAD.results.anno.nodup.txt.gz) which stores imputation quality asIMP_QUAL. - Fix: added
"rs_id","dbsnp_rs_id","dbsnp_id"to rsID aliases β broadens coverage for MVP and dbSNP-derived header variants. - Fix (
gwas_list.txt):MI_PAN_withmultiallelicpath was missing the leading/β would have failed at file open. - Fix (
gwas_list.txt):AF_PANbuild was19but the TOPMed Freeze 5 file usesposition_b38(hg38 coordinates) β corrected tobuild=38to prevent double-liftover. - Fix (
gwas_list.txt):MEM_LIGHTwas64GBforPAD_EUR_MVP,PAD_EUR_FINNGEN,PAD_EUR_UKBβ SLURM requires64G; corrected. - Change (
gwas_list.txt): TAG study names standardised toTAG_EUR_*naming convention for consistency with other phenotypeβancestry naming in the list. - Files:
gwas_process.py(v1.4.15 β v1.4.16),gwas_list.txt.
- Problem: The submit script passes
--fill-eafglobally for all studies. Studies with no EAF column trigger a per-variant tabix lookup across the full VCF for every variant (O(n)), which is prohibitively slow for large files (e.g. 7.7M variants Γ tabix = many hours). - Fix: Added
--no-fill-eafflag that suppresses the EAF lookup even when--fill-eafis present. Intended for use as a per-studyEXTRA_FLAGSoverride ingwas_list.txt. - Usage: Add
;--no-fill-eafas COL12 ingwas_list.txtfor the affected study. EAF will be filled properly at theprocess-check-afstage from the 1KG VCF anyway. - Logging:
--no-fill-eafis logged in the Toggles line. A separate info message confirms suppression when both flags are present. - Files:
gwas_process.pyv1.4.15.
- Root cause: The Suzuki2024 T2DGGI file uses
Chromsome(typo, missing 'o') for chromosome andNonEffectAllele(no underscore) for the non-effect allele. Neither matched existing aliases, so gwaslab never received a CHR or NEA column. - Symptom 1:
KeyError: Index(['CHR'])inrun_normalize()at theduplicated(subset=["CHR","POS"])multi-allelic count β crashed beforeremove_dupwas even called. - Fix 1: Added
"chromsome"to CHR aliases inSUMSTATS_ALIASES(typo-tolerant match). - Fix 2: Added
"noneffectallele"(no underscore) to NEA aliases inSUMSTATS_ALIASES. - Fix 3: Guarded the
duplicated(subset=["CHR","POS"])call in bothrun_normalize()andrun_processing()with a column-existence check (_has_chr_pos) so a missing CHR column logs 0 multi-allelics rather than raisingKeyError. - Note: No SNPID column in this file is not a blocker β gwaslab derives CHR:POS:NEA:EA IDs via
fix_idonce CHR and NEA are correctly mapped. - Files:
gwas_process.pyv1.4.14.
- Feature: reference directory now defaults to
REF_DIRfromgwas2cojo.conf(parsed next to the script) instead of a hardcoded placeholder. A warning is printed if the conf is absent orREF_DIRis unset. - Feature: new
--buildargument (all/hg19/hg38, default:all) filters downloads to the requested genome build(s). - Feature: new
--ancestryargument (EUR/PAN/AFR/EAS/AMR/SAS/all, default:EUR) selects which 1KG population VCF(s) to download. Default isEURto avoid accidentally triggering all 12 large VCF downloads. - Change: the flat
TO_DOWNLOADlist replaced by structured_ANCESTRY_VCFSand_BUILD_FILESdictionaries;build_download_list()assembles the final keyword list at runtime based on the selected builds and ancestries. Non-ancestry-specific files (dbSNP, FASTA, recombination maps, GTFs, HapMap3 EAF, SNPIDβrsID tables) are always included for the requested build(s). - Files:
gwaslab.download_refs.py,README.md.
2026-03-19 β¨ gwaslab.process.py β detect OR column mislabelled as BETA and auto-rename (v1.4.13)
- Feature: new
check_or_vs_beta()function called during preprocess (afterstandardise_columns). If the standardisedORcolumn contains any negative values it cannot be a true odds ratio β the source file has mislabelled a BETA/log-odds column asOR. The function renamesORβBETAwith a warning log line showing the count and percentage of negative values, and processing continues normally. If bothORandBETAare already present the check is skipped. - Example:
tag.logonset.tbl.withN.txt.gzhas a column namedORcontaining effect sizes like-0.0054,-0.0049, which are clearly log-odds / BETA values. Without this fix the mislabelled column passed through torun_check_refwhereflip_allele_statstried to compute1 / ORand either hitFloatingPointError(OR = 0) or silently produced nonsensical results. - Files:
gwas_process.py(v1.4.12 β v1.4.13).
2026-03-19 π gwaslab.process.py β FloatingPointError in flip_allele_stats when OR = 0 (v1.4.12)
- Bug:
run_check_refcrashed withFloatingPointError: divide by zero encountered in divideinside gwaslab'sflip_by_inversewhen flipping OR-based studies (e.g. TAG_LogOnset). gwaslab computesOR = 1 / ORfor flipped variants; if any OR value is 0 (missing data stored as zero rather than NaN), this raises aFloatingPointError. The error affected all 22 chromosomes (44 total errors = 2 per chromosome). - πFixed
gwas_process.pyβrun_check_refnow checks for anORcolumn before callingflip_allele_stats. Any rows whereOR = 0are dropped with a warning log line before the flip, preventing the divide-by-zero. OR = 0 is not a biologically valid value; these are treated as missing data. - Files:
gwas_process.py(v1.4.11 β v1.4.12).
2026-03-19 π gwaslab.process.py β plot_mqq crashes with TypeError when EAF is entirely missing (v1.4.11)
- Bug:
run_mergeβplot_full_datasetcrashed withTypeError: cannot unpack non-iterable NoneType objectwhen the dataset had no valid EAF values (e.g. DIAMANTE-TA PAN file has no EAF column). gwaslab's_mqqplotreturnsNoneinstead of(plot, log)when it finds no plottable data, and the call site did not guard against this. Theplot_dafcall already had atry/except, butplot_mqq(both pre-QC and QC) did not. - πFixed
gwas_process.pyβ bothplot_mqqloops (pre-QC inplot_full_datasetand QC inplot_qc_dataset) are now wrapped intry/except TypeErrorthat logs a warning and skips the plot rather than crashing the pipeline. All other outputs (parquet, TSV.GZ, COJO, leads) are unaffected. - Files:
gwas_process.py(v1.4.10 β v1.4.11).
2026-03-19 π gwaslab.process.check.py β normalize "after dedup" count not shown for default mode (v1.2.2)
- Bug: the normalize metric showed
liftover β 38only (no variant count) for studies run with the defaultmode="md". Root cause:gwas_process.pyv1.4.9 changed the log message from"After duplicate removal"to"After multi-allelic and duplicate variant removal", but the regex in_metrics_normalizestill matched only the old wording. Studies run with--keep-multiallelic(mode="d") still wrote the old message, so they showed the count while default-mode studies did not. - πFixed
harmonia.check.pyβ_metrics_normalize()regex broadened toAfter (?:multi-allelic and )?duplicate(?:\s+variant)? removal:to match both message variants. - Files:
harmonia.check.py(v1.2.1 β v1.2.2).
2026-03-19 π gwaslab.process.check.py β "after dedup" variant count missing thousands separator (v1.2.1)
- Bug: the normalize metric displayed the post-dedup variant count without comma separators (e.g.
20073068 after dedup) because_first()returns the raw matched string and the log line itself omits commas. - πFixed
harmonia.check.pyβ_metrics_normalize()now converts the matched string tointand reformats it with{:,}before appending to the metric string, yielding20,073,068 after dedup. - Files:
harmonia.check.py(v1.2.0 β v1.2.1).
2026-03-19 π gwaslab.process.check.py β false β 22/26 chr when submit script arrays over 26 but split only produced 22 (v1.2.0)
- Bug: array stages (checkref, inferstrand, assignrsid, checkaf) reported
β 22/26 chrand setany_error = Truefor autosome-only datasets. The submit script always arrays over all 26 chromosomes; for non-autosomal chromosomes the job finds no input data and finishes without writing a[SAVE]marker, so_is_done()returnedFalsefor those 4 jobs. The code then sawn_done=22, n_total=26and flagged a warning even though every autosomal chromosome completed successfully. - πFixed
harmonia.check.py:- After processing the
splitstage, the chromosome count is now stored inn_split_chr. - A new
split_autosome_onlyflag is set whenn_split_chr == 22; it is OR-ed with the existingn_non_auto == 0check into a combinedautosome_onlyflag. - Array stages now track
n_auto_done(autosomal chromosomes that completed) separately fromn_done(all chromosomes). Whenautosome_only, the effective countseff_done / eff_totalaren_auto_done / 22, so non-autosomal "not done" files are ignored. checkrefaggregation is restricted to autosomal chromosome texts whensplit_autosome_onlyand non-autosomal log files exist, preventing empty files from skewing match-rate stats.
- After processing the
- Files:
harmonia.check.py(v1.1.0 β v1.2.0).
2026-03-19 β¨ gwaslab.process.check.py β wider metric column, full "unmatched" display, and autosome-only detection (v1.1.0)
- Fix: metric column widened from 46 to 58 characters (table width 100 β 112) so the full
unmatched N,NNN,NNNvalue is no longer truncated tounmatβ¦in the checkref row. - Feature: array stages (checkref, inferstrand, assignrsid, checkaf) now distinguish between truly incomplete runs and datasets that contain only the 22 autosomes. When all 22 autosomal chromosomes completed and no non-autosomal files exist, the status is
β doneinstead of the misleadingβ 22/26 chr, andany_erroris no longer set β making real failures much easier to spot. - Feature: inferstrand / assignrsid / checkaf metric in autosome-only mode shows
22 autosomes completeinstead of22/22 complete. - Feature: split metric now appends
, no non-autosomalwhen exactly 22 chromosomes are present. - Files:
harmonia.check.py(v1.0.0 β v1.1.0).
- Feature:
gwas_list.txtnow supports an optional 12th semicolon-delimited field (EXTRA_FLAGS) for per-study flags passed verbatim togwas_process.py. Use.as a no-op placeholder. Multiple flags are space-separated within the field. - Example: append
;--keep-multiallelicto a study line to retain multi-allelic variants for that study only, while all other studies use the defaultmode="md"removal. - Example:
;--keep-multiallelic --no-figuresto combine multiple flags. - Files:
harmonia.array_for_submit.sh(reads COL12, appends to CMD array);harmonia.submit_staged.sh(parses COL12 and documents it; the field passes through to the worker viaLINE). - Logging: the worker script echoes
Extra flags : <value>in the job header for traceability.
2026-03-19 β¨ Extended column alias coverage for three new GWAS header formats (gwaslab.process.py v1.4.10)
- Feature: Added aliases for three additional GWAS summary statistics header formats:
- Format 1 (
Tested_Allele/Freq_Tested_Allele_in_HRS):tested_alleleβ EA;freq_tested_allele_in_hrsβ EAF. - Format 2 (meta-analysis fixed-effects):
chromosome(b37)β CHR;position(b37)β POS;chrposidβ SNPID;fixed-effects_betaβ BETA;fixed-effects_seβ SE;fixed-effects_p-valueβ P. - Format 3 (GWAS Catalog harmonised
hm_*columns):hm_variant_id/variant_idβ SNPID;hm_rsidβ rsID;hm_chromβ CHR;hm_posβ POS;hm_effect_alleleβ EA;hm_other_alleleβ NEA;hm_betaβ BETA;hm_effect_allele_frequencyβ EAF.
- Format 1 (
- Design:
hm_*aliases are placed before their bare equivalents in each list so that when a harmonised GWAS Catalog file contains bothhm_effect_alleleandeffect_allele, the harmonised column is preferred byresolve_column().
- Feature: new
--keep-multiallelicflag. By defaultremove_dupruns withmode="md"(remove duplicates and multi-allelic variants). With--keep-multiallelicit runs withmode="d"(duplicates only), leaving multi-allelic sites in the dataset. Useful when the GWAS reports genuine multi-allelic signals or for exploratory analysis before committing to a COJO run. - Feature: before calling
remove_dup, the number of variants at multi-allelic positions (same CHR:POS, different alleles) is now counted viaduplicated(subset=["CHR","POS"], keep=False)and included in the post-removal log line:After multi-allelic and duplicate variant removal: N variants remain (X removed; Y variants were at multi-allelic positions). - Change: log message changed from
"After duplicate removal"to"After multi-allelic and duplicate variant removal"to accurately describe what was removed. - π οΈUpdated:
run_normalize()andrun_processing()both receive the newkeep_multiallelickwarg; both call sites inmain()passkeep_multiallelic=args.keep_multiallelic.
2026-03-19 β¨ gwaslab.process.cleanup.sh β granular pickle retention flags; remove qc.pkl by default (cleanup.sh)
- Change:
KEEP_QC_PKLdefault flipped from1β0. The*.qc.pklcontains onlyself.data(identical to.qc.parquet),self.log(redundant with archived SLURM logs), and gwaslab internal state flags β nothing needed for downstream analysis. Pass--keep-qc-pklto retain it. - Feature:
*.normalize.pklmoved out of the unconditional removal list and into its own conditional block, controlled by--keep-normalize-pkl(default: remove). Useful if you want to reload the normalized Sumstats object without re-running preprocess + normalize. - Change:
--remove-qc-pklflag removed (now the default); replaced by--keep-qc-pklto opt in to retention. - Change: raw pkl loop now also skips
*.normalize.pkl(handled by its own block) in addition to*.qc.pkl. - π οΈUpdated: header comment and usage examples updated accordingly.
2026-03-19 π gwaslab.process.cleanup.sh β per-chromosome intermediate parquets not removed (cleanup.sh)
- Bug: per-chromosome intermediate parquets (
*.chr*.normalize.parquet,*.chr*.checkref.parquet,*.chr*.inferstrand.parquet,*.chr*.assignrsid.parquet,*.chr*.checkaf.parquet) were not included in the cleanup patterns. These files are the stage-to-stage handoffs written bysave_chrom_parquet()for each of the 26 chromosome array tasks, and collectively represent the largest share of intermediate disk usage (e.g. 26 Γ 5 stages Γ ~28 MB = ~3.6 GB per study for a large GWAS). - πFixed
harmonia.cleanup.shβ added all five*.chr*.{stage}.parquetglob patterns to thepatternsarray incleanup_study(). Updated the header comment to document them.
- Feature: after removing intermediate checkpoints, the cleanup script now moves all SLURM
*.out/*.errfiles belonging to the study fromLOG_DIR(default:OUT_BASE) into${OUT_BASE}/<STUDY>/logs/. This keeps the submit directory tidy and preserves logs in a study-specific location for future use withharmonia.check.py. - New flags:
--no-archive-logs(skip archiving),--log-dir PATH(override source directory when SLURM logs land elsewhere). - After archiving, the script prints the exact
harmonia.check.pycommand to inspect the archived logs. - Log archiving is enabled by default;
--dry-runmode previews what would be moved without touching files.
2026-03-19 π gwaslab.process.cleanup.sh β wrong output directory path and fragile glob (cleanup.sh)
- Bug:
harmonia.cleanup.shwas silently doing nothing in all three modes (--study,--all,--config). Root cause: all three study-directory paths were constructed as${OUT_BASE}/${STUDY_NAME}/GWASCatalog, but/GWASCatalogis only appended bygwas_process.pywhen--outputis not passed on the command line. The pipeline always passes--output "${OUT_BASE}/${GWAS_NAME}"explicitly (viaarray_for_submit.sh), sooutput_loc = args.outputβ no/GWASCatalogsuffix. Every directory existence check therefore failed and cleanup was skipped without any error. - πFixed
harmonia.cleanup.sh:--studymode:${OUT_BASE}/${STUDY_NAME}/GWASCatalogβ${OUT_BASE}/${STUDY_NAME}--allmode: glob"${OUT_BASE}"/*/GWASCatalogβ"${OUT_BASE}"/*/;basename "$(dirname ...)"βbasename "${study_dir}"--configmode:${OUT_BASE}/${GWAS_NAME}/GWASCatalogβ${OUT_BASE}/${GWAS_NAME}- Fragile unquoted glob
local raw_pkl_pattern="${study_dir}/"*.pkl(expands at assignment time) β quoted"${study_dir}/*.pkl"(expands atforloop time)
2026-03-19 π gwaslab.process.check.py β AttributeError on optional regex group (check.py v1.0.1)
- Bug:
AttributeError: 'NoneType' object has no attribute 'strip'in_first()when called with a regex containing an optional capturing group ((...)?). The outerre.search()matched (somwas notNone), butm.group(1)wasNonebecause the optional group did not participate in the match. Them.group(group).strip() if m else defaultguard only checked for a missing match, not for aNonegroup value. - πFixed
harmonia.check.pyβ_first()now checksval = m.group(group)separately and returnsdefaultifval is None. The broken\[SAVE\] QC Parquetregex with an optional group was also removed (it was dead code β the result was never used;qc_nvia theAfter QCpattern was the operative extraction). Bumped to v1.0.1.
2026-03-18 π§° Added gwaslab.process.check.py β pipeline run-status checker (check.py v1.0.0 / gwaslab.process.py v1.4.8)
- New tool
harmonia.check.py(v1.0.0): standalone Python script that parses*.out/*.errlog files produced by the staged gwaslab pipeline and prints a per-stage summary table with key QC metrics, warning/error counts, and overall pass/fail status. - Supports checking a single study (
python gwaslab.process.check.py GWAS_ID [log_dir]), all studies in a directory (--all), or only studies with problems (--errors-only). - Parses: variant counts from preprocess/normalize, liftover status, chr-split count, checkref match rate + flipped/unmatched variant totals aggregated across chromosomes, and merge combined/QC-pass/COJO variant counts.
- Distinguishes real errors (Traceback,
*Error:,Illegal instruction,[ERROR]) from known-benign upstream warnings (gwaslab FutureWarning/UserWarning/SettingWithCopyWarning, matplotlib, htslib[W::]). - πUpdated:
README.mdβ addedharmonia.check.pyto the HPC files table and added a dedicatedπ©Ί Check run statussection with usage examples and sample output. - π οΈUpdated: Bumped
gwas_process.pyto v1.4.8 (2026-03-18) to mark this as a versioned release.
- Warning:
FutureWarning: The behavior of DataFrame concatenation with empty or all-NA entries is deprecatedwas emitted from line 903 during the merge stage. Root cause: per-chromosome shards for continuous traits (noN_cases/N_controls) contain all-NA entries in those nullable integer columns. Whenpd.concatsees a mix of all-NA and populated shards it warns about dtype inference, even though the parquet-preservedInt64dtypes are already correct and consistent across all shards. - πFixed
gwas_process.pyβrun_merge()now (1) filters out genuinely empty DataFrames before concat as a safety guard, and (2) wrapspd.concatin awarnings.catch_warnings()context that suppresses only this specific FutureWarning. The current concat behaviour is exactly correct for our use case; the suppression will be revisited if pandas changes its dtype inference in a way that affects results. - π οΈUpdated: Added
import warningsto the import block. Bumped version to1.4.7(2026-03-18).
- Bug: All per-chromosome SLURM array jobs crashed with
Illegal instruction (core dumped)preceded by apolarsRuntimeWarning:Missing required CPU features: avx2, fma, bmi1, bmi2, lzcnt, movbe. The standardpolarswheel on PyPI is compiled with AVX2/FMA intrinsics; older HPC compute nodes (pre-Haswell microarchitecture) lack those instruction-set extensions. SettingPOLARS_SKIP_CPU_CHECK=1only suppresses the Python-level RuntimeWarning β the binary still faults the moment any AVX2 instruction executes. The crash occurs insidegwaslabwhich importspolarsinternally. - πFixed
environment.ymlβ replaced"polars>=1.27.0"with"polars-lts-cpu>=1.27.0". Thepolars-lts-cpuPyPI package is the official CPU-compatible build of Polars, compiled for SSE2/SSE4 without AVX2 or FMA requirements. It provides an identical public API and can run on any x86-64 node regardless of CPU generation. The tradeoff is a modest performance reduction on modern nodes (typically 10β20 % slower for vectorised operations), which is negligible compared to the I/O and Python overhead in this pipeline. β οΈ Action required: rebuild the conda environment after pulling this change:mamba env remove -n gwas2cojo && mamba env create -f environment.yml. If the environment must be updated in-place without rebuilding:pip uninstall polars && pip install "polars-lts-cpu>=1.27.0".- π οΈUpdated: Bumped version to
1.4.6(2026-03-18).
2026-03-18 π Categorical re-encoding by gwaslab init breaks flip_allele_stats in per-chr stages (v1.4.5)
- Bug:
process-check-ref(and downstream per-chromosome stages) crashed withTypeError: Cannot setitem on a Categorical with a new category, set the categories firstinsidegwaslab.flip_allele_stats(), even thoughload_chrom_parquet()already converted Categorical columns toobjectbefore passing the DataFrame tomake_sumstats_from_chrom_df(). Root cause: gwaslab'sgl.Sumstats.__init__()callsbasic_check()internally, which re-encodesEAandNEAaspd.Categorical. Because the input is a per-chromosome shard, each column's category set only contains the allele values observed on that chromosome. Whenflip_allele_stats()then tries to swapEAβNEAfor 117,715 variants (e.g. LAS chromosome 9), it attempts to assign anEAvalue that is present inNEA's category set but absent fromEA's subset, causing the pandas error. The earlierv1.4.2fix inload_chrom_parquet()was not sufficient because it converted before construction, and construction undoes it. - πFixed
gwas_process.pyβmake_sumstats_from_chrom_df()now runs a second Categorical-to-object conversion ongwas_obj.dataimmediately aftermake_sumstats_object()returns. All columns identified byselect_dtypes(include="category")(typicallyEA,NEA,SNPID) are converted to plainobjectdtype. This conversion is applied once at object-creation time and persists through all downstream per-chromosome processing steps (check_ref,infer_strand,assign_rsid,check_af). Theload_chrom_parquet()conversion is kept as a pre-construction safety net. - π οΈUpdated: Bumped version to
1.4.5(2026-03-18).
- πAdded
test/generate_test_data.pyβ stdlib-only Python script that generates 10,000 synthetic biallelic SNPs and writes four files: a genetic reference (ref.txt.gz) and three GWAS summary-statistics files covering different column-naming conventions (gwas_metal_tab.txt.gz,gwas_plink2.txt.gz,gwas_saige.txt.gz). Datasets include deliberate complement-strand and allele-switched variants so the NOP, FLIP, and translated-allele branches ofselect_action()are all exercised. Regenerate at any time withpython3 test/generate_test_data.py. - πAdded
test/ref.txt.gz,test/gwas_metal_tab.txt.gz,test/gwas_plink2.txt.gz,test/gwas_saige.txt.gzβ pre-generated test fixtures (~870 KB total) committed sobash test/run.shruns without a generation step. - π οΈUpdated
test/run.shβ extended from a 1-variant smoke test to four tests: (0) the original A.txt/B.txt sanity check; (1) METAL-style with full auto-detection andgwas2cojo-verify.pyvalidation (0 errors); (2) PLINK2-style exercising the newA1FREQ/OBS_CTaliases; (3) SAIGE-style requiring--gwas:effect Allele2 --gwas:other Allele1 --gwas:freq AF_Allele2manual overrides.
- πFixed
equal_alleles(a, b)β the second comparison wasa.ref == b.ref(duplicate of the first), meaning the other allele (oth) was never checked. Corrected toa.oth == b.oth. - πFixed
switched_alleles(a, b)β the body referenced free variablesgenandgwas(local names inside the callerverify()) instead of the function parametersaandb. As a module-level function, Python resolves free variables in the global scope, so any call that reached a FLIP assertion would raiseNameError: name 'gen' is not defined. Corrected toa.ref == b.oth and a.oth == b.ref. - π οΈUpdated
Last updatedate to2026-03-18.
- πFixed
select()insideread_gwas()βexcept IndexErrorwas catching the wrong exception type:list.index()raisesValueError, notIndexError. A bad user-supplied--gwas:<col>value therefore propagated as an uncaughtValueErrorinstead of printing the helpful "not found" diagnostic. Corrected toexcept ValueError. - πFixed
GWAS_H_NCONTROL_OPTIONS/GWAS_H_NCASE_OPTIONSβ the entries'TotalCases'and'TotalSampleSize'were in the wrong lists (swapped).TotalCasesbelongs in the case count list;TotalSampleSizebelongs in the total-N list. Corrected:GWAS_H_NCONTROL_OPTIONSnow contains'TotalControls'/'n_controls';GWAS_H_NCASE_OPTIONSnow contains'TotalCases'/'n_cases'. - πFixed
gwas_header_auto(gwas_filename)β function body used the undefined namefilenameinstead of the parametergwas_filename, and calledfopen(filename, 'rt')with two arguments whilefopen()only accepts one. Also used the undefined nameheadersinstead ofheader. Corrected tofopen(gwas_filename)andlen(header). - πAdded column aliases for widely-used GWAS tool outputs:
GWAS_H_FREQ_OPTIONS:'A1FREQ'(PLINK2.afreq/.linear/.logistic),'FRQ'(PLINK 1.9.frq)GWAS_H_NTOTAL_OPTIONS:'OBS_CT'(PLINK2 observation count),'n_total'
- π οΈUpdated
Last updatedate to2026-03-18.
- π οΈUpdated: Bumped version to
1.4.4(2026-03-18). - Bug: After the
gwas2cojo.confintroduction, SLURM jobs immediately failed withERROR: /var/spool/slurmd/job<ID>/gwas2cojo.conf not found. SLURM copies the worker script (array_for_submit.sh) to its own temporary spool directory before executing it on the compute node, soBASH_SOURCE[0]inside the job resolves to the spool path rather than the original script location. The conf-file lookup"${SCRIPT_DIR}/gwas2cojo.conf"therefore searched in/var/spool/slurmd/job<ID>/where no conf file exists. - πFixed
harmonia.array_for_submit.shβ the conf-loading stanza now checks the environment variableGWAS2COJO_CONFfirst (exported by the submit scripts, which run on the login node and always have the correct absolute path). TheBASH_SOURCE-relative lookup is retained as a fallback for direct local invocation only. An improved error message names all three possible causes when the conf is still not found. - π οΈUpdated
harmonia.submit_staged.sh,harmonia.submit.shβ both scripts nowexport GWAS2COJO_CONF="${CONF}"immediately after sourcing the conf. SLURM propagates all exported environment variables to job environments by default (--export=ALL), so the absolute path is reliably available inside every job regardless of which spool directory SLURM uses.
- π οΈUpdated: Bumped version to
1.4.3(2026-03-18). - πRemoved all hardcoded HPC-specific paths and institutional email addresses (
@umcutrecht.nl) from every tracked file in the repository so the codebase is clean for public use. - πAdded
gwas2cojo.conf.exampleβ a single site-configuration template containing five variables (PYTHON_SCRIPT,REF_DIR,OUT_BASE,CONDA_ENV,EMAIL). Users copy it togwas2cojo.conf(gitignored) and fill in their local values once. - π οΈUpdated
harmonia.array_for_submit.sh,harmonia.submit.sh,harmonia.submit_staged.sh,harmonia.cleanup.shβ replaced per-scriptUSER CONFIGURATIONblocks with a uniform conf-loading stanza (source "${SCRIPT_DIR}/gwas2cojo.conf"). All four scripts now emit a clear error with instructions ifgwas2cojo.confis missing. - π οΈUpdated
.gitignoreβ addedgwas2cojo.confandgwas_list.txtso local site settings and study lists are never accidentally committed. - π οΈUpdated
gwaslab.download_refs.pyβ replaced hardcodedDEFAULT_REF_DIRpath and docstring with placeholder values. - πAdded
gwas_list.example.txtβ a minimal three-study example config (CAD_Aragam,CHARGE_CAC_EA,AF) with placeholder/path/to/gwas_datasets/prefixes, HEADER comments, and resource annotations. Serves as the committed template; users copy togwas_list.txt(gitignored) and update paths. - π οΈUpdated
gwas2cojo.py,gwas2cojo-verify.pyβ replaced@umcutrecht.nlbanner addresses with obfuscated personal addresses (lennart[at]landsmeer[dot]email,s.w.vanderlaan[at]gmail[dot]com), matching the format already used ingwas_process.pyand the README licence block. - π οΈUpdated
README.mdβ added aβοΈ One-time site setupsection explaining thegwas2cojo.confworkflow; addedgwas2cojo.conf.exampleandgwas_list.example.txtto the HPC files table; replaced remaining HPC paths and institutional emails throughout.
- π οΈUpdated: Bumped version to
1.4.2(2026-03-18). - Bug 1: All per-chromosome
process-check-refandprocess-infer-strandjobs failed withTypeError: Cannot setitem on a Categorical with a new category, set the categories firstinside gwaslab'sflip_allele_stats(). gwaslab encodes EA/NEA/SNPID aspd.Categoricalafterbasic_check()for memory efficiency, and parquet round-trips preserve that dtype. A per-chromosome shard's Categorical column only contains the allele categories actually present on that chromosome. Whenflip_allele_statstries to swap EAβNEA for a variant whose allele (e.g. an indel sequence) exists in EA's category set but not NEA's on that chromosome, pandas refuses the assignment. In the whole-genome path this never surfaced because the full Categorical across all chromosomes includes all values in both columns simultaneously. - πFixed
gwas_process.pyβload_chrom_parquet()now converts allpd.CategoricalDtypecolumns to plainobjectdtype immediately after reading the parquet (df.select_dtypes(include="category")), before the DataFrame is passed tomake_sumstats_from_chrom_df(). This only affects EA/NEA/SNPID-style string columns; numeric columns (STATUSInt64, CHR, POS, BETA, SE, P, N, etc.) are not Categorical and are completely unaffected. gwaslab operates identically onobjectdtype allele strings for all harmonise/check operations; Categorical is purely a memory optimisation that is not required for correctness. - Bug 2: The
mergestage failed for CHARGE_CAC_EA_AA withZeroDivisionError: division by zeroinside gwaslab'splot_daf()(num / len(sumstats)wherelen(sumstats) == 0). The study had too few variants with a valid DAF value after processing (EAF largely absent or all-NaN), leaving an empty subset after DAF filtering inside gwaslab's plot routine. - πFixed
gwas_process.pyβ wrappedgwas_obj.plot_daf()in bothplot_full_dataset()andplot_qc_dataset()withtry/except ZeroDivisionError. When triggered, alogging.warningis emitted and the DAF plot is skipped; the rest of the merge stage (Manhattan, QQ, QC, leads, COJO) continues normally. - π οΈUpdated
harmonia.submit.shβ addedNODES,CPUS,EMAIL, andMAIL_TYPEvariables and passed--nodes,--cpus-per-task,--mail-type,--mail-userto thesbatchcall. Previously these were absent, relying on the now-removed#SBATCHdirectives inarray_for_submit.sh. - π οΈUpdated
harmonia.array_for_submit.shβ removed#SBATCH --mail-type=END,FAILand#SBATCH --mail-userdirectives from the worker script header. SLURM merges#SBATCHdirectives from the script with command-line flags rather than letting the command line override them, so the hardcodedENDin the script was causing end-of-job emails despite both submit scripts setting--mail-type=FAIL. Mail settings are now solely controlled by the calling submit script. Updated the comment to correctly name bothsubmit.shandsubmit_staged.shas the controlling scripts.
- π οΈUpdated: Bumped version to
1.4.1(2026-03-18). - Bug: The v1.4.0 per-chromosome refactor of
harmonia.submit_staged.shdropped three SLURM job settings that were present in the earlier script:--nodes,--cpus-per-task, and--mail-type/--mail-user. As a result all submitted jobs would inherit SLURM defaults (typically 1 CPU, which starves the multi-threaded Python worker that requests--threads 8viaWORKER_FLAGS), and no failure-notification emails would be sent. - πFixed
harmonia.submit_staged.shβ added four variables to the USER CONFIGURATION block (NODES=1,CPUS=8,EMAIL,MAIL_TYPE="FAIL") and passed--nodes,--cpus-per-task,--mail-type,--mail-userto all eightsbatchcalls (preprocess, normalize, split, check-ref, infer-strand, assign-rsid, check-af, merge).CPUSis intentionally kept in sync with the--threads Nvalue inWORKER_FLAGS.
- Bug: In all staged pipeline paths (
process-check-ref,process-infer-strand,process-assign-rsid,process-check-af,qc, and the newmerge),normalise_build(REFERENCE)was used instead ofbuild_numwhen selecting the reference FASTA, dbSNP VCF, and Sumstats build, and when setting the chromosome map for Manhattan/QQ plots.REFERENCEis set fromargs.build(the original input build, e.g."19") and is never updated between staged invocations.build_numis correctly set to"38"at startup for any hg19/hg18+liftover study. - Impact: For any study submitted with
--liftover, all four heavy stages and both plot-generating stages would use:hg19.fa.gzinstead ofhg38.fa.gzincheck_refβ coordinates are hg38, FASTA is hg19 β nearly all variants incorrectly flagged as MISREF and lost.GCF_000001405.25.gz(hg19 dbSNP) instead ofGCF_000001405.40.gz(hg38 dbSNP) inassign_rsidβ rsIDs assigned from the wrong coordinate space.build="19"in reconstructedSumstatsobjects (per-chr and merge paths) β wrong internal build attribute for all downstream gwaslab operations.build=referenceinplot_mqqβ hg19 chromosome-length map applied to hg38 positions β distorted Manhattan plots.
- Why not seen before: The staged whole-genome path (pre-v1.4.0) always OOM'd inside
process-check-refor later, so these stages never produced output. The per-chromosome refactor (v1.4.0) is specifically designed to make these stages complete β meaning the wrong results would be written and stored for the first time. - πFixed
gwas_process.pyβ replacednormalise_build(REFERENCE)/REFERENCEwithbuild_numat nine call sites acrossmain()andrun_merge():process-check-refper-chr and whole-genome:run_check_ref(gwas_obj, build_num, args.ref)(FASTA path)process-assign-rsidper-chr and whole-genome:run_assign_rsid(gwas_obj, build_num, args.ref, β¦)(dbSNP VCF path)make_sumstats_from_chrom_df(df, build_num)in all four per-chr stage branches and inrun_merge()plot_full_dataset(β¦, build_num, β¦)andplot_qc_dataset(β¦, build_num, β¦)inprocess-check-af(whole-genome),qc, andmerge
- π οΈUpdated: Bumped version to
1.4.0(2026-03-17). - Context: Studies were OOM-failing at
process-check-ref,process-infer-strand,process-assign-rsid, andprocess-check-afeven at 128β256 G. The root cause is that these stages sweep large VCF files (1KG ~84 M variants, dbSNP ~1 B variants) against the full genome-wide GWAS dataset. The fix splits the dataset by chromosome before the heavy stages so each VCF-sweep job works on ~1/22 of the variants. - πAdded
process-splitstage togwas_process.pyβ loads{stem}.normalize.pkl, splits by chromosome into per-chromosome BROTLI parquets ({stem}.chr{N}.normalize.parquet, N = 1β26), and writes a{stem}.chrsplit.jsonmanifest. CHR values follow gwaslab'sInt64convention: 1β22 = autosomes, 23 = X, 24 = Y, 25 = nonPAR, 26 = MT. Parquets preserve the STATUS bitmask column so gwaslab state is maintained across the per-chr jobs. - πAdded
mergestage togwas_process.pyβ concatenates all{stem}.chr{N}.checkaf.parquetshards into a single genome-wide DataFrame, recreates a gwaslabSumstatsobject (with STATUS restored), then runs QC filtering, plots (Manhattan, QQ, DAF), lead-variant extraction, and COJO output. Replaces the separateqc+cojostages in the per-chromosome pipeline path. - πAdded
--chrom Nargument (int 1β26) togwas_process.py. When set,process-check-ref,process-infer-strand,process-assign-rsid, andprocess-check-afeach operate on a single chromosome shard ({stem}.chr{N}.{prev}.parquetβ{stem}.chr{N}.{next}.parquet). If the shard does not exist the stage exits gracefully with exit code 0, satisfying SLURMafterokdependencies for the next array stage. - πAdded helper functions:
load_chrom_parquet(),save_chrom_parquet(),make_sumstats_from_chrom_df(),split_by_chrom(),load_chrsplit_manifest(),run_merge(). - π οΈUpdated
harmonia.submit_staged.shβ the four heavy process stages are now submitted as SLURM array jobs (--array=1-26); aprocess-splitjob is inserted betweenprocess-normalizeand the array stages; amergejob replaces theqc+cojotail. Fixed resources:process-split16 G / 30 min.afterokon an array job ID waits for all 26 tasks; absent-chromosome tasks (exit 0) satisfy the dependency automatically. Job count per study: ~107 (vs. 8 before), well within the site limit of 120,000. Updated monitor/cancel hints. - π οΈUpdated
harmonia.array_for_submit.shβ appends--chrom ${SLURM_ARRAY_TASK_ID}to the Python command when running as an array task; logs the chromosome in the job header.
- πAdded:
gwaslab.download_refs.pyβ utility script and complete inventory of all gwaslab reference files, using gwaslab's built-ingl.download_ref()function. Active entries (AFR, EAS, AMR, SAS for both hg19 and hg38) are downloaded; all other files already present at the reference directory are listed as comments and can be uncommented to (re-)download. Covered categories: 1KG population VCFs (all six populations, hg19 + hg38), HapMap3 EAF tables, 1KG SNPIDβrsID conversion tables, dbSNP v151/v157 VCFs (very large, NCBI FTP), UCSC reference FASTA, recombination maps, and Ensembl/RefSeq GTF files. The.tbiindex is fetched automatically alongside each VCF. Target directory defaults to/path/to/references/gwaslab/; override with--ref-dir. Prints a summary viagl.check_downloaded_ref()on completion. - π οΈUpdated:
README.mdβ added aπ₯ Reference file managementsection with a full reference-file inventory table (keyword, filename, default status), usage instructions, and a note about Dropbox/NCBI accessibility on HPC. Addedgwaslab.download_refs.pyto the HPC helper-scripts file table.
- π οΈUpdated:
environment.ymlβ overhauled to reflect the full dependency set required bygwas_process.py. Upgraded Python from3.11to3.12. Moved all Python packages to thepip:block with pinned or bounded versions:numpy>=1.21.2,<2,adjusttext==0.8,matplotlib>=3.8,<3.9,pandas>=1.3,!=1.5,pysam==0.22.1,scikit-allel>=1.3.5,scipy>=1.12,seaborn>=0.12,h5py>=3.10.0,pyarrow,polars>=1.27.0,sumstats-liftover==1.1.0,jupyter==1.0.0,gwaslab,pyliftover,tqdm.bcftoolsretained as a conda dependency (bioconda channel) rather than a pip package. Replaceddefaultschannel withnodefaultsto avoid the Anaconda commercial repository, which is not permitted at many academic institutions; all packages are sourced exclusively fromconda-forgeandbioconda. - π οΈUpdated:
README.mdβ replaced the requirements and installation sections. Now documents Python 3.12 andbcftoolsas requirements; provides two installation paths (Option A:mamba env create -f environment.yml; Option B: manualmamba create+pip install); updated verification command to importgwaslabandpolars; updated troubleshooting guidance for dependency conflicts and biocondabcftools.
- π οΈUpdated:
gwas_list.txtβ added two new columns:MEM_LIGHT(COL10) andTIME_LIGHT(COL11) for the moderate pipeline stages (process-normalize,process-check-ref,qc). The existingMEM(COL8) andTIME(COL9) columns are unchanged and continue to control the heavy VCF-sweep stages (process-infer-strand,process-assign-rsid,process-check-af). Note added to header:MEM_LIGHTshould be set higher for studies with many columns or complex allele structure (e.g. the AF multi-ancestry meta-analysis required 128G atprocess-check-refdespite having fewer variants than EUR studies that passed at 64G). - π οΈUpdated:
harmonia.submit_staged.shβ replaced per-stage fixed defaults with two script-level fallback defaults (MEM_LIGHT_DEFAULT=64G,MEM_HEAVY_DEFAULT=128G). Per-studyMEM_LIGHT/TIME_LIGHTare read from COL10/COL11 and applied to all light-tier stages (process-normalize,process-check-ref,qc); if absent the fallbacks are used. Report table now shows both tiers alongside the job chain. - π οΈUpdated: Active entries in
gwas_list.txtβMEM_LIGHT/TIME_LIGHTassigned per study:32G/12hfor standard EUR studies;64G/24hfor PAN and large EUR studies;128G/24hfor AF (known to require higher memory atprocess-check-ref).
- π οΈUpdated: Bumped version to
1.3.0(2026-03-16). - πAdded: Five
--stage process-*sub-stages togwas_process.py, splitting the monolithic process stage by memory profile. Each sub-stage saves a pickle checkpoint so subsequent stages can be submitted as independent SLURM jobs with their own resources:process-normalizeβbasic_check+remove_dup+liftoverβ{stem}.normalize.pkl(medium)process-check-refβcheck_ref+flip_allele_stats+fix_idβ{stem}.checkref.pkl(medium)process-infer-strandβinfer_strand2+flip_allele_statsβ{stem}.inferstrand.pkl(high β 1KG VCF sweep)process-assign-rsidβassign_rsidvia dbSNP VCF sweep β{stem}.assignrsid.pkl(extreme β dbSNP sweep; skipped when--dbsnpnot set)process-check-afβcheck_af2β final raw outputs.pkl+.parquet+.tsv.gz(high β 1KG VCF sweep)
- πAdded:
run_normalize(),run_check_ref(),run_infer_strand(),run_assign_rsid(),run_check_af()β individual runner functions extracted fromrun_processing(), each containing exactly the steps for their sub-stage. - πAdded:
save_process_checkpoint()andload_process_checkpoint()β pickle-based checkpoint I/O for process sub-stages, with descriptive error messages on missing files. - πAdded:
_PROCESS_CHECKPOINT_METAlookup dict mapping checkpoint suffixes to stage names and their predecessor, used in error messages when a checkpoint is missing. - π οΈUpdated:
--stagechoices inparse_args()now include all fiveprocess-*sub-stages alongsideall,preprocess,qc, andcojo. - π οΈUpdated:
main()β added guard that exits with an error if--stage process-assign-rsidis used without--dbsnp. - π οΈUpdated:
main()β_pickle_required_stagesguard extended to cover all process sub-stages that require a prior-stage checkpoint. - π οΈUpdated: Header comment in
main()documents the full checkpoint chain:preprocess β normalize β checkref β inferstrand β assignrsid β checkaf β qc β cojo. - πAdded:
harmonia.submit_staged.shβ new script that submits one SLURM job per stage per study with--dependency=afterokchaining. If a stage fails, SLURM cancels all downstream stages for that study automatically; other studies are unaffected.MEMandTIMEfromgwas_list.txtare applied to the two heaviest stages (process-infer-strandandprocess-assign-rsid); all other stages use fixed resource defaults defined at the top of the script.process-assign-rsidis omitted when--dbsnpis absent fromWORKER_FLAGS. - πAdded:
harmonia.cleanup.shβ removes all intermediate checkpoint files after a successful run. Final outputs (.parquet,.tsv.gz,.qc.*,.cojo.gz,.leads.tsv,.log,PLOTS/) are never touched. Supports--study NAME,--all, or--config gwas_list.txtscope;--dry-runprints what would be deleted without removing;--keep-raw-pklpreserves the final raw pickle;--remove-qc-pklalso removes the QC pickle (kept by default).
- π οΈUpdated: Bumped version to
1.2.0(2026-03-16). - πAdded:
--stageflag togwas_process.pywith four stages:preprocess,process,qc, andcojo(plusall, the default, which preserves the existing end-to-end behaviour). Each stage can be submitted as a separate SLURM job with its own--memand--time, allowing resource-light stages to run at64G / 48hwhile memory-intensive steps (process:check_ref,infer_strand2,assign_rsid) can be given128Gβ256G / 96hindependently. - πAdded:
save_preprocess_checkpoint()β writes{stem}.preprocess.parquet(BROTLI-compressed standardised DataFrame) and{stem}.preprocess.json(detected build metadata) as the handoff from--stage preprocessto--stage process. - πAdded:
load_preprocess_checkpoint()β reads the parquet + JSON checkpoint written by--stage preprocessand restores thereference,build_num, andinput_buildso subsequent stages use identical file stems. - πAdded:
import jsonto top-level imports (previously absent; required by the new checkpoint metadata functions). - π οΈUpdated:
main()β refactored into four clearly labelled stage blocks (STAGE: preprocess,STAGE: process,STAGE: qc,STAGE: cojo). In--stage allmode the blocks execute in sequence without touching disk checkpoints, preserving current behaviour. In individual-stage mode each block saves its checkpoint and returns early. - π οΈUpdated:
main()β--only-qcis now a backward-compatible alias for--stage qc; a deprecation notice is logged when it is used. - πAdded: Guard in
main()that exits with an error if--stage qcor--stage cojois combined with--no-pickle, since both stages require a pickle written by a prior stage. - π οΈUpdated: Stage summary at end of each stage block now logs the next recommended stage invocation (e.g.
Next: --stage process (pass the same --gwas / --build / --liftover / --output flags)).
- π οΈUpdated: Bumped version to
1.1.0(2026-03-16). - πAdded:
import gc(previously commented out) to enable explicit garbage collection at stage boundaries. - πAdded:
--no-pickleflag togwas_process.py. When set,.pklfiles are skipped for both raw and QC outputs, reducing peak memory and disk usage on the save step. The gwaslab.logfile is still written regardless. Note:--only-qcrequires a pickle from a prior run, so it is incompatible with--no-pickle. - π οΈUpdated:
save_raw_outputs()andsave_qc_outputs()β eliminated the parquet read-back pattern (pd.read_parquet(parquet_path)) that was used to generate the TSV.GZ. Both functions now write the TSV directly from the in-memorygwas_obj.data/gwas_obj_qc.data, avoiding a full extra copy of the data just to write one file. - π οΈUpdated:
main()β addeddel gwas_data; gc.collect()immediately afterplot_raw_histograms()(the last use of the raw DataFrame). This frees the raw pandas DataFrame before the heavy processing steps (check_ref,infer_strand2,assign_rsid,check_af2), preventing two full-size DataFrames from coexisting in RAM throughout the pipeline. - π οΈUpdated:
main()β addeddel gwas_obj; gc.collect()immediately afterapply_qc()returnsgwas_obj_qc. The unfiltered object is freed before saving QC outputs and generating QC plots, so only one copy of the data is in memory at a time during the QC stage. - π οΈUpdated:
write_cojo()β removed unnecessary.copy()call (df = gwas_obj.data.copy()βdf = gwas_obj.data). All downstream accesses are read-only (column selection,astype, constructing a newpd.DataFrame), so the copy was wasted memory.
- π οΈUpdated: The
gwas_list.txtfile to use semicolons (;) as the field delimiter instead of tabs, avoiding parsing issues when paths or values contain whitespace. - πAdded: Two new columns to
gwas_list.txt:MEM(COL8, SLURM memory per job, e.g.64Gor128G) andTIME(COL9, SLURM time limit per job, e.g.48:00:00), allowing resource requirements to be set individually per dataset. - π οΈUpdated:
harmonia.submit.shto submit one independent SLURM job per dataset instead of a single array job. Memory (--mem) and time (--time) are now read from the config file and passed to eachsbatchcall individually, so datasets with different resource needs no longer share a single limit. Each job receives its own--job-name,--output, and--errorderived from the dataset name. - π οΈUpdated:
harmonia.array_for_submit.shto act as a single-dataset worker script. Removed array job logic (SLURM_ARRAY_TASK_ID), removed fixed--mem,--time,--output, and--errorSBATCH directives (these are now set dynamically byharmonia.submit.sh). The script now accepts a semicolon-delimited config line as its first argument and parses it directly.
- πAdded: New GWAS datasets to the
gwas_list.txtfile, including:- AFGen Roselli 2018 dataset for allele frequencies (AF) with b38 positions.
- GLGC Graham 2021 datasets for HDL, LDL, TC, TG, and non-HDL traits in European populations.
- π§°Fixed: Issue with time of the SLURM job in
harmonia.array_for_submit.shto allow for longer processing times, especially for larger GWAS datasets. Updated the time limit from 1 hour to 4 hours to accommodate the increased computational demands of processing multiple large GWAS datasets.
- πAdded: New GWAS datasets to the
gwas_list.txtfile, including:- ISGC GigaStroke datasets for ALLSTROKE, IS, CES, LAS, and SVD subtypes.
- CHARGE cIMT (Franceschini 2018) and CHARGE Plaque (Franceschini 2018) datasets.
- π οΈUpdated: The
gwas_list.txtfile to ensure consistency in formatting and correct file paths. - π οΈUpdated: Changed the SLURM parameters for
harmonia.array_for_submit.sh.
- πAdded: A notebook to test drive some functions and option using
gwaslab.- New functionality to save QC-filtered output in
gwaslab.process.ipynb. - Plots for QC-filtered dataset in
gwaslab.process.ipynb. - Extraction of lead SNPs in
gwaslab.process.ipynb.
- New functionality to save QC-filtered output in
- πAdded: New script to process a given GWAS using
gwaslab. This script will:- Load the GWAS summary statistics.
- Perform liftover if necessary.
- Check reference alleles and flip if needed.
- Check for duplicates and remove them.
- Check for strand issues and resolve them.
- Check for allele frequency issues and filter variants accordingly.
- Perform QC filtering.
- Generate plots for both the full dataset and the QC-filtered dataset.
- Extract lead SNPs from the QC-filtered dataset.
- Ensure the
stemvariable is defined for both normal and --only-qc paths, allowing consistent file naming across different branches of the code. - Updated plotting functions in
gwas_process.pyto include verbose logging and ensure that plots are saved with the correct DPI settings. - Handles the case where a pickle file was created and the --only-qc flag is used to regenerate plots without re-running the full pipeline.
- π οΈUpdated: The
LICENSEfile to correct the copyright year. - π οΈUpdated: The
.gitignorefile to include new directories and files that should be ignored by git. - π οΈUpdated: The
CHANGES.mdfile to document the new functions and updates made to the codebase. - π οΈUpdated: The
README.mdfile to reflect the new functionality and provide instructions for using the new script and notebook. - π οΈUpdated: The
gwas_process.pyfile to include the new script for processing GWAS summary statistics and to ensure that thestemvariable is defined in all relevant branches of the code. - πAdded: Scripts for submitting GWAS processing jobs:
harmonia.submit.sh: A shell script to submit a GWAS processing job to a cluster usingsbatch.harmonia.array_for_submit.sh: A shell script to submit an array of GWAS processing jobs for multiple datasets or parameters. This is controlled by theharmonia.submit.shscript, which can be configured to run multiple instances of the processing script with different arguments.gwas_list.txt: A text file containing a list of GWAS datasets to be processed. This file is used by theharmonia.array_for_submit.shscript to determine which datasets to process in the array job. Each line in the file should specify a GWAS dataset, and the processing script will read this file to know which datasets to run on.