Metadata-Version: 2.4
Name: HugeNats
Version: 0.4.1
Summary: Wrapper de int para numeros naturales grandes con slicing por bits
Author-email: nand0san <hancaidolosdos@hotmail.com>
License-Expression: MIT
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy
Requires-Dist: numba
Dynamic: license-file

# HugeNat

Wrapper inmutable sobre `int` de Python para numeros naturales grandes (N >= 0) con indexado y slicing de bits, vistas NumPy y un nucleo listo para Numba.

Internamente es un `int` de Python. No reinventa aritmetica: delega en CPython y solo anade acceso por bits, vistas de limbs `uint64[]`, y kernels Numba para pipelines numericos sin overhead de objetos.

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

## Instalacion

```bash
pip install HugeNats
```

Dependencias: `numpy`, `numba`.

---

## Creacion

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

### Desde int

```python
x = HugeNat(42)
x = HugeNat(0)           # cero es valido
# HugeNat(-1)             # ValueError: solo naturales
```

### Desde float

Acepta floats que sean enteros exactos, no negativos y <= 2**53.

```python
x = HugeNat(3.0)          # HugeNat(3)
# HugeNat(3.5)             # ValueError: debe ser entero exacto
# HugeNat(float(2**54))    # ValueError: demasiado grande
```

### Desde lista o tupla (big-endian)

Las listas y tuplas se interpretan en orden **big-endian**: el primer elemento es el limb mas significativo. Es la convencion natural de lectura izquierda-a-derecha.

```python
x = HugeNat([1, 2, 3])
# Equivale a: 1 * 2^128 + 2 * 2^64 + 3

x = HugeNat([0xDEAD, 0xBEEF])
# limb MS = 0xDEAD, limb LS = 0xBEEF
```

Los ceros a la izquierda (mayor peso) se recortan automaticamente:

```python
HugeNat([0, 0, 5]) == HugeNat(5)  # True
```

### Desde np.ndarray (little-endian)

Los arrays NumPy se interpretan en orden **little-endian**: el elemento 0 es el limb menos significativo. Es el formato interno de HugeNat.

```python
limbs = np.array([0xFFFFFFFFFFFFFFFF, 0x1], dtype=np.uint64)
x = HugeNat(limbs)
# limb 0 (LS) = 0xFFFF...F, limb 1 (MS) = 0x1
```

### Desde otro HugeNat

Copia el valor y hereda `bit_length` fijo (si lo tiene), salvo que se pase uno nuevo.

```python
a = HugeNat(42, bit_length=16)
b = HugeNat(a)                   # copia, hereda bit_length=16
c = HugeNat(a, bit_length=32)    # copia, sobreescribe a bit_length=32
```

### Con anchura logica fija (`bit_length`)

Por defecto la anchura es `int(x).bit_length()`. Si se pasa `bit_length`, se fija:

- Si el valor tiene mas bits, se trunca por arriba.
- Si tiene menos, se consideran ceros a la izquierda.
- `bit_length()` y `len(x)` devuelven siempre la anchura fijada.

```python
padded = HugeNat(0b1011, bit_length=8)     # 00001011, ancho logico 8
cut    = HugeNat(0b110101, bit_length=4)    # 0101, truncado a 4 bits

padded.bit_length()  # 8
len(padded)          # 8  (equivale a bit_length())
```

### Desde bytes

```python
x = HugeNat.from_bytes(b'\x10\x00\x7b', byteorder="big")
b = x.to_bytes(length=3, byteorder="big", signed=False)
```

### Desde limbs big-endian + bit_length

```python
words_be = np.array([0x0001, 0xFFFFFFFFFFFFFFFF], dtype=np.uint64)
x = HugeNat.from_words_be(words_be, bit_length=128)
```

### Desde core Numba (limbs LE + nbits)

```python
x = HugeNat.from_core(limbs, nbits)                    # LE (default)
x = HugeNat.from_core(limbs_be, nbits, word_order="be") # BE
```

---

## Conversiones

```python
x = HugeNat(42)

int(x)    # 42
float(x)  # 42.0
bool(x)   # True  (False solo para HugeNat(0))
str(x)    # '42'
hash(x)   # consistente con int
len(x)    # bit_length()
```

---

## Aritmetica

Todas las operaciones aceptan `HugeNat` o `int >= 0` y devuelven `HugeNat`. Los operadores reflejados estan implementados (`2 + HugeNat(3)` funciona).

```python
a = HugeNat(10)
b = HugeNat(7)

a + b       # HugeNat(17)
a - b       # HugeNat(3)      -- ValueError si resultado < 0
a * b       # HugeNat(70)
a // b      # HugeNat(1)
a % b       # HugeNat(3)
divmod(a,b) # (HugeNat(1), HugeNat(3))
a ** 3      # HugeNat(1000)
pow(a, 3, 7)# HugeNat(6)     -- exponenciacion modular
2 ** a      # HugeNat(1024)  -- reflejado
abs(a)      # HugeNat(10)    -- identidad
+a          # HugeNat(10)    -- identidad
-a          # ValueError: no permite negativos
```

