Metadata-Version: 2.4
Name: HugeNats
Version: 0.5.0
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: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Operating System :: OS Independent
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy
Requires-Dist: numba
Dynamic: license-file

# HugeNats

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/hugenats

## Instalacion

```bash
pip install HugeNats
```

Dependencias: `numpy`, `numba`.

---

## Creacion

```python
import numpy as np
from hugenats import HugeNats
```

### Desde int

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

### Desde float

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

```python
x = HugeNats(3.0)          # HugeNats(3)
# HugeNats(3.5)             # ValueError: debe ser entero exacto
# HugeNats(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 = HugeNats([1, 2, 3])
# Equivale a: 1 * 2^128 + 2 * 2^64 + 3

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

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

```python
HugeNats([0, 0, 5]) == HugeNats(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 HugeNats.

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

### Desde otro HugeNats

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

```python
a = HugeNats(42, bit_length=16)
b = HugeNats(a)                   # copia, hereda bit_length=16
c = HugeNats(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 = HugeNats(0b1011, bit_length=8)     # 00001011, ancho logico 8
cut    = HugeNats(0b110101, bit_length=4)    # 0101, truncado a 4 bits

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

### Desde bytes

```python
x = HugeNats.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 = HugeNats.from_words_be(words_be, bit_length=128)
```

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

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

---

## Conversiones

```python
x = HugeNats(42)

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

HugeNats(0b1011).weight   # 3  (bits a 1, atajo de bit_count())
```

---

## Aritmetica

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

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

a + b       # HugeNats(17)
a - b       # HugeNats(3)      -- ValueError si resultado < 0
a * b       # HugeNats(70)
a // b      # HugeNats(1)
a % b       # HugeNats(3)
divmod(a,b) # (HugeNats(1), HugeNats(3))
a ** 3      # HugeNats(1000)
pow(a, 3, 7)# HugeNats(6)     -- exponenciacion modular
2 ** a      # HugeNats(1024)  -- reflejado
abs(a)      # HugeNats(10)    -- identidad
+a          # HugeNats(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 `HugeNats`, `int`, y para `==` tambien listas/tuplas/arrays.

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

---

## Operaciones bitwise

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

a & b    # HugeNats(0b1000)   AND
a | b    # HugeNats(0b1110)   OR
a ^ b    # HugeNats(0b0110)   XOR
a << 2   # HugeNats(0b110000) shift left
a >> 1   # HugeNats(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 = HugeNats(0xFF, bit_length=8)
b = HugeNats(0x0F, bit_length=8)

(a & b).bit_length()                    # 8  (heredado)
(HugeNats(0x80, bit_length=8) << 1)      # HugeNats(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 `HugeNats` 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.
- `HugeNats(0)` sin anchura: `bit_length()=0`, no hay bits que voltear, devuelve `HugeNats(0, bit_length=0)`.

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

x = HugeNats(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 = HugeNats(0b100101)

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

HugeNats(0).rotl(5)   # HugeNats(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 = HugeNats(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 = HugeNats(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 = HugeNats(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 = HugeNats(0b11, bit_length=2)
b = HugeNats(0b01, bit_length=2)
r = a.append(b)                   # bits: 01|11 -> 7, bit_length=4

# Reconstruir desde split
x = HugeNats(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 = HugeNats(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 = HugeNats(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`)

HugeNats 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 = HugeNats(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 `HugeNats` desde `(limbs, nbits)`. Fija `bit_length` a `nbits`.

```python
y = HugeNats.from_core(limbs, nbits)
y = HugeNats.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 = HugeNats(0xAAAABBBBCCCCDDDD_1111222233334444, bit_length=128)

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

---

## Extraccion directa a `uint64[]`

APIs que evitan crear objetos `HugeNats` 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 = HugeNats(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 `HugeNats`. Para encadenar operaciones sobre arrays crudos.

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

OR element-wise. Entrada y salida BE.

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

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

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

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

### `HugeNats.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 HugeNats
arr = x.to_words_be()
shifted = HugeNats.shift_left(arr, 32, 256)
combined = HugeNats.bitwise_or(shifted, arr, 256)
chunk = HugeNats.extract_bits(combined, 64, 128)
result = HugeNats.from_words_be(chunk, 128)
```

---

## Kernels Numba (`hugenats.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 hugenats.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 HugeNats |
| `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 |
|--------|------|----------|-------|
| `HugeNats(value, bit_length=None)` | constructor | HugeNats | int, float, list(BE), ndarray(LE), HugeNats |
| `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 |
| `weight` | propiedad | int | bits a 1 (peso de Hamming, atajo de `bit_count()`) |
| `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 | HugeNats | como `int.from_bytes` |
| `to_core(copy, word_order)` | instancia | `(uint64[], int)` | LE o BE |
| `from_core(limbs, nbits, word_order)` | classmethod | HugeNats | LE o BE |
| `to_words_be()` | instancia | `uint64[]` | solo BE |
| `from_words_be(words, bit_length)` | classmethod | HugeNats | solo BE |
| `split(n)` | instancia | tuple[HugeNats] | LSB primero |
| `append(*others)` | instancia | HugeNats | 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 | HugeNats | 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.
- HugeNats 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 `HugeNats_demo.ipynb`.
