Skip to main content

Wrapper de int para numeros naturales grandes con slicing por bits

Project description

HugeNat

HugeNat es un wrapper ligero sobre int que mantiene la semántica de los enteros de Python para números naturales (ℕ₀) y añade indexado/slicing por bits, vistas NumPy y un núcleo listo para Numba.

Repositorio: https://github.com/nand0san/huge_nat

Instalación

pip install HugeNats

Creación rápida

import numpy as np
from hugenat import HugeNat

# Desde un entero no negativo
x = HugeNat(123456789)

# Desde un float exactamente entero (sin parte decimal, <= 2**53)
f = HugeNat(3.0)          # → HugeNat(3)
# HugeNat(3.5)            → ValueError: El float debe ser un entero exacto
# HugeNat(float(2**54))   → ValueError: demasiado grande para representación exacta

# Desde limbs (uint64, little-endian: limb 0 es LSB)
limbs = np.array([0xFFFFFFFFFFFFFFFF, 0x1], dtype=np.uint64)
y = HugeNat(limbs)

# Desde una lista/tupla de enteros (se convierten a uint64 y se recortan ceros finales)
z = HugeNat([1, 2, 3])

# Con anchura lógica fija: conserva ceros a la izquierda o trunca por arriba
padded = HugeNat(0b1011, bit_length=8)     # 00001011 (ancho lógico 8)
cut = HugeNat(0b110101, bit_length=4)      # 0101 (se recorta a 4 bits)

# Desde array uint64 big-endian (MSW primero) + bit_length obligatorio
words_be = np.array([0x0001, 0xFFFFFFFFFFFFFFFF], dtype=np.uint64)
w = HugeNat.from_words_be(words_be, 128)

API tipo int

  • int(x), float(x), bool(x), hash(x), str(x) reflejan al entero interno.
  • Métodos compatibles: bit_length(), bit_count(), to_bytes(), from_bytes().
  • Aritmética y bitwise aceptan solo naturales (HugeNat o int >= 0) y devuelven siempre HugeNat.
  • Las restas que producirían un valor negativo lanzan ValueError.
  • -x lanza ValueError (los naturales no tienen negación).
  • Si se declara bit_length al construir, bit_length() devuelve esa anchura lógica fijada.
  • Las operaciones bitwise (&, |, ^, <<, >>) preservan _fixed_nbits automáticamente cuando self tiene ancho fijo. Si ambos operandos son HugeNat con el mismo ancho fijo, el resultado lo hereda; si solo self lo tiene, también; si ambos difieren, el resultado no tiene ancho fijo. Los shifts aplican máscara al ancho fijo.
  • Operadores unarios: abs(x) y +x devuelven una copia (identidad para naturales, preservan bit_length fijo).
  • divmod(a, b) devuelve (HugeNat, HugeNat).
  • Todos los operadores reflejados están implementados, incluyendo ** (2 ** HugeNat(3) -> HugeNat(8)).
  • bool no se acepta como shift en <<, >>, rotl ni rotr; usa int explícito.
a = HugeNat(10)
b = HugeNat(7)

