Skip to content

E1 Phase 0: HFSP rebuild, pair deduplication, and a uniform embedding cohort - #5

Open
tsenoner wants to merge 6 commits into
mainfrom
fix/hfsp-rebuild-e1
Open

E1 Phase 0: HFSP rebuild, pair deduplication, and a uniform embedding cohort#5
tsenoner wants to merge 6 commits into
mainfrom
fix/hfsp-rebuild-e1

Conversation

@tsenoner

@tsenoner tsenoner commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Phase 0 of the resubmission. Rebuilds the pair table on a corrected HFSP score, removes a
systematic weighting bias, and makes all embedding arms cover the same proteins.

What changed

HFSP + search parameters. --max-seqs is pinned to 300, the value that actually produced
the pair table on disk and the one Mahlich et al. 2018 used (their Methods set
--alignment-mode 3, --num-iterations 3, --e-profile 1e-10, -e 1e-3 and never
--max-seqs). The script had claimed 1000, which did not describe the data. Foldseek stays at
1000, matching its own table.

Pair deduplication. The profile search is directional, so a pair may be reported as (A,B),
as (B,A), or both. Nothing collapsed them, so 61.5% of unordered sequence pairs carried double
weight. That weighting is not random — duplicated pairs are measurably the more similar ones
(mean fident 0.5158 vs 0.4142; median E-value 1.34e-59 vs 8.74e-35), so the raw table inflated
exactly the sequence-similarity signal under review. It also fed conflicting targets to the
symmetric probe arms for 30.9% of sequence and 44.9% of structural pairs while the
concatenating arms never saw the conflict, biasing the capacity ladder.

Sequence side keeps the lowest-E-value alignment whole (HFSP is non-linear in (PIDE, L), so
averaging would fabricate an alignment that never existed); structure side takes the mean
alntmscore, which is symmetric by definition — verified on pairs whose proteins differ in
length, 41.7% carry identical scores both ways versus 12.6% matching a target-normalised model.

Uniform cohort. The 15 arms did not cover the same proteins, and a pair is dropped when
either protein is missing, so the loss is quadratic. The cohort is now proteins of ≤2000
residues (540,881 of 542,238) — the pipeline's own --max_seq_len default, which three arms
were already built to. Membership collapses from seven patterns to two: 526,871 present
everywhere, 14,010 missing only clean/esm1b. Intersection 78.00% → 97.41%.

clean/esm1b stay at 526,871 by design; cutting to ESM-1b's 1022-token cap would cost every arm
a further 14,010 proteins to accommodate one model. The discrepancy is documented instead.

Verification

Every predicted number was stated before the run and matched exactly: MMseqs side
68,976,426 retained without dedup and 40,916,356 with; Foldseek 185,683,143 / 98,893,186;
totals 187,450,209 / 99,803,629. --no-dedupe reproduces the legacy table under a distinct
filename so the HFSP delta stays separately attributable.

Suite: 1180 passed, 2 skipped (was 1160). New tests cover the dedup rules, the cohort filter,
and the coverage set maths.

Also here

  • plm figures coverage-upset plus a committed coverage freeze, so the figure redraws offline
    (enumerating 542k HDF5 keys costs ~3 min/file over GPFS).
  • load_h5_keysets uses the core driver — measured 66.8x on esm1b.h5 (161.7s → 2.4s),
    size-guarded at 4.0e9 against the login node's 4 GiB per-user cgroup.
  • Two LRZ jobs: completing the interrupted ESM2-3B embeddings, and building the cohort.

🤖 Generated with Claude Code

https://claude.ai/code/session_011cdSkVB9XdhjZP4smvX44Z

tsenoner and others added 6 commits August 5, 2026 19:45
Mahlich et al. 2018 (Bioinformatics 34:i304-i312), whose HFSP score this
pipeline reproduces, set --alignment-mode 3, --num-iterations 3,
--e-profile 1e-10 and -e 1e-3 but never --max-seqs -- i.e. MMseqs2's
default of 300. The sequence table on disk is at exactly 300; the script's
stated default of 1000 did not describe the data it produced.

Foldseek stays at 1000, matching its own table. HFSP is defined only
against the sequence search, so binding the structural search to 300 would
buy no methodological fidelity while invalidating every alntmscore probe.

Both caps bite and both belong in Methods: 234,571 of 542,238 sequence
queries (43.3%) sit exactly at 300, contributing 68.7% of all rows, versus
11.8% of queries / 22.9% of rows for Foldseek at 1000. The prefilter ranks
by score, so the dropped hits are the remote ones -- the pair set is
enriched for high-identity pairs inside large families.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011cdSkVB9XdhjZP4smvX44Z
…l pair

