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
KPIs / Acceptance criteria
Scope / non-goals
Standards checklist
Problem
aa.load_datasetproduces different output in different processes, on its deterministic path (random=False, the default). Two independent causes, both from iterating aset.1. Class blocks are ordered by set iteration, so the returned rows differ
_load_dataset.py:312doeslabels = set(df_seq[ut.COL_LABEL])and then concatenates one block per label. For theAA_*datasets the labels are strings, and Python randomises string hashing per process, so the block order — and therefore which rows survive anncut — changes between runs: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:list_non_canonical_aacomes from aset, 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:re.subonACDVWYUX[-UX]ACDVWY--[U-X]ACD--Y--[X-U]re.error: bad character range X-USo a run can silently corrupt valid residues, or crash, depending on the process. Note also that
list_non_canonical_aais 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
removebranch (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_datasetreturns the same frame for the same arguments in any process, and the non-canonical handling treats its characters as literals.Requirements
sorted(labels)or the order the labels appear in the file) so the result does not depend on hash seed.re.escape), so-can never form a range and a metacharacter can never widen a match.df.values, unless there is a reason it must cover entry names.AA_*outputs in notebooks and any frozen fixture, so it is a deliberate output change that needs a re-execution pass.KPIs / Acceptance criteria
PYTHONHASHSEED0..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 containingU,Xand canonicalV/W.re.erroris reachable from any bundled dataset and any ordering.DOM_*/SEQ_*) datasets stay byte-identical to today.Scope / non-goals
data_handling, no new dependency.aaanalysis/_data/.random_stateseed): that fixes the sampling, this fixes the ordering and the character handling, and both are needed for a reproducible load.Standards checklist