int(a + b)        # 17
int(a * b)        # 70
int(a // b)       # 1
int(a % b)        # 3
int(a << 3)       # 80
int(a | b), int(a & b), int(a ^ b)
float(a)          # 10.0

divmod(a, b)      # (HugeNat(1), HugeNat(3))
2 ** HugeNat(3)   # HugeNat(8)
abs(a)            # HugeNat(10)
-a                # ValueError: HugeNat no permite valores negativos

Indexado de bits

  • Convención: LSB = índice 0. Índices negativos son relativos a bit_length().
  • Fuera de rango devuelve 0.
x = HugeNat(0b1101101)  # 109

x[0]    # 1 (LSB)
x[-1]   # 1 (MSB)
x[100]  # 0

Slicing de bits

  • step en {None, 1} usa ruta rápida: normaliza como Python y rellena con ceros si el límite superior excede bit_length().
  • Cualquier otro step (salvo 0) usa ruta general con semántica completa de slicing de listas y reempaquetado LSB-first.
  • Con step < 0, si el índice inicial cae por encima de bit_length(), esos bits se consideran ceros implícitos.
  • step == 0 -> ValueError.
x = HugeNat(0b1101101)

x[0:3]        # bits 0..2 -> 0b101 (5)
x[2:5]        # 0b110 (6)
x[0:7:2]      # toma cada 2 bits -> 0b1011 (11)
x[5:0:-2]     # slicing con paso negativo
x[3:10]       # incluye ceros implícitos por encima de bit_length()

Anchura lógica fija (bit_length al construir)

Por defecto, HugeNat se comporta como antes: la anchura es int(x).bit_length().

Si pasas bit_length=..., fijas la anchura lógica del objeto:

  • si el valor tiene más bits, se trunca por arriba;
  • si tiene menos, se consideran ceros a la izquierda;
  • bit_length() devuelve siempre la anchura fijada.
x = HugeNat(0b1011, bit_length=8)

int(x)         # 11
x.bit_length() # 8
x[-1]          # 0  (bit más alto lógico)
x[-8]          # 1

División en trozos (split(n))

split(n) divide el número en n trozos de igual cantidad de bits.

  • n debe ser int y n >= 1.
  • bit_length() debe ser divisible por n, si no lanza ValueError.
  • Devuelve una tupla de HugeNat en orden LSB -> MSB (trozo de menor peso primero).
  • Cada trozo conserva su anchura lógica fija (bit_length del trozo).
  • Si bit_length() == 0, devuelve n trozos HugeNat(0, bit_length=0).
x = HugeNat(0b1011010110010001, bit_length=16)
parts = x.split(4)

[int(p) for p in parts]       # [0b0001, 0b1001, 0b0101, 0b1011] -> [1, 9, 5, 11]
[p.bit_length() for p in parts]  # [4, 4, 4, 4]

Concatenación de bits (append)

append(B, C, ...) concatena los bits de uno o más HugeNat sobre los de self.

  • Los bits de self quedan en las posiciones bajas (LSB), los de B justo encima, los de C encima de B, etc.
  • Se respeta el bit_length() de cada operando, incluidos los ceros altos declarados.
  • El resultado tiene bit_length = suma de los anchos de todos los operandos.
  • Es la operación inversa de split: parts[0].append(*parts[1:]) reconstruye el valor original.
a = HugeNat(0b11, bit_length=2)     # 11
b = HugeNat(0b01, bit_length=2)     # 01
r = a.append(b)
int(r), r.bit_length()              # (7, 4)  ->  bits: 01|11

# Con ceros altos preservados
a = HugeNat(0b1, bit_length=4)      # 0001
b = HugeNat(0b1, bit_length=4)      # 0001
r = a.append(b)
int(r), r.bit_length()              # (17, 8) ->  bits: 0001|0001

# Múltiples operandos
a = HugeNat(0b10, bit_length=2)
b = HugeNat(0b01, bit_length=2)
c = HugeNat(0b11, bit_length=2)
r = a.append(b, c)
int(r), r.bit_length()              # (54, 6) ->  bits: 11|01|10

# Inversa de split
x = HugeNat(0xDEADBEEFCAFEBABE, bit_length=64)
parts = x.split(4)
reconstructed = parts[0].append(*parts[1:])
assert int(reconstructed) == int(x)

Array de bits

bits(order="msb->lsb" | "lsb->msb", length=None) devuelve np.ndarray de uint8.

x = HugeNat(0b1011)

np.asarray(x.bits())                  # array([1, 0, 1, 1], dtype=uint8)
np.asarray(x.bits(order="lsb->msb")) # array([1, 1, 0, 1], dtype=uint8)
np.asarray(x.bits(length=8))          # padding a la izquierda: 00001011

Cadena de bits agrupados

bits_str(order="msb->lsb" | "lsb->msb", group=64, sep=" ") para depurar o mostrar.

x = HugeNat(0x0123456789ABCDEFFEDCBA9876543210)

x.bits_str(group=4)          # grupos de 4 bits
x.bits_str(group=8)          # grupos de 1 byte
x.bits_str(order="lsb->msb", group=8)

Bytes ida y vuelta

x = HugeNat(2**20 + 123)
length = (x.bit_length() + 7) // 8

b = x.to_bytes(length=length, byteorder="big", signed=False)
y = HugeNat.from_bytes(b, byteorder="big", signed=False)

assert int(y) == int(x)

Rotaciones de bits

Las rotaciones usan el ancho lógico (bit_length()):

x = HugeNat(0b100101)

int(x.rotl(2))  # 0b010110
int(x.rotr(2))  # 0b011001

HugeNat(0).rotl(5)  # -> HugeNat(0)

NOT bit a bit (~)

~x voltea todos los bits dentro del ancho lógico del valor y devuelve un HugeNat con ese mismo ancho fijado.

  • Con bit_length declarado: voltea dentro de esa anchura.
  • Sin bit_length declarado: usa el bit_length() natural (número de bits significativos).
  • HugeNat(0) sin anchura declarada tiene bit_length() = 0; no hay bits que voltear, devuelve HugeNat(0, bit_length=0).
  • ~~x == x siempre.
~HugeNat(0b1011, bit_length=4)   # → HugeNat(4, bit_length=4)  → 0b0100
~HugeNat(0b1011)                  # → HugeNat(4, bit_length=4)  (bit_length natural = 4)
~HugeNat(0, bit_length=8)         # → HugeNat(255, bit_length=8) → 0xFF
~HugeNat(0xFF, bit_length=8)      # → HugeNat(0, bit_length=8)

# Uso típico: máscara de bits
x = HugeNat(0b10110010, bit_length=8)
mask = ~x                         # → 0b01001101, bit_length=8

Núcleo Numba-friendly

to_core(copy=True, word_order="le") devuelve (limbs, nbits) con limbs: uint64[::1] (1D contiguo) y nbits: int.

  • copy=True (default): devuelve una copia independiente.
  • copy=False: devuelve una vista read-only de la cache interna (sin allocar).
  • word_order="le" (default): limbs en little-endian (limb 0 = bits 0..63).
  • word_order="be": limbs en big-endian (MSB primero).

Los limbs se cachean internamente (HugeNat es inmutable), por lo que llamadas repetidas con copy=False son O(1).

from_core(limbs, nbits, word_order="le") reconstruye el valor y fija la anchura lógica a nbits. Con word_order="be", invierte los limbs antes de procesarlos.

Ejemplo Numba (histograma/unique de nibbles de 4 bits, empezando en el LSB) con signatura estricta y retorno tipado. Observa que seen:uint8 y counts:uint32 requieren types.Tuple, no UniTuple.

import numpy as np
from numba import njit, types

x = HugeNat(2**127 + 0xF00D)
limbs, nbits = x.to_core()

# Asegurar contigüidad y tipos exactos para la signatura
limbs = np.ascontiguousarray(limbs, dtype=np.uint64)
nbits = np.int64(nbits)

RET = types.Tuple((types.Array(types.uint8, 1, "C"), types.Array(types.uint32, 1, "C")))
SIG = RET(types.Array(types.uint64, 1, "C"), types.int64)

@njit(SIG, cache=False)
def unique_nibbles_core(limbs, nbits):
    counts = np.zeros(16, dtype=np.uint32)
    seen = np.zeros(16, dtype=np.uint8)

    if nbits <= 0 or limbs.size == 0:
        return seen, counts

    n_nibbles = nbits >> 2  # solo nibbles completos

    for k in range(n_nibbles):
        bitpos = k << 2       # desplaza 4 bits desde el LSB
        limb_i = bitpos >> 6
        off = bitpos & 63

        x0 = limbs[limb_i]
        if off <= 60:
            nib = (x0 >> np.uint64(off)) & np.uint64(0xF)
        else:
            lo = x0 >> np.uint64(off)
            hi = np.uint64(0)
            if limb_i + 1 < limbs.size:
                hi = limbs[limb_i + 1] << np.uint64(64 - off)
            nib = (lo | hi) & np.uint64(0xF)

        idx = int(nib)
        counts[idx] += np.uint32(1)
        seen[idx] = np.uint8(1)

    return seen, counts

seen, counts = unique_nibbles_core(limbs, nbits)
unique_values = np.nonzero(seen)[0].astype(np.uint8)

# Vuelta al wrapper para seguir trabajando en Python
y = HugeNat.from_core(limbs, nbits)
assert int(y) == int(x)

unique_values, counts[unique_values]

API big-endian de palabras (to_words_be / from_words_be)

Para pipelines que trabajan 100% en orden big-endian de palabras (MSW primero), estas funciones evitan la fricción de convertir manualmente entre LE y BE.

to_words_be() -> np.ndarray

Devuelve uint64[] con la palabra más significativa primero. Equivale a to_core(word_order="be") pero sin la tupla (limbs, nbits).

from_words_be(words_be, bit_length) -> HugeNat

Classmethod. Reconstruye un HugeNat desde uint64[] BE + bit_length obligatorio.

x = HugeNat(0xDEADBEEFCAFEBABE, bit_length=128)

# Exportar como array BE
be = x.to_words_be()          # np.array([0x0000000000000000, 0xDEADBEEFCAFEBABE], uint64)

# Reconstruir desde array BE
y = HugeNat.from_words_be(be, 128)
assert int(y) == int(x)

Extracción directa a uint64[] (sin objetos intermedios)

Estas APIs evitan crear objetos HugeNat por trozo, devolviendo arrays NumPy directamente desde la cache de limbs.

split_core(parts, word_order="le")

Divide en parts trozos iguales y devuelve un array 2D (parts, words_per_part) de uint64. Fila 0 = chunk LSB.

x = HugeNat(0xDEADBEEFCAFEBABE, bit_length=64)
arr = x.split_core(4)      # shape (4, 1), dtype uint64
arr_be = x.split_core(4, word_order="be")

split_array(parts, word_order="be")

Idéntico a split_core pero con default word_order="be". Pensado para pipelines que trabajan enteramente en big-endian.

x = HugeNat((1 << 256) - 1, bit_length=256)

# Dividir en 4 partes de 64 bits, cada fila en BE
parts_be = x.split_array(4)                # shape (4, 1), dtype uint64, BE
parts_le = x.split_array(4, word_order="le")  # equivale a split_core(4)

split_u64(parts)

Variante para chunks de hasta 64 bits. Devuelve array 1D (parts,) de uint64.

x = HugeNat(0xDEADBEEFCAFEBABE, bit_length=64)
chunks = x.split_u64(4)    # array([0xBABE, 0xCAFE, 0xBEEF, 0xDEAD], dtype=uint64)

extract_bits_core(start, width, word_order="le")

Extrae width bits desde la posición start como array 1D de uint64.

x = HugeNat(2**200 + 123456789)
low_128 = x.extract_bits_core(0, 128)           # 2 limbs
mid_64  = x.extract_bits_core(64, 64)           # 1 limb
be_view = x.extract_bits_core(0, 128, word_order="be")

extract_bits_u64(start, width)

Variante para width <= 64. Devuelve un solo np.uint64.

x = HugeNat(0xDEADBEEFCAFEBABE, bit_length=64)
x.extract_bits_u64(0, 16)   # np.uint64(0xBABE)
x.extract_bits_u64(48, 16)  # np.uint64(0xDEAD)

Operaciones bitwise sobre arrays (métodos estáticos)

Estos métodos estáticos operan directamente sobre arrays uint64[] big-endian sin crear objetos HugeNat intermedios. Están pensados para pipelines que encadenan operaciones sobre arrays BE.

HugeNat.bitwise_or(a_be, b_be, bit_length) -> np.ndarray

OR element-wise de dos arrays BE. Devuelve array BE de la anchura especificada.

a = HugeNat(0xF0F0, bit_length=64)
b = HugeNat(0x0F0F, bit_length=64)

result_be = HugeNat.bitwise_or(a.to_words_be(), b.to_words_be(), 64)
y = HugeNat.from_words_be(result_be, 64)
assert int(y) == 0xFFFF

HugeNat.shift_left(a_be, k, bit_length) -> np.ndarray

Shift left por k bits. Trunca al bit_length dado. Devuelve array BE.

x = HugeNat(0x1, bit_length=128)

result_be = HugeNat.shift_left(x.to_words_be(), 64, 128)
y = HugeNat.from_words_be(result_be, 128)
assert int(y) == 1 << 64

HugeNat.extract_bits(a_be, start, width) -> np.ndarray

Extrae width bits desde posición start de un array BE. Devuelve array BE.

x = HugeNat(0xDEADBEEFCAFEBABE, bit_length=64)

result_be = HugeNat.extract_bits(x.to_words_be(), 16, 32)
y = HugeNat.from_words_be(result_be, 32)
assert int(y) == (0xDEADBEEFCAFEBABE >> 16) & 0xFFFFFFFF

Ejemplo: pipeline encadenado sin objetos HugeNat

# Operar enteramente sobre arrays uint64 BE
x = HugeNat(some_value, bit_length=256)
arr = x.to_words_be()

# Encadenar operaciones sin crear HugeNat intermedios
shifted = HugeNat.shift_left(arr, 32, 256)
combined = HugeNat.bitwise_or(shifted, arr, 256)
chunk = HugeNat.extract_bits(combined, 64, 128)

# Solo al final convertir de vuelta si se necesita
result = HugeNat.from_words_be(chunk, 128)

Concatenación de arrays (concat_array)

HugeNat.concat_array(parts_2d, part_bits, word_order="be") es la inversa de split_array. Concatena un array 2D de partes en un array 1D.

  • parts_2d: array 2D (n_parts, words_per_part) de uint64. Fila 0 = chunk LSB.
  • part_bits: bits por cada parte.
  • word_order: "be" (default) o "le" — el orden de las palabras en cada fila y en el resultado.
x = HugeNat((1 << 256) - 1, bit_length=256)

# Roundtrip: split → concat
parts_be = x.split_array(4)                              # 4 partes de 64 bits, BE
recon_be = HugeNat.concat_array(parts_be, 64)            # reconstruir, BE
y = HugeNat.from_words_be(recon_be, 256)
assert int(y) == int(x)

# También funciona con LE
parts_le = x.split_core(4, word_order="le")
recon_le = HugeNat.concat_array(parts_le, 64, word_order="le")
z = HugeNat.from_core(recon_le, 256, word_order="le")
assert int(z) == int(x)

Kernels Numba (hugenat.numba_core)

Funciones @njit reutilizables que operan directamente sobre arrays (limbs, nbits) en little-endian. Ideales para pipelines Numba donde se necesita evitar la sobrecarga de crear objetos Python.

from hugenat.numba_core import (
    split_equal_parts_core,
    extract_range_core,
    concat_parts_core,
    bitwise_or_core,
    shift_left_core,
)

x = HugeNat(0xDEADBEEFCAFEBABE, bit_length=64)
limbs, nbits = x.to_core()

# Dividir en 4 trozos de 16 bits → array 2D (4, 1)
parts_2d = split_equal_parts_core(limbs, int(nbits), 4)

# Extraer 32 bits desde posición 16 → (limbs_out, nbits_out)
out_limbs, out_nbits = extract_range_core(limbs, int(nbits), 16, 32)

# Reconstruir desde partes 2D
recon_limbs, recon_nbits = concat_parts_core(parts_2d, 16)
y = HugeNat.from_core(recon_limbs, recon_nbits)
assert int(y) == int(x)

# OR de dos arrays LE → (limbs_out, nbits_out)
a_le = np.array([0xFF00], dtype=np.uint64)
b_le = np.array([0x00FF], dtype=np.uint64)
or_limbs, or_nbits = bitwise_or_core(a_le, b_le)
# or_limbs[0] == 0xFFFF

# Shift left por k bits → (limbs_out, nbits_out)
shl_limbs, shl_nbits = shift_left_core(limbs, 32)

Referencia de kernels

Kernel Entrada Salida Descripción
split_equal_parts_core(limbs, nbits, parts) limbs LE, nbits, parts array 2D (parts, words_per_part) Divide en partes iguales
extract_range_core(limbs, nbits, start, width) limbs LE, nbits, start, width (limbs_out, nbits_out) Extrae rango de bits
concat_parts_core(parts_2d, chunk_bits) array 2D LE, chunk_bits (limbs_out, nbits_out) Concatena partes
bitwise_or_core(a, b) dos arrays LE (limbs_out, nbits_out) OR element-wise
shift_left_core(limbs, k) limbs LE, k bits (limbs_out, nbits_out) Shift left

Resumen rápido de la API (referencia)

Método / Función Tipo Entrada Salida Notas
HugeNat(value, bit_length=None) constructor int, float, array, HugeNat HugeNat
to_core(copy, word_order) instancia (uint64[], int) LE o BE
from_core(limbs, nbits, word_order) classmethod uint64[], int HugeNat LE o BE
to_words_be() instancia uint64[] Solo BE
from_words_be(words_be, bit_length) classmethod uint64[], int HugeNat Solo BE
split(n) instancia int tuple[HugeNat] Objetos HugeNat
split_core(parts, word_order) instancia int, str uint64[parts, words] Default LE
split_array(parts, word_order) instancia int, str uint64[parts, words] Default BE
split_u64(parts) instancia int uint64[parts] Chunks <= 64 bits
extract_bits_core(start, width, word_order) instancia int, int, str uint64[] Default LE
extract_bits_u64(start, width) instancia int, int uint64 Width <= 64
HugeNat.bitwise_or(a_be, b_be, bit_length) static uint64[], uint64[], int uint64[] BE in/out
HugeNat.shift_left(a_be, k, bit_length) static uint64[], int, int uint64[] BE in/out
HugeNat.extract_bits(a_be, start, width) static uint64[], int, int uint64[] BE in/out
HugeNat.concat_array(parts_2d, part_bits, word_order) static uint64[n, w], int, str uint64[] Default BE
append(*others) instancia HugeNat... HugeNat Inversa de split

Contrato de dominio

  • Solo enteros >= 0 o arrays 1D de limbs uint64 (little-endian). Valores negativos o dimensiones distintas lanzan ValueError.
  • Sin bit_length explícito, los ceros de mayor peso se recortan automáticamente.
  • Con bit_length explícito, la anchura lógica se conserva aunque haya ceros a la izquierda.
  • Las restas que producirían negativos lanzan ValueError.

Desarrollo

  • Dependencias: numpy, numba.
  • Dependencias de desarrollo: pytest.
  • Ejecuta la batería completa: pytest -q.

Las demostraciones completas viven en HugeNat_demo.ipynb y cubren todos los ejemplos anteriores.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

hugenats-0.3.1.tar.gz (36.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

hugenats-0.3.1-py3-none-any.whl (19.0 kB view details)

Uploaded Python 3

File details

Details for the file hugenats-0.3.1.tar.gz.

File metadata

  • Download URL: hugenats-0.3.1.tar.gz
  • Upload date:
  • Size: 36.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for hugenats-0.3.1.tar.gz
Algorithm Hash digest
SHA256 7cc18404342164de9eccd6d4a9aa30d9f8975833d5ff0af22a8e0d7e0d73daea
MD5 9e18e3e5598026d9ca723d7ccdd739d0
BLAKE2b-256 b3e9bcd0ede896de29c04a593fc81cb4673082038f587e3e31edc38300cc8817

See more details on using hashes here.

File details

Details for the file hugenats-0.3.1-py3-none-any.whl.

File metadata

  • Download URL: hugenats-0.3.1-py3-none-any.whl
  • Upload date:
  • Size: 19.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for hugenats-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 41173e33d646ab729dff47b2f8f9a21dbac1d4759966f1955601a36dd9ea73a6
MD5 d3809423d9ffcc3f828953e1bb715de6
BLAKE2b-256 ce1ed48675a84786b6cd67be14bf1fdeffb03048fbdb650de81b11ab89653eab

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page