bitpattern
bitpattern provides BDDSet, a set type for values that can be encoded as fixed-width
integers: for instance, IEEE floats and twos-complement integers, unicode codepoints,
network addresses, UUIDs, dates and times, enums and tuples of any of these. BDDSets can
efficiently represent sets far too large to list and still support the usual set
operations, counting, uniform sampling and even indexing.
Consider for example 64 bit floats. How many finite normal floats are there? Can we sample them directly? These sets are huge and noncontiguous, so you can't express them in traditional data structures.
>>> from bitpattern.codecs import float64
>>> normal = float64.finite - float64.subnormal - float64.zeros
>>> normal
BDDSet(float64, [2.2250738585072014e-308, 2.225073858507202e-308, 2.2250738585072024e-308, 2.225073858507203e-308, ..., -1.7976931348623155e+308, -1.7976931348623157e+308], size=18428729675200069632)
>>> 0.25 in normal
True
>>> list(normal[:3])
[2.2250738585072014e-308, 2.225073858507202e-308, 2.2250738585072024e-308]
>>> normal.choice()
2.3171912436506225e+167
>>> float64.range(1.0, 2.0).size
4503599627370496
float64 is the codec that tells bitpattern how to encode float64 values
as integers.
Tour
Here's a quick tour, with sampling seeded so it's repeatable:
>>> import random, uuid
>>> from datetime import UTC, date, datetime
>>> from enum import Flag
>>> from bitpattern.codecs import (
... Flags, char, dates, datetimes, float8_e4m3fn, ipv4, project, uint16, uuid128,
... )
>>> rng = random.Random(0)
>>> # fp8
>>> float8_e4m3fn.finite.size
254
>>> float8_e4m3fn.positive[-1]
448.0
>>> float8_e4m3fn.decode(float8_e4m3fn.encode(0.3))
0.3125
>>> list(float8_e4m3fn.range(1.0, 2.0))
[1.0, 1.125, 1.25, 1.375, 1.5, 1.625, 1.75, 1.875]
>>> # Unicode characters
>>> "É" in char.category("Lu")
True
>>> "".join(char.category("Sm") & char.range("\0", "\x80"))
'+<=>|~'
>>> char.range("a", "{") == char.category("Ll") & char.range("\0", "\x80")
True
>>> # `a * b` is the set of pairs, ie. the cartesian product
>>> networks = ipv4.cidr("10.0.0.0/8") | ipv4.cidr("192.168.0.0/16")
>>> private = networks * uint16.range(1024, 65536)
>>> private.size
1086559617024
>>> ("10.1.2.3", 8080) in private
True
>>> private.choice(rng=rng)
(IPv4Address('10.100.105.211'), 22477)
>>> project(private, 1) == uint16.range(1024, 65536)
True
>>> # UUIDs
>>> uuid128.version(4).size == 2**122
True
>>> uuid.uuid4() in uuid128.version(4)
True
>>> uuid128.version(7).choice(rng=rng)
UUID('14ba5e5a-eaa5-7157-a3e7-0682c2094cac')
>>> # Dates and datetimes
>>> dates.range(date(2024, 1, 1), date(2025, 1, 1)).size
366
>>> new_year = datetime(2024, 1, 1, tzinfo=UTC)
>>> datetimes.range(new_year, datetime(2024, 1, 2, tzinfo=UTC)).size
86400000000
>>> # Flags
>>> class Permission(Flag):
... READ = 4
... WRITE = 2
... EXECUTE = 1
>>> permissions = Flags(Permission)
>>> list(permissions.having(Permission.READ))
[<Permission.READ: 4>, <Permission.READ|EXECUTE: 5>, <Permission.READ|WRITE: 6>, <Permission.READ|WRITE|EXECUTE: 7>]
>>> permissions.pattern("?1?") == permissions.having(Permission.WRITE)
True
Very large sets
bitpattern uses a data structure called a Binary Decision Diagram to encode
extremely large sets. Whereas set operations are typically described in terms
of the size of the set, bitpattern sets support most operations in O(#bits) of
the largest member of the set. #bits is called the width of the set.
Here's how bitpattern's sets compare to other ways of storing a set. n and m are
set sizes, w is the width (number of bits of the largest member), and |a| is the
number of nodes in a's diagram. |a| is at most n * w and usually much smaller. A
set written as a single pattern has |a| <= w, since only its fixed bits need nodes.
For most use cases w is a constant and may be read as O(1).
sorted list |
set |
balanced tree | BDDSet |
|
|---|---|---|---|---|
x in a |
O(log n) | O(1) | O(log n) | O(w) |
a[i] |
O(1) | n/a | O(log n) | O(w) |
len(a) |
O(1) | O(1) | O(1) | O(1) |
a | b, a & b, a - b |
O(n + m) | O(n + m) | O(n + m) | O(|a| · |b|) |
~a |
O(2w) | O(2w) | O(2w) | O(|a|) |
slice a[i:j] |
O(j - i) | n/a | O(log n + j - i) | O(w) |
a == b |
O(n) | O(n) | O(n) | O(1) |
hash(a) |
O(n) | O(n) | O(n) | O(w) |
| random member | O(1) | O(n) | O(log n) | O(w) |
| memory | O(n) | O(n) | O(n) | O(|a|) |
a == b is O(1) between sets of the same codec. Between IntSets of different widths
it's O(w).
Hypothesis
bitpattern's sets are particularly useful with hypothesis.
Hypothesis really likes you to express and sample from the true domain of your
input data. Rejection sampling (in hypothesis literally sampling from the whole
range and then calling reject on inputs that don't match your criteria) frequently
eliminates too much data. By default hypothesis will fail tests that reject
more than ~80% of inputs, but for instance subnormals are well under 1% of floats, so
this isn't practical.
bitpattern.strategies turns these sets into hypothesis strategies:
from hypothesis import given
from bitpattern.codecs import float64
from bitpattern.strategies import from_set
@given(from_set(float64.finite - float64.subnormal))
def test_kernel_matches_reference(x: float): ...
Patterns
The pattern syntax follows normal glob rules. Patterns are expressed as quartets of 4
bits separated by ., most significant to least significant, with 0 and 1 representing
a fixed bit, ? can be either, and * is shorthand for multiple ?. The leading quartet can
be shorter for widths that aren't a multiple of 4.
Patterns are sets, and work with normal set operations.
>>> from bitpattern import Pattern
>>> Pattern("*1.0000")
Pattern('???1.0000')
>>> Pattern("0000") | Pattern("0001")
Pattern('000?')
>>> ~Pattern("00??")
Pattern('01??') | Pattern('1???')
Codecs accept patterns too and match them against the bits of their encoding. A float64 is a sign bit, 11 exponent bits and 52 mantissa bits. Quiet NaNs have every exponent bit and the top mantissa bit set:
>>> quiet = float64.pattern("?111.1111.1111.1*.*.*.*.*.*.*.*.*.*.*.*.*")
>>> quiet <= float64.nan
True
Codecs
A Codec encodes values of a type as fixed-width integers and decodes them back. Its
sets are BDDSets. To write one, subclass Codec and implement encode and decode:
>>> from bitpattern import Codec
>>> class Ascii(Codec[str]):
... def encode(self, value: object) -> int:
... if not isinstance(value, str):
... raise TypeError(f"{value!r} isn't a string")
... if len(value) != 1 or not value.isascii():
... raise ValueError(f"{value!r} isn't an ASCII character")
... return ord(value)
...
... def decode(self, bits: int) -> str:
... return chr(bits)
>>> ascii = Ascii(7, "ascii")
>>> ascii.set("hello")
BDDSet(ascii, ['e', 'h', 'l', 'o'])
>>> ascii.pattern("1?0.0001") # case only changes bit 5
BDDSet(ascii, ['A', 'a'])
encode should raise TypeError for a value of the wrong type and ValueError for a
value it can't encode. decode may raise IndexError for bits that aren't a value.
If some bit patterns aren't values, override all with the ones that are. ~ and
pattern then leave the others out. If encodings sort the same as their values,
subclass OrderedCodec instead for min, max and range(start, stop).
bitpattern.codecs includes:
float16,float32andfloat64as their IEEE 754 bits, and the ML formatsbfloat16,float8_e5m2andfloat8_e4m3fn. They have sets likefinite,nan,infinities,subnormalandzeros.int8throughint128in two's complement, anduint8throughuint128.char, Unicode scalar values as one-character strings, withcategory(name)for general categories like"Lu"or"L".ipv4andipv6, withcidr("10.0.0.0/8").networks(addresses)finds the fewest CIDR blocks that make up a set.mac, MAC addresses like01:23:45:67:89:ab, withmulticast,unicast,local,universal,broadcastandoui(prefix).uuid128,uuid.UUIDs, withversion(n)for RFC 9562's versions 1 to 8 andrfc9562for its variant.datesanddatetimes.datetimesholds timezone-aware datetimes and decodes them into UTC.DateTime(tz)decodes intotzinstead, andnaive_datetimesholds naive datetimes.Members(SomeEnum)for an Enum's members, andFlags(SomeFlag)for a Flag's combinations, withhaving(flags)andlacking(flags).
All of these but the enums and flags are ordered, with min, max and
range(start, stop). A bound of None leaves that end unbounded.
product(a, b, c) is the set of tuples itertools.product(a, b, c) would give, and
a * b is product(a, b). Their codec is a Tuple, with the first field's bits on
top. A product has about as many nodes as its sets combined. project gets the values
of one field.
IntSet
IntSet backs BDDSets and patterns, and works on its own too. An IntSet is an
immutable set of non-negative integers below 2 ** width. It's a collections.abc.Set
and a Sequence of its members in sorted order. Slicing gives back a set.
An IntSet is a reduced, ordered binary decision diagram, or BDD (Bryant, 1986).
bitpattern.BDD is the diagram itself.
>>> from bitpattern import IntSet
>>> s = IntSet.range(3, 17)
>>> s
IntSet([3, 4, 5, 6, ..., 15, 16], size=14, width=5)
>>> s[2]
5
>>> s[2:5]
IntSet([5, 6, 7], width=5)
>>> s & IntSet([1, 2, 3, 4])
IntSet([3, 4], width=5)
>>> ~IntSet([1, 3], width=2)
IntSet([0, 2], width=2)
R. E. Bryant. Graph-Based Algorithms for Boolean Function Manipulation. IEEE Transactions on Computers, 35(8):677–691, 1986.
Metadata
Release files for bitpattern 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| bitpattern-0.2.0.tar.gz | 68.6 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| bitpattern-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 103.4 kB
Release files / bitpattern-0.2.0.tar.gz
| Download URL | bitpattern-0.2.0.tar.gz |
|---|---|
| Size | 68.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5256a4e4a1a95bfad8ea3d72fd1c4f3f5911aa6c1c77b52417cca37f5f464385
|
|
BLAKE2b-256 checksum How to use checksums |
51a707da973230a9d1ecde494f4d37a8cf9d3778c319a6b606fc13c37aed7234
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.0
|
Release files / bitpattern-0.2.0-py3-none-any.whl
| Download URL | bitpattern-0.2.0-py3-none-any.whl |
|---|---|
| Size | 34.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f7c92935b482c378eb9dcbc5db7293aec038f032319b4ecb726bc455f6e02183
|
|
BLAKE2b-256 checksum How to use checksums |
cde65e5481aa7c6900e65775a8f07f05774826893e1c4095ffd877555797f684
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.0
|
Security for bitpattern 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.