--num-iterations 3 makes the search a profile search, which is directional:
a pair may be reported as (A,B), as (B,A), or as both, with different
alignments. Nothing collapsed them, so 61.5% of unordered sequence pairs
(74.9% of structural ones) carried double weight while the rest carried
single weight.

That weighting is not random. Duplicated pairs are measurably the MORE
similar ones -- mean fident 0.5158 vs 0.4142, mean alntmscore 0.8304 vs
0.6319, median E-value 1.34e-59 vs 8.74e-35. The raw table therefore
inflates exactly the sequence-similarity signal the reviewers questioned.
It also feeds conflicting targets to the symmetric probe arms
(linear_distance, euclidean) for 30.9% of sequence and 44.9% of structural
pairs, while the concatenating arms never see the conflict -- biasing the
capacity ladder.

The two sides get deliberately different rules:
  sequence  -- keep the single lowest-E-value alignment. HFSP is non-linear
               in (PIDE, L), so averaging would fabricate an alignment that
               never existed and desync fident/nident/mismatch.
  structure -- mean alntmscore. It is a lone scalar, and symmetric by
               definition: verified on pairs whose proteins differ in
               length, 41.7% carry identical scores both ways versus 12.6%
               matching a target-normalised model, so Foldseek #312 does
               not apply here. Max would re-bias the duplicated subset up.

--no-dedupe reproduces the legacy directional table and writes it to
merged_protein_similarity_nodedup.parquet, so it can never overwrite the
canonical one and the HFSP delta stays separately attributable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011cdSkVB9XdhjZP4smvX44Z
The 15 embedding arms do not cover the same proteins, and
src/shared/datasets.py:99-105 drops a pair when EITHER protein is missing,
so coverage loss is quadratic. esm2_3b was trained and scored on 558,947
test pairs where ten other arms got 872,572, yet is published at rank #10.
A ranking is only a ranking if every row was scored on the same data, so
this figure is the honest disclosure of the input to that claim.

