Skip to content

load_dataset is not deterministic: set-ordered class blocks, and a regex character class that can gap canonical residues #588

Description

@breimanntools

Problem

aa.load_dataset produces different output in different processes, on its deterministic path (random=False, the default). Two independent causes, both from iterating a set.

1. Class blocks are ordered by set iteration, so the returned rows differ

_load_dataset.py:312 does labels = set(df_seq[ut.COL_LABEL]) and then concatenates one block per label. For the AA_* datasets the labels are strings, and Python randomises string hashing per process, so the block order — and therefore which rows survive an n cut — changes between runs:

$ for s in 0 1 2; do PYTHONHASHSEED=$s python -c "..."; done
hashseed=0  entries: CASPASE3_1_pos126, _pos127, CASPASE3_2_pos116   digest bcd67197
hashseed=1  entries: CASPASE3_1_pos126, _pos127, CASPASE3_2_pos116   digest bcd67197
hashseed=2  entries: CASPASE3_1_pos4,   _pos5,   _pos6               digest bcf9dec9

Different proteins, from the same call, with no randomness requested. Measured over the bundled data: 30 of 83 dataset/variant digests change across PYTHONHASHSEED.

2. non_canonical_aa='gap' / 'keep' builds a regex character class from a set

_load_dataset.py:90:

df[ut.COL_SEQ] = [re.sub(f'[{"".join(list_non_canonical_aa)}]', ut.STR_AA_GAP, x) for x in df[ut.COL_SEQ]]

list_non_canonical_aa comes from a set, so its order varies — and the gap symbol is -, which inside a character class means range. The same input therefore yields three different behaviours depending on hash order:

class re.sub on ACDVWYUX
[-UX] ACDVWY-- correct
[U-X] ACD--Y-- canonical V and W silently replaced by gaps
[X-U] — re.error: bad character range X-U

So a run can silently corrupt valid residues, or crash, depending on the process. Note also that list_non_canonical_aa is built from every column of the frame (vf(df.values)), so entry names contribute digits, _ and lowercase letters to that class, which widens the range hazard considerably.

The sibling remove branch (line 87) joins with | instead, so ordering does not change which rows match there — but the characters are equally unescaped, so a regex metacharacter appearing in any column (. is the dangerous one) would silently match everything.

Goal

load_dataset returns the same frame for the same arguments in any process, and the non-canonical handling treats its characters as literals.

Requirements

  • Order the label blocks deterministically (sorted(labels) or the order the labels appear in the file) so the result does not depend on hash seed.
  • Build the non-canonical character handling from a sorted list and escape every character (re.escape), so - can never form a range and a metacharacter can never widen a match.
  • Restrict the character scan to the sequence column rather than df.values, unless there is a reason it must cover entry names.
  • Decide the compatibility question: fixing (1) changes the committed row order of the AA_* outputs in notebooks and any frozen fixture, so it is a deliberate output change that needs a re-execution pass.

KPIs / Acceptance criteria

  • The same call under PYTHONHASHSEED 0..4 yields byte-identical frames for all bundled datasets; asserted in a test that runs the load in a subprocess.
  • non_canonical_aa='gap' never replaces a canonical residue: asserted on a sequence containing U, X and canonical V/W.
  • No re.error is reachable from any bundled dataset and any ordering.
  • The int-labelled (DOM_* / SEQ_*) datasets stay byte-identical to today.

Scope / non-goals

Standards checklist

  • Frontend validates, backend trusts the frontend
  • pyright: no new diagnostics
  • A regression test per cause; the subprocess test is the only honest way to exercise hash-seed variation

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    prio:1Very importanttopic:dataData related improvementstype:bugSomething isn't working

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions