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

## 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 devuelve `0`.
- `~x` no está definido y lanza `ValueError`.

```python
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, 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()` expone `(limbs, nbits)` contiguos en little-endian por palabra de 64 bits; `from_core()` reconstruye el mismo valor. Útil para kernels `njit` sin dependencias externas.

```python
from numba import njit

x = HugeNat(2**130 + 5)
limbs, nbits = x.to_core()

@njit
def popcount_core(limbs, nbits):
    total = 0
    for i in range(limbs.size):
        total += int(limbs[i]).bit_count()
    return total

popcount_core(limbs, nbits)
y = HugeNat.from_core(limbs, nbits)
assert int(y) == int(x)
```

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