**Nota sobre `pow`**: `pow(base, exp, mod)` usa la exponenciacion modular nativa de Python, muy eficiente para valores grandes (RSA, Diffie-Hellman, etc.).

---

## Comparaciones

Soporta `==`, `!=`, `<`, `<=`, `>`, `>=` entre `HugeNat`, `int`, y para `==` tambien listas/tuplas/arrays.

```python
HugeNat(100) < HugeNat(200)   # True
HugeNat(100) >= 50             # True
HugeNat([1, 2]) == [1, 2]     # True (lista BE)
```

---

## Operaciones bitwise

```python
a = HugeNat(0b1100)
b = HugeNat(0b1010)

a & b    # HugeNat(0b1000)   AND
a | b    # HugeNat(0b1110)   OR
a ^ b    # HugeNat(0b0110)   XOR
a << 2   # HugeNat(0b110000) shift left
a >> 1   # HugeNat(0b110)    shift right
~a       # NOT (ver seccion dedicada)
```

Los shifts no aceptan `bool` como argumento; usar `int` explicito.

### Preservacion de `bit_length` fijo

Las operaciones bitwise preservan `_fixed_nbits` automaticamente:

- Si ambos operandos tienen el **mismo** ancho fijo: el resultado lo hereda.
- Si solo `self` lo tiene: el resultado hereda de `self`.
- Si ambos difieren: resultado sin ancho fijo.
- Los shifts preservan siempre el ancho de `self` y aplican mascara.

```python
a = HugeNat(0xFF, bit_length=8)
b = HugeNat(0x0F, bit_length=8)

(a & b).bit_length()                    # 8  (heredado)
(HugeNat(0x80, bit_length=8) << 1)      # HugeNat(0, bit_length=8) -- truncado
```