Measured: only 422,972 of 542,238 proteins (78.00%) are in every arm, in
five tiers -- 542,238 complete; 542,237 (one outlier, Q9H195, the longest
protein in the cohort at 13,477 aa, which ProtT5 could not embed and
prottucker/random_1024 inherit); 540,881 (default --max_seq_len 2000);
526,871 (ESM-1b's 1022-token cap, which CLEAN inherits by construction);
435,298 (esm2_3b, an interrupted run, being completed). Once esm2_3b
lands the intersection rises to 526,871 and stops there -- ESM-1b's cap
then binds, which is why topping arms up can never yield a uniform test
set and intersecting is the only construction that can.

freeze/embedding_key_coverage.json (2.2 KB) is committed and is the
default source, so the figure redraws offline: the .h5 files live on
LRZ/Zenodo and enumerating 542k HDF5 keys costs ~3 min per file over
GPFS. Same philosophy as the pair index -- the repo pins the manifest, the
image is a reproducible build product. Integrity is asserted on write: the
patterns must reconstruct counts, universe and intersection exactly.

Set maths is separated from rendering and pinned by tests; --h5-dir
rebuilds from the HDF5 files, caching a <stem>.keys.txt sidecar stamped
with (size, mtime) so a changed file invalidates its own cache.
Exposed as `plm figures coverage-upset`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011cdSkVB9XdhjZP4smvX44Z
esm2_3b.h5 holds 435,298 of the cohort's 542,238 proteins. The gap is an
interrupted run, not a filter: present and missing length distributions
are indistinguishable (median 295 vs 296, mean 353.3 vs 354.2, p95 860 vs
864, >1024 aa 2.8% in both) and the cohort's longest protein was embedded.
So an append-mode resume is the correct fix -- _partition_cohort filters
already-present proteins before any forward pass.

Three traps handled:
  --max_seq_len 6000: the DEFAULT IS 2000 and silently skips longer
    sequences, and the file already holds 1,092 proteins above it. The
    missing set maxes at 5,412, so 6000 covers all of them.
  no --bf16: the existing 435,298 vectors are float32; mixing precision
    inside one arm is the confound the reviewers objected to.
  a key-count assertion instead of an exit code: the append writer counts
    a skip as a success (embedding_generation.py:965), so a misconfigured
    resume writes nothing, logs success and exits 0.

It seeds a working copy so the md5-verified Zenodo deposit is never
appended to. Partition measured with --test-only on 2026-08-05: MIG starts
same-day where H100 was 2026-08-18 and A100-80x4 2026-08-19; shorter
--time moved none of them, so queue position rather than backfill binds.
MIG needs BOTH --qos=mig and the typed --gres=gpu:3g.20gb:1.

Also gitignores docs/manuscript_changes.md, the internal ledger of changes
that must reach the manuscript text (origin is public; same rule as D-8).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011cdSkVB9XdhjZP4smvX44Z
The 15 arms do not cover the same proteins, and _load_and_filter_data drops
a pair when EITHER protein is missing from that arm's HDF5, so the loss is
quadratic in coverage. Measured on the published grid, esm2_3b was scored
on 558,947 test pairs where ten other arms got 872,572, yet is reported at
rank #10. A ranking whose rows were scored on different data is not a
ranking.

Exclusion rather than completion, because one gap cannot be closed:
ESM-1b's learned positional embeddings cap at 1022 tokens, so ~2.8% of the
cohort can never be embedded by it and CLEAN inherits exactly that set by
construction. Topping arms up can therefore never yield a uniform test set;
intersecting is the only construction that can. The single longest protein
(Q9H195, 13,477 aa, which ProtT5 could not embed and prottucker and
random_1024 inherit) is the smallest instance of the same rule.

A load-time filter over a committed id list, NOT deletion from the .h5
files: those are the md5-verified Zenodo deposit, deletion is irreversible,
and each file would stop matching its published checksum. The freeze stores
the EXCLUDED ids because that half is ~34x smaller (15,367 vs 526,871).

No freeze committed yet => empty exclusion => this is a no-op, so adopting
the cohort is an explicit act rather than an accident. The list itself is
generated once esm2_3b finishes: doing it now would bake in 119,266
exclusions instead of 15,367 and discard 103,899 proteins for nothing.

exclusion_summary separates "removed" from "excluded but already absent
here", so the filter cannot look like it did more work on the deficient
arms than it did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011cdSkVB9XdhjZP4smvX44Z
One pass over each arm does three jobs at once, because each is a full
rewrite anyway:

  CONSOLIDATE  15 arms live in data/zenodo_17469268/ while
    run_experiments.py:49 globs <cohort>/embeddings/*.h5 -- it would find 2
    files, print "Found 2 embedding files" and report success (blocker B2).
    The directory is also named after the VERSION DOI, while the concept DOI
    17469267 is the one to cite, and a new version is imminent.
  CUT to <=2000 aa  the cohort becomes 540,881 of 542,238. Not an invented
    threshold: it is the pipeline's own --max_seq_len default, and
    esm3_open/esmc_300m/esmc_600m already sit at exactly 540,881.
  REPACK  `del f[key]` unlinks without reclaiming space, so deleting in
    place would ship a new deposit at the old size with dead blocks inside.

Measured result (LRZ, 2026-08-06), now committed as
freeze/embedding_key_coverage_cohort2k.json: 13 arms hold the full 540,881
and the membership structure collapses to TWO patterns -- 526,871 present
everywhere, 14,010 missing only clean/esm1b. Before the cut it was seven
patterns and a 422,972 intersection (78.00%); after, 97.41%.

clean/esm1b stay at 526,871 by design. Cutting to ESM-1b's 1022-token cap
would cost every arm a further 14,010 proteins to accommodate one model's
positional limit, so the discrepancy is documented instead. Because a pair
needs both proteins, 97.41% protein coverage is ~94.9% of pairs for those
two arms -- disclose wherever either appears in a ranking.

esm2_3b is sourced from the completed working copy, not the stale deposit.
Its 51 CUDA-OOM failures are ALL >2000 aa (min 2,414, max 5,412), so the
cut removes exactly the proteins that failed: esm2_3b lands at 540,881.

Also applies the measured HDF5 fix to load_h5_keysets: these are
superblock-v0 symbol-table groups, ~0.52 metadata reads per key at 356 B,
and on GPFS an identical 356 B read costs 3.0 us within an 8.4 MB span vs
175 us across 4.62 GB. The core driver reads the file in one sequential
pass instead -- measured 66.8x on esm1b.h5 (161.7 s -> 2.4 s), key sets
byte-identical. Size-guarded at 4.0e9: a 3.53 GB file peaked at 3.70 GB RSS
under the login node's 4 GiB per-user cgroup while a 4.62 GB one was
SIGKILLed, and it must not run concurrently.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011cdSkVB9XdhjZP4smvX44Z
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant