Repository navigation
Conversation
…rs in one-hot encoding
one_hot_dna, one_hot, tokenize, OneHot and ReorderedOneHot now encode a
lowercase letter like its uppercase form. A letter in neither alphabet
nor neutral_alphabet raises a ValueError that names it. Before,
one_hot_dna and OneHot() turned both into rows of zeros, while one_hot,
tokenize and OneHot with other arguments raised a KeyError. So soft-masked
repeats became rows of zeros, and the result of OneHot depended on its
code path.
one_hot_dna stays a vectorized table lookup, and its table now has a row
for each of the 256 byte values. Sequences with unknown or non-ASCII
letters go through one_hot, so both functions give the same values and
errors. Letters that differ only in case but encode differently, e.g. in
alphabet="ACGTacgt", raise a ValueError.
one_hot and tokenize now also accept an empty sequence instead of
raising an IndexError.
BREAKING CHANGE: lowercase letters encode like uppercase ones, and
letters in neither alphabet nor neutral_alphabet raise a ValueError
instead of encoding as rows of zeros or raising a KeyError. To encode
IUPAC codes such as R or Y like N, add them to neutral_alphabet, e.g.
neutral_alphabet=list("NRYSWKMBDHV"). MIGRATING.md lists the changes.
The transforms now differ from kipoiseq, so the README points to MIGRATING.md for the differences.
…strings
A str was ambiguous: one_hot_dna read neutral_alphabet="NRY" as three
letters, while one_hot, tokenize and OneHot read it as one letter of
three characters and failed with an empty AssertionError.
one_hot_dna, one_hot, tokenize, one_hot2string, OneHot and
ReorderedOneHot now take alphabet and neutral_alphabet only as a list or
tuple of strings. They raise a TypeError for a str, for another type and
for elements that are not strings. The message of the TypeError names
both readings of the str, e.g. list("NRY") or ["NRY"]. An empty
neutral_alphabet stays allowed.
The defaults of neutral_alphabet become ("N",), and calls with the
defaults return the same arrays as before. one_hot_dna no longer splits
the elements of neutral_alphabet into characters, and ReorderedOneHot no
longer calls utils.parse_alphabet.
BREAKING CHANGE: alphabet and neutral_alphabet must be lists or tuples
of strings, and a str raises a TypeError. alphabet="ACGT" becomes
alphabet=list("ACGT"), and neutral_alphabet="N" becomes
neutral_alphabet=["N"]. one_hot_dna(seq, neutral_alphabet=["NRY"]) now
raises a ValueError: pass list("NRY") instead.
tokenize checked its input with bare assert statements. So
one_hot("ACGT", neutral_alphabet=["NRY"]) failed with an empty
AssertionError, and under python -O the checks did not run.
tokenize, and so one_hot, OneHot and ReorderedOneHot, now raise a
ValueError if the letters of alphabet and neutral_alphabet differ in
length, or if len(seq) is not a multiple of the letter length. The
message gives alphabet and neutral_alphabet, or len(seq). For an invalid
alphabet_axis, ReorderedOneHot raises a ValueError instead of an
AssertionError too.
Hoeze
added this pull request to stack #16
October 15, 2026 22:32
Hoeze
force-pushed
the
feat/one-hot-case-insensitive
branch
from
October 16, 2026 19:49
78b78f8 to
6c7fc2a
Compare
Hoeze
commented
Oct 17, 2026
| from dataclasses import dataclass | ||
|
|
||
|
|
||
| def _as_letters(letters: object, name: str) -> tuple[str, ...]: |
Member
Author
There was a problem hiding this comment.
remove the name parameter and give the function a better name
ReorderedOneHot served the axis layouts of the kipoi dataloaders. np.expand_dims and np.swapaxes give each of these layouts from the one-hot array, and a test checks their shapes. MIGRATING.md shows the replacement. BREAKING CHANGE: ReorderedOneHot is removed. Reorder the axes of the one-hot array x with numpy instead, e.g. ReorderedOneHot(alphabet_axis=0, dummy_axis=1) becomes np.swapaxes(np.expand_dims(x, 1), 0, 2). get_output_shape has no replacement.
Hoeze
force-pushed
the
feat/one-hot-case-insensitive
branch
from
October 17, 2026 14:08
71975ea to
377e2f2
Compare
The new module kipoiseq2.alphabets holds the letters of the encodings. Alphabet is a frozen dataclass. It holds the letters in the order of the one-hot columns and the tokens. Its mapping ambiguous holds the letters that each ambiguity code stands for, e.g. R for A or G. A code for all letters, e.g. N, is neutral. Alphabet checks the letters and the codes once. It raises a TypeError for a str, which can mean several letters or one letter of several characters, and for letters that are not strings. It raises a ValueError for no letters, empty letters, letters of different lengths and letters that repeat. It also raises a ValueError for a code that is a letter or that differs in length from the letters, and for a code with fewer than 2 letters, with a repeated letter or with a letter that is not in the alphabet. It compares letters exactly, so "A" and "a" are different letters. - Alphabet stores the letters of each code in the order of the letters, so equal sets compare equal. It keeps the order of the codes, because this order sets their tokens. So two alphabets are equal only if their codes have the same order. - The presets DNA and RNA have N and the other IUPAC ambiguity codes, because GRCh37 and GRCh38 contain a few of them. N comes first. - AMINO_ACIDS has the 20 standard amino acids as letters, and X, B and Z as ambiguity codes. - The package kipoiseq2 exports Alphabet and the presets. - The docs show the defaults as in the source, e.g. DNA.letters instead of the tuple of letters. BREAKING CHANGE: kipoiseq2.utils.DNA, RNA, AMINO_ACIDS and alphabets are removed. Use the presets DNA, RNA and AMINO_ACIDS of kipoiseq2.alphabets instead. Their letters are DNA.letters and so on. AMINO_ACIDS.letters holds 20 amino acids instead of 22, because B and Z are ambiguity codes now.
Hoeze
force-pushed
the
feat/one-hot-case-insensitive
branch
from
October 17, 2026 16:22
377e2f2 to
6c7d344
Compare
The new module kipoiseq2.encoders has two frozen classes. Encoder one-hot encodes sequences, and Tokenizer converts them to integer tokens. Each instance holds the complete configuration of its output. Its constructor checks the configuration and builds the lookup once, so its method only applies it. The package kipoiseq2 exports both classes. - Encoder(alphabet=DNA, *, ambiguous="zeros", dtype=np.uint8) has the method one_hot(seq). It returns an array of shape (L, len(alphabet.letters)). The rule ambiguous sets the rows of the ambiguity codes of the alphabet. "zeros" gives a row of zeros. "fractional" gives 1/k in the columns of the k letters of a code, so N gives 0.25 in every DNA column and R gives [0.5, 0, 0.5, 0]. "ones" gives 1 in each of these k columns, in any dtype, so N gives [1, 1, 1, 1] and R gives [1, 0, 1, 0]. It keeps the letters of each code in integer dtypes, but its rows do not sum to 1. - The method decode(x) converts the array back. It maps a row to the letter or code with this row. With "zeros", all codes share the row of zeros, so this row becomes the first code, e.g. N. With "fractional" and "ones", each code has its own row. decode raises a ValueError for any other row. - Tokenizer(alphabet=DNA, *, token_map=None, dtype=np.uint8) has the method tokenize(seq). It maps letters[i] to i, and each ambiguity code to a token of its own after the letters, in the order of the codes. For DNA, N is 4, R is 5 and V is 14. So the token of a letter or code is the index of its row in the table of Encoder. token_map sets other tokens, e.g. the vocabulary of a pretrained model, and several letters and codes can share one. Its keys are the letters and the codes of the alphabet, so 15 keys for DNA. - The constructors raise a TypeError for an argument of a wrong type, also for dtype=None, which numpy reads as float64. They raise a ValueError for another rule than "zeros", "fractional" or "ones", for "fractional" with a dtype that is not a floating-point type, or if the keys of token_map are not the letters and the codes. - Both classes compare by their configuration, hash and pickle, e.g. for the worker processes of a data loader. They are safe as default arguments. - The lookup is case-sensitive, so a lowercase letter no longer encodes like its uppercase form. A letter that is not in the alphabet raises a ValueError that names it. If the alphabet has its uppercase form, the message points to seq.upper() and to FastaStringExtractor(force_upper=True). - For letters of one ASCII character, the lookup translates the bytes of the sequence with a table of 256 bytes and takes the rows of the result. decode builds the str from the bytes of these letters instead of joining them. Other letters, e.g. codons, use a dict and a join. - decode checks the rows with a loop over the columns, because numpy reduces an axis of a few values slowly. With "fractional" and "ones", it compares only the rows that are not the row of a letter with the rows of the codes. The times per call for random ACGTN, against fa84de8, an earlier version of this change, are medians of many calls: 1 kb, tokens 4.0 us -> 1.1 us 1 kb, one-hot float32 4.0 us -> 2.6 us, or 2.4 us in uint8 196,608 bases, tokens 202 us -> 55 us 196,608 bases, one-hot 229 us -> 204 us, or 183 us in uint8 The ambiguity codes replace the neutral letters of fa60431, an earlier version of this change. Against fa60431, tokens and one-hot keep their times. decode gets faster, except in float32 for 1 kb. float32 means neutral_value=0.25 before and "fractional" after. Each case ran in its own process, alternating before and after, 3 times. The times are the medians of these 3 processes, each the median of 21 runs: 1 kb, tokens 1.13 us -> 1.14 us 1 kb, one-hot uint8 2.55 us -> 2.58 us 1 kb, one-hot float32 2.65 us -> 2.66 us 1 kb, decode uint8 33 us -> 26 us 1 kb, decode float32 34 us -> 36 us 196,608 bases, tokens 59 us -> 58 us 196,608 bases, one-hot 196 us -> 197 us in uint8 196,608 bases, one-hot 228 us -> 229 us in float32 196,608 bases, decode 2.8 ms -> 2.2 ms in uint8 196,608 bases, decode 1.27 ms -> 0.96 ms in float32 With "ones" in uint8, one-hot takes 2.57 us for 1 kb and 197 us for 196,608 bases, and decode takes 35 us and 2.6 ms. README.md shows the encoders and states the case rule. MIGRATING.md maps each removed name to its replacement. BREAKING CHANGE: one_hot_dna, one_hot, tokenize, token2one_hot, one_hot2token, one_hot2string and OneHot are removed. Use Encoder(alphabet, ...).one_hot(seq), Encoder.decode and Tokenizer(alphabet, ...).tokenize(seq) instead. They take an Alphabet instead of alphabet and neutral_alphabet. A neutral letter becomes an ambiguity code for all letters. Encoder returns uint8 and encodes the codes as rows of zeros by default, and Encoder(DNA, ambiguous="fractional", dtype=np.float32) gives the old result for N. neutral_value is gone: 0 becomes ambiguous="zeros", 0.25 becomes ambiguous="fractional" with a float dtype, and 1 becomes ambiguous="ones". The encoders are case-sensitive, so lowercase letters raise a ValueError. one_hot_dna encoded unknown letters as rows of zeros. With DNA, the IUPAC ambiguity codes now encode by the rule ambiguous, and other unknown letters raise a ValueError. Tokenizer encodes each code as a token of its own after the letters instead of -1.
Hoeze
force-pushed
the
feat/one-hot-case-insensitive
branch
from
October 17, 2026 17:19
6c7d344 to
affe21f
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
tl;dr: the new module
kipoiseq2.encodershas the frozen classesEncoderandTokenizer, which replaceone_hot,tokenizeand their helpers. AnAlphabetholds the letters and the letters that each ambiguity code stands for. The ruleambiguousofEncodersets the rows of the codes:"zeros","fractional"or"ones".Tokenizergives each code its own token. The lookup is case-sensitive, and a letter that is not in theAlphabetraises a ValueError that names it.Encoder(alphabet=DNA, *, ambiguous="zeros", dtype=np.uint8)one-hot encodes withone_hot(seq). It returns an array of shape(L, len(alphabet.letters)). A letter gives 1 in its column, and a code of k letters gives the row of the ruleambiguous:"zeros"gives a row of zeros."fractional"gives 1/k in the columns of the letters of the code, soNgives 0.25 in every DNA column andRgives[0.5, 0, 0.5, 0]. It needs a floating-point dtype."ones"gives 1 in the columns of the letters of the code, in any dtype, soNgives[1, 1, 1, 1]andRgives[1, 0, 1, 0]. It keeps the letters of each code in integer dtypes. Its rows do not sum to 1, so a model trained on one-hot input seesNas a strong signal.decode(x)converts the array back to a str. It maps each row to the letter or code with this row, and the first code wins if several codes share a row. With"zeros", all codes share the row of zeros, so this row becomes the first code, e.g.N. With"fractional"and"ones", each code has its own row, sodecode(one_hot(seq))equalsseq.decoderaises a ValueError for any other row.Tokenizer(alphabet=DNA, *, token_map=None, dtype=np.uint8)converts a sequence to integer tokens withtokenize(seq). By default,letters[i]becomes the tokeni, and each code becomes a token of its own after the letters, in the order of the codes. ForDNA,Nis 4,Ris 5 andVis 14. So the token of a letter or code is the index of its row in the table ofEncoder.token_mapsets other tokens, e.g. the vocabulary of a pretrained model, and several letters and codes can share one. Its keys are the letters and the codes of the alphabet, so 15 keys forDNA.Both constructors check the configuration once. They raise a TypeError for a wrong type, also for
dtype=None, which numpy would silently read as float64. They raise a ValueError for a rule other than"zeros","fractional"or"ones", for"fractional"with a dtype that is not a floating-point type, or if the keys oftoken_mapare not the letters and the codes. Both classes compare by their configuration, hash, have a repr and pickle, e.g. for the worker processes of a data loader. They are safe as default arguments.The new module
kipoiseq2.alphabetshas the frozen dataclassAlphabet(letters, ambiguous={code: letters})and the presetsDNA,RNAandAMINO_ACIDS.DNAandRNAhaveNand the other IUPAC ambiguity codes, because GRCh37 and GRCh38 contain a few of them.AMINO_ACIDShas the 20 standard amino acids as letters and the codesX,BandZ. For a vocabulary without the IUPAC codes, e.g. the one of HyenaDNA, use the strictAlphabet(["A", "C", "G", "T"], ambiguous={"N": ["A", "C", "G", "T"]}).Alphabetraises a TypeError for a str, which the old functions read in two different ways. It raises a ValueError for letters that are empty, differ in length or repeat. It also raises a ValueError for a code that is a letter, differs in length from the letters, or stands for fewer than 2 letters or for letters outside the alphabet. Two alphabets are equal only with their codes in the same order, because the order sets the tokens. The packagekipoiseq2exportsAlphabet, the presets,EncoderandTokenizer.The lookup is case-sensitive, so a lowercase letter of a soft-masked repeat is unknown unless the alphabet contains it. A letter that is not in the alphabet raises a ValueError that names it. For ASCII lowercase letters whose uppercase form is in the alphabet, the message points to
seq.upper()and toFastaStringExtractor. Before,one_hot_dnaandOneHot()encoded unknown and lowercase letters as rows of zeros, and the other code paths raised a KeyError. NowDNAandRNAencode the IUPAC ambiguity codes by the ruleambiguous, and all other unknown letters raise.For letters of one ASCII character, the lookup translates the bytes of the sequence with a table of 256 bytes and takes the rows of the result.
decodebuilds the str from the bytes of these letters instead of joining them. Other letters, e.g. codons, use a dict and a join.decodechecks the rows with a loop over the columns, because numpy reduces an axis of a few values slowly. With"fractional"and"ones", it compares only the rows that are not the row of a letter with the rows of the codes. The times per call for random ACGTN are for 1 kb and for 196,608 bases, on Python 3.14 and numpy 2.5. Each case ran in its own process, 3 times. Each time is the median of these 3 processes, and each process reports the median of 21 runs:"ones"takes the same time."fractional": 2.66 us and 229 us.decodein uint8: 26 us and 2.2 ms with"zeros", and 35 us and 2.6 ms with"ones".decodein float32 with"fractional": 36 us and 0.96 ms.ReorderedOneHotis gone, becausenp.expand_dimsandnp.swapaxesgive each of its layouts from theEncoder.one_hotarray.tokenize,token2one_hot,one_hot2tokenandone_hot2stringare gone, becauseTokenizerandEncoder.decodereplace them. Letters of different lengths and alen(seq)that is not a multiple of the letter length raise a ValueError instead of an AssertionError.With
-1, numpy indexing took the last row, sonp.eye(4)[tokens]silently encodedNasT. Now a table with one row per letter raises an IndexError for the token of a code. An embedding withlen(alphabet.letters) + len(alphabet.ambiguous)rows takes the tokens as they are.README.md shows the encoders and states the case rule. MIGRATING.md lists each removed name with its replacement.
BREAKING CHANGE:
one_hot_dna,one_hot,tokenize,token2one_hot,one_hot2token,one_hot2string,OneHotandReorderedOneHotare removed. UseEncoder(alphabet, ...).one_hot(seq),Encoder.decodeandTokenizer(alphabet, ...).tokenize(seq)instead, andnp.expand_dimsandnp.swapaxesinstead ofReorderedOneHot. They take anAlphabetinstead ofalphabet,neutral_alphabetoralphabet_size. A neutral letter becomes an ambiguity code for all letters. The presets ofkipoiseq2.alphabetsreplacekipoiseq2.utils.DNA,RNA,AMINO_ACIDSandalphabets.AMINO_ACIDS.lettersholds 20 amino acids instead of 22, becauseBandZare codes now.Encoderreturns uint8 and encodes the codes as rows of zeros by default.Encoder(DNA, ambiguous="fractional", dtype=np.float32)gives the old result ofone_hot_dnaforN.neutral_valueis gone: 0 becomesambiguous="zeros", 0.25 becomesambiguous="fractional"with a float dtype, and 1 becomesambiguous="ones". The encoders are case-sensitive, so letters that are not in the alphabet, also lowercase ones, raise a ValueError. Uppercase the sequence first, e.g. withseq.upper(). WithDNAandRNA, the IUPAC ambiguity codes encode by the ruleambiguous. With"fractional", they give fractions of their bases, whereone_hot_dnagave rows of zeros. Atoken_mapneeds a key for each letter and code, so 15 keys forDNA.Tokenizerencodes each code as a token of its own after the letters instead of-1, and it returns uint8 by default.decodetakes the array of one sequence and returns a str.feat!: add kipoiseq2.alphabets with Alphabet and its presets
feat(transforms)!: remove ReorderedOneHot
fix(alphabets): raise ValueError for letters of different lengths