La aritmetica (+, -, *, //, %, **) **no** preserva `bit_length` fijo.

---

## NOT bit a bit (`~`)

`~x` voltea todos los bits dentro del ancho logico y devuelve un `HugeNat` con ese ancho fijado.

- Con `bit_length` declarado: voltea dentro de esa anchura.
- Sin `bit_length` declarado: usa el `bit_length()` natural.
- `~~x == x` siempre.
- `HugeNat(0)` sin anchura: `bit_length()=0`, no hay bits que voltear, devuelve `HugeNat(0, bit_length=0)`.

```python
~HugeNat(0b1011, bit_length=4)    # HugeNat(4, bit_length=4)  -> 0b0100
~HugeNat(0, bit_length=8)         # HugeNat(255, bit_length=8) -> todo unos
~HugeNat(0xFF, bit_length=8)      # HugeNat(0, bit_length=8)

x = HugeNat(0b10110010, bit_length=8)
int(x & ~x)                        # 0 (siempre)
```

---

## Rotaciones de bits

Rotan dentro del ancho logico (`bit_length()`). Shift grande se normaliza con `mod bit_length()`.

```python
x = HugeNat(0b100101)

x.rotl(2)    # HugeNat(22, bit_length=6)   rota a la izquierda
x.rotr(2)    # HugeNat(25, bit_length=6)   rota a la derecha

HugeNat(0).rotl(5)   # HugeNat(0), sin error
```

---

## Indexado de bits

Convencion: `LSB = indice 0`. Indices negativos cuentan desde el MSB. Fuera de rango devuelve `0` (no lanza excepcion).

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

x[0]     # 1 (LSB)
x[-1]    # 1 (MSB)
x[100]   # 0 (fuera de rango)
x[-100]  # 0 (fuera de rango)
```

---

## Slicing de bits

El bit `start` pasa a ser el bit 0 del resultado.

- `step=None` o `1`: ruta rapida, normaliza limites como Python.
- `step != 0, != 1`: ruta general con semantica completa de slicing de listas.
- Bits por encima de `bit_length()` se consideran ceros implicitos.

```python
x = HugeNat(0b1101101)

int(x[0:3])       # 5   (bits 0..2 -> 0b101)
int(x[2:5])       # 6   (0b110)
int(x[0:7:2])     # 11  (cada 2 bits -> 0b1011)
int(x[5:0:-2])    # paso negativo
```

---

## Division en trozos (`split`)

`split(n)` divide en `n` trozos de igual cantidad de bits. Devuelve tupla LSB-primero.

- `bit_length()` debe ser divisible por `n`.
- Cada trozo conserva su `bit_length` fijo.

```python
x = HugeNat(0b1011010110010001, bit_length=16)
parts = x.split(4)

[int(p) for p in parts]           # [1, 9, 5, 11]  (LSB -> MSB)
[p.bit_length() for p in parts]   # [4, 4, 4, 4]
```

---

## Concatenacion de bits (`append`)

`append(B, C, ...)` coloca los bits de `self` en las posiciones bajas (LSB), los de `B` encima, etc. Inversa de `split`.

```python
a = HugeNat(0b11, bit_length=2)
b = HugeNat(0b01, bit_length=2)
r = a.append(b)                   # bits: 01|11 -> 7, bit_length=4

# Reconstruir desde split
x = HugeNat(0xDEADBEEF, bit_length=32)
parts = x.split(4)
assert int(parts[0].append(*parts[1:])) == int(x)
```

---

## Vista de bits

### Array NumPy (`bits`)

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

```python
x = HugeNat(0b1011)

x.bits()                          # array([1, 0, 1, 1], uint8)  MSB primero
x.bits(order="lsb->msb")         # array([1, 1, 0, 1], uint8)  LSB primero
x.bits(length=8)                  # padding: array([0, 0, 0, 0, 1, 0, 1, 1])
```

### Cadena de texto (`bits_str`)

`bits_str(order="msb->lsb", group=64, sep=" ")` -- los grupos se alinean desde la derecha (el primer grupo puede ser mas corto).

```python
x = HugeNat(0b101101)

x.bits_str(group=4)    # '10 1101'   (grupo corto a la izquierda)
x.bits_str(group=8)    # '101101'    (cabe en un grupo)
```

---

## Nucleo Numba-friendly (`to_core` / `from_core`)

HugeNat es inmutable. Los limbs `uint64[]` se cachean internamente; llamadas repetidas con `copy=False` son O(1).

### `to_core(copy=True, word_order="le")`

Devuelve `(limbs: uint64[::1], nbits: int64)`.

- `copy=True` (default): copia independiente, escribible.
- `copy=False`: vista read-only de la cache interna (sin alloc).
- `word_order="le"` (default): limb 0 = bits 0..63.
- `word_order="be"`: MSB primero.

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

# Para Numba, asegurar contiguidad y tipos exactos:
limbs = np.ascontiguousarray(limbs, dtype=np.uint64)
nbits = np.int64(nbits)
```

### `from_core(limbs, nbits, word_order="le")`

Reconstruye `HugeNat` desde `(limbs, nbits)`. Fija `bit_length` a `nbits`.

```python
y = HugeNat.from_core(limbs, nbits)
y = HugeNat.from_core(limbs_be, nbits, word_order="be")
```

---

## API big-endian de palabras

### `to_words_be()` / `from_words_be(words_be, bit_length)`

Para pipelines 100% big-endian. `to_words_be()` devuelve `uint64[]` con MSW primero. `from_words_be` requiere `bit_length` explicito.

```python
x = HugeNat(0xAAAABBBBCCCCDDDD_1111222233334444, bit_length=128)

be = x.to_words_be()
y = HugeNat.from_words_be(be, 128)
assert int(y) == int(x)
```

---

## Extraccion directa a `uint64[]`

APIs que evitan crear objetos `HugeNat` por trozo, devolviendo arrays directamente.

### `split_core(parts, word_order="le")`

Array 2D `(parts, words_per_part)`. Fila 0 = chunk LSB. Default LE.

### `split_array(parts, word_order="be")`

Identico a `split_core` pero con **default BE**. Para pipelines big-endian.

### `split_u64(parts)`

Array 1D `(parts,)` de `uint64`. Para chunks de hasta 64 bits.

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

Extrae `width` bits desde posicion `start` como array 1D de `uint64`.

### `extract_bits_u64(start, width)`

Extrae hasta 64 bits como un solo `np.uint64`.

```python
x = HugeNat(0xDEADBEEFCAFEBABE, bit_length=64)

x.split_core(4)            # shape (4, 1), LE
x.split_array(4)           # shape (4, 1), BE
x.split_u64(4)             # array([0xBABE, 0xCAFE, 0xBEEF, 0xDEAD])

x.extract_bits_core(0, 16) # array([0xBABE])
x.extract_bits_u64(48, 16) # np.uint64(0xDEAD)
```

---

## Operaciones estaticas sobre arrays BE

Metodos estaticos que operan sobre `uint64[]` big-endian sin crear objetos `HugeNat`. Para encadenar operaciones sobre arrays crudos.

### `HugeNat.bitwise_or(a_be, b_be, bit_length)`

OR element-wise. Entrada y salida BE.

### `HugeNat.shift_left(a_be, k, bit_length)`

Shift left por `k` bits. Trunca al `bit_length`. Entrada y salida BE.

### `HugeNat.extract_bits(a_be, start, width)`

Extrae `width` bits desde posicion `start`. Entrada y salida BE.

### `HugeNat.concat_array(parts_2d, part_bits, word_order="be")`

Inversa de `split_array`. Concatena array 2D de partes en array 1D.

```python
# Pipeline encadenado sin objetos HugeNat
arr = x.to_words_be()
shifted = HugeNat.shift_left(arr, 32, 256)
combined = HugeNat.bitwise_or(shifted, arr, 256)
chunk = HugeNat.extract_bits(combined, 64, 128)
result = HugeNat.from_words_be(chunk, 128)
```

---

## Kernels Numba (`hugenat.numba_core`)

Funciones `@njit` que operan sobre `(limbs, nbits)` en little-endian. Pueden llamarse desde otras funciones `@njit` para componer pipelines sin overhead de objetos Python.

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

| Kernel | Entrada | Salida | Descripcion |
|--------|---------|--------|-------------|
| `split_equal_parts_core(limbs, nbits, parts)` | limbs LE, nbits, parts | array 2D `(parts, words)` | 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 |

---

## Convencion de endianness

| Entrada | Orden | Razon |
|---------|-------|-------|
| `list` / `tuple` al constructor | **Big-endian** (elemento 0 = MS) | Lectura natural izquierda-a-derecha |
| `np.ndarray` al constructor | **Little-endian** (elemento 0 = LS) | Formato interno de HugeNat |
| `to_core()` default | Little-endian | Formato interno |
| `to_words_be()` | Big-endian | API BE explicita |
| `split_core()` default | Little-endian | Coherente con `to_core` |
| `split_array()` default | Big-endian | Coherente con `to_words_be` |
| Metodos estaticos (`bitwise_or`, etc.) | Big-endian | API BE explicita |
| Kernels Numba (`numba_core`) | Little-endian | Formato nativo de computo |

---

## Referencia rapida

| Metodo | Tipo | Devuelve | Notas |
|--------|------|----------|-------|
| `HugeNat(value, bit_length=None)` | constructor | HugeNat | int, float, list(BE), ndarray(LE), HugeNat |
| `int(x)`, `float(x)`, `bool(x)`, `str(x)` | conversion | tipo nativo | |
| `len(x)` | propiedad | int | = `bit_length()` |
| `bit_length()` | instancia | int | anchura logica |
| `bit_count()` | instancia | int | bits a 1 |
| `bits(order, length)` | instancia | `uint8[]` | vista de bits |
| `bits_str(order, group, sep)` | instancia | str | texto agrupado |
| `to_bytes(length, byteorder, signed)` | instancia | bytes | como `int.to_bytes` |
| `from_bytes(data, byteorder, signed)` | classmethod | HugeNat | como `int.from_bytes` |
| `to_core(copy, word_order)` | instancia | `(uint64[], int)` | LE o BE |
| `from_core(limbs, nbits, word_order)` | classmethod | HugeNat | LE o BE |
| `to_words_be()` | instancia | `uint64[]` | solo BE |
| `from_words_be(words, bit_length)` | classmethod | HugeNat | solo BE |
| `split(n)` | instancia | tuple[HugeNat] | LSB primero |
| `append(*others)` | instancia | HugeNat | inversa de split |
| `split_core(parts, word_order)` | instancia | `uint64[n, w]` | default LE |
| `split_array(parts, word_order)` | instancia | `uint64[n, w]` | **default BE** |
| `split_u64(parts)` | instancia | `uint64[n]` | chunks <= 64b |
| `extract_bits_core(start, width, word_order)` | instancia | `uint64[]` | default LE |
| `extract_bits_u64(start, width)` | instancia | `uint64` | width <= 64 |
| `rotl(shift)` / `rotr(shift)` | instancia | HugeNat | rotacion |
| `bitwise_or(a_be, b_be, bit_length)` | static | `uint64[]` | BE |
| `shift_left(a_be, k, bit_length)` | static | `uint64[]` | BE |
| `extract_bits(a_be, start, width)` | static | `uint64[]` | BE |
| `concat_array(parts_2d, part_bits, word_order)` | static | `uint64[]` | **default BE** |

---

## Contrato de dominio

- Solo enteros >= 0. Valores negativos o restas con resultado negativo lanzan `ValueError`.
- `bool` no se acepta como shift en `<<`, `>>`, `rotl`, `rotr`.
- Sin `bit_length` explicito, los ceros de mayor peso se recortan.
- Con `bit_length` explicito, la anchura logica se conserva siempre.
- HugeNat es inmutable (usa `__slots__`, sin `__dict__`). Los limbs se cachean internamente.
- `split(n)` requiere que `bit_length()` sea divisible por `n`.

## Desarrollo

```bash
pip install -r requirements.txt
pytest -q
```

Demostraciones interactivas en `HugeNat_demo.ipynb`.
