# Внутреннее устройство clouds_system

## Архитектура модуля

```
clouds_system/
├── __init__.py     — публичный экспорт
└── clouds.py       — вся логика
```

Модуль состоит из четырёх слоёв:

1. **Конфигурация** — `LayerConfig`, `CloudConfig`, `DaySnapshot`
2. **Генератор шума** — `_perlin_core` (Numba JIT), `_generate_noise`
3. **Прогрев JIT** — `warmup()`, флаг `_WARMED_UP`
4. **Система облаков** — `CloudSystem`

---

## Детерминизм и seed-схема

Вся система воспроизводима: один `world_seed` даёт одинаковую последовательность дней.

Seed для каждого дня и слоя вычисляется по формуле:

```
day_seed   = world_seed + day × 2654435761   # константа Кнута — хорошее рассеивание
wind_seed  = day_seed XOR 0xDEADBEEF         # отделяет ветер от шума
layer_seed = day_seed + layer.seed_salt       # уникален для каждого слоя
```

Константа `2654435761` (множитель Кнута) обеспечивает равномерное распределение
seed'ов по дням — соседние дни дают несвязанные шумовые карты.

Соли слоёв (`0x11111111`, `0x22222222`, `0x33333333`) гарантируют независимость
шума между слоями в пределах одного дня.

---

## Генератор шума Перлина

### Принцип

Используется **фрактальный шум Перлина** (fBm — fractional Brownian motion):
несколько октав суммируются с убывающей амплитудой и возрастающей частотой.

```
amplitude: 1.0 → 0.5 → 0.25 → 0.125 → ...
frequency: f   → 2f  → 4f   → 8f    → ...
```

Результат нормируется в `[0, 1]`:

```python
out = out / max_val + 0.5
np.clip(out, 0, 1, out=out)
```

### `_generate_noise(width, height, seed, scale, octaves)`

Подготавливает таблицы и запускает `_perlin_core` для каждой октавы.

1. Из `seed` через `np.random.default_rng` генерирует:
   - перестановочную таблицу `perm[512]` (256 элементов, повторённых дважды для wrap-around)
   - таблицу градиентов `gx[256]`, `gy[256]` — случайные единичные векторы

2. Для каждой октавы вызывает `_perlin_core` с текущей `frequency` и `amplitude`,
   затем `amplitude × 0.5`, `frequency × 2.0`

### `_perlin_core` (Numba JIT)

Скомпилированное ядро. Параметры декоратора:
- `parallel=True` — строки карты обрабатываются параллельно через OpenMP
- `fastmath=True` — ослабляет IEEE 754, даёт +10-30% скорости
- `cache=True` — сохраняет скомпилированный артефакт в `__pycache__`

Алгоритм для каждого пикселя `(x, y)`:

```
1. Вычислить дробные координаты в пространстве шума: xf, yf
2. Найти ячейку сетки: xi = int(x / scale), yi = int(y / scale)
3. Применить fade-функцию: u = 6t⁵ − 15t⁴ + 10t³ (по x и y)
4. Получить градиенты четырёх ближайших узлов через perm + gx/gy
5. Вычислить dot-произведения градиентов с расстояниями до пикселя
6. Билинейно интерполировать четыре значения через u и v
7. Добавить результат × amplitude в выходной массив
```

Fade-функция `6t⁵ − 15t⁴ + 10t³` обеспечивает нулевую первую и вторую производную
на границах ячеек — визуально плавные переходы без артефактов.

---

## Слои облаков

Три стандартных слоя (`DEFAULT_LAYERS`) отличаются по трём осям:

| Слой | scale_mult | Эффект | shift_mult | Эффект | thresh_delta | Эффект |
|------|-----------|--------|-----------|--------|-------------|--------|
| низкие | 0.6 | мелкие облака | 0.5 | медленные | −0.05 | порог ниже → гуще |
| средние | 1.0 | базовый размер | 1.0 | базовая скорость | 0.00 | базовый порог |
| высокие | 1.7 | крупные облака | 1.8 | быстрые | +0.05 | порог выше → реже |

Количество слоёв не фиксировано — `CloudSystem` работает с любым `list[LayerConfig]`.

---

## Жизненный цикл дня (`next_day`)

```
day += 1
day_seed = world_seed + day × 2654435761

wind_dir ← random из 8 направлений (seed: day_seed XOR 0xDEADBEEF)

для каждого слоя i:
    если день 1:
        base_map[i] = _generate_noise(...)   # новая карта шума
    иначе:
        base_map[i] = get_layer(i, t=1.0)    # конец предыдущего дня

    wind_directions[i] = wind_dir
    wind_vecs[i]       = WIND_VECTORS[wind_dir]
```

Переход между днями непрерывен: каждый день стартует ровно там, где закончился предыдущий.
Ветер одинаков для всех слоёв в один день, но каждый слой двигается с разной скоростью
через `shift_mult`.

---

## Движение облаков (`get_layer`)

Базовая карта не изменяется в течение дня — она только сдвигается через `np.roll`:

```python
shift_x = round(dx × max_shift × shift_mult × t)
shift_y = round(dy × max_shift × shift_mult × t)

moved = np.roll(base_map, shift_x, axis=1)
moved = np.roll(moved,    shift_y, axis=0)
```

`t ∈ [0, 1]` — прогресс дня. При `t=0` сдвига нет, при `t=1` сдвиг максимален.

`np.roll` реализует **тороидальный сдвиг**: облака, уходящие за один край карты,
появляются с противоположного. При умеренных значениях `max_shift` это незаметно.

---

## Пороги облачности

Для каждого слоя порог вычисляется как:

```
threshold = cloud_thresh + layer.thresh_delta
```

Клетка считается облачной если плотность шума ≥ threshold:

```python
mask = density_map >= threshold
```

`thresh_delta` позволяет настраивать густоту каждого слоя независимо от базового порога.

---

## `build_snapshot`

Собирает `DaySnapshot` из `n_ticks` временных срезов:

```python
ticks = np.empty((n_ticks, H, W), dtype=uint8)

для tick в range(n_ticks):
    t = tick / (n_ticks - 1)
    ticks[tick] = is_cloudy(t)   # OR по всем слоям
```

`is_cloudy` объединяет маски всех слоёв через `np.any`:

```python
np.any(np.stack(get_layer_masks(t)), axis=0)
```

Клетка `ticks[tick, y, x]` = `1` если хотя бы один слой облачен в этой точке в этот тик.

---

## Прогрев JIT (`warmup`)

Numba компилирует `_perlin_core` при первом вызове. Флаг `_WARMED_UP` (модульный уровень)
гарантирует, что компиляция происходит ровно один раз за время жизни процесса.

`warmup()` нужно вызывать с теми же `scale` и `octaves`, что будут в `CloudConfig` —
Numba компилирует под конкретные типы аргументов. Несовпадение типов спровоцирует
повторную компиляцию при первом реальном вызове.

Скомпилированный артефакт сохраняется в `__pycache__` рядом с `clouds.py` (`cache=True`).
При следующем запуске процесса компиляция пропускается — артефакт загружается с диска.

---

## Зависимости

| Библиотека | Роль |
|-----------|------|
| `numpy` | массивы, `np.roll`, `np.any`, `np.stack` |
| `numba` | JIT-компиляция `_perlin_core`, параллелизм через OpenMP |
