HugeNats 0.1.5
Wrapper de int para numeros naturales grandes con slicing por bits
Requires Python: >=3.9

# 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

```bash
pip install HugeNats
```

## Creación rápida

```python
import numpy as np
from hugenat import HugeNat

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

# 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])
```

## API tipo int

- `int(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 devuelven siempre `HugeNat` y rechazan resultados negativos en restas.

```python
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)
```

## Indexado de bits

- Convención: `LSB = índice 0`. Índices negativos son relativos a `bit_length()`.
- Fuera de rango lanza `IndexError`.
- `~x` no está definido y lanza `ValueError`.

```python
x = HugeNat(0b1101101)  # 109

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

## Slicing de bits

- `step` en `{None, 1}` usa ruta rápida: normaliza como Python, recorta a `[0, nbits]` y recompacta para que el bit `start` pase a ser el bit 0.
- Cualquier otro `step` (salvo 0) usa ruta general con semántica completa de slicing de listas y reempaquetado LSB-first.
- `step == 0` -> `ValueError`.

```python
x = HugeNat(0b1101101)

x[0:3]        # bits 0..2 -> 0b101 (5)
x[2:5]        # 0b110 (6)
x[0:7:2]      # toma cada 2 bits -> 0b10011 (19)
x[5:0:-2]    # slicing con paso negativo
```

## Vista de bits como array

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

```python
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.

```python
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

```python
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 natural (`bit_length()`):

```python
x = HugeNat(0b100101)

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

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

## Núcleo Numba-friendly

`to_core()` devuelve siempre `limbs: uint64[::1]` (1D contiguo, little-endian por palabra; `limb 0` contiene los bits 0..63) y `nbits: int`. `from_core()` reconstruye exactamente el mismo valor.

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`.

```python
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=True)
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]
```

## Contrato de dominio

- Solo enteros `>= 0` o arrays 1D de limbs `uint64` (little-endian). Valores negativos o dimensiones distintas lanzan `ValueError`.
- Los ceros de mayor peso se recortan automáticamente.
- Las restas que producirían negativos lanzan `ValueError`.

## Desarrollo

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

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