feat(generator): план состава мира — зерно, приток, двухкуковые пары
- Зачем: - тикет #38: состав мира должен быть чистой функцией зерна, а счётчики будущего манифеста — известны до генерации хоть одного события. - Что: - `world.py` — конфигурация мира одним модулем чистых данных: приток, недельная волна, профиль возвратов, доли покупателей и пар, D0. - `seeds.py` — иерархия подпотоков на `SeedSequence`: состав мира (ось и предыстория) отдельно от дней и их компонентов. - `plan.py` — ленивый план состава: когорта дня, аудитория дня из окна возвратов, гарантированные заказы пар, счётчики горизонта. Случайность — только целыми числами. - спека, раздел 1: вторая кука пары рождается по затухающему профилю возвратов; раздел 9: измеренные числа канонического мира, оценка накопленной аудитории поправлена с ≈60 до 68 тыс. - CONTEXT.md: термин «когорта дня»; README генератора — новые модули. - Проверка: - make test (295 тестов), make lint, make typecheck; - тесты проверены мутациями: 12 подмен в плане и конфигурации, каждая роняет ровно свой тест. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
"""Иерархия зёрен: откуда любая часть мира берёт свою случайность.
|
||||
|
||||
Дерево подпотоков (спека генератора, раздел 2):
|
||||
|
||||
корневое зерно
|
||||
├── состав мира
|
||||
│ ├── ось → номер дня: когорта этого дня
|
||||
│ └── предыстория → глубина: когорта дня до D0
|
||||
└── дни
|
||||
└── номер дня → трафик, торговля, расхождения, опоздания
|
||||
|
||||
Механизм — `numpy.random.SeedSequence`: потомок полностью определяется парой
|
||||
(зерно, позиция в дереве), а не порядком вычислений. Сверено через Context7
|
||||
по документации numpy (2026-08-01) и проверено тестом: `spawn_key`, выписанный
|
||||
руками, даёт тот же подпоток, что цепочка `spawn`. На этом держатся три
|
||||
обещания: параллельный прогон равен последовательному, день N+1 не трогает
|
||||
дни 1…N, правка одного компонента меняет только его часть снимка.
|
||||
|
||||
Позиция в дереве — неотрицательные целые, а дни предыстории отрицательны;
|
||||
поэтому у предыстории своя ветвь, а не общий ряд с осью.
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
from enum import IntEnum
|
||||
|
||||
import numpy as np
|
||||
|
||||
# Канонический зерно эталонного мира — константа репозитория; манифест хранит
|
||||
# его в паспорте мира. Свои зёрна менти крутит без гарантий манифеста.
|
||||
CANONICAL_SEED = 20260601
|
||||
|
||||
|
||||
class Component(IntEnum):
|
||||
"""Подпотоки внутри дня; порядок объявления — позиция в дереве."""
|
||||
|
||||
TRAFFIC = 0
|
||||
COMMERCE = 1
|
||||
DISCREPANCIES = 2
|
||||
LATECOMERS = 3
|
||||
|
||||
|
||||
class _Branch(IntEnum):
|
||||
"""Две ветви корня: постоянный состав мира и проживание дней."""
|
||||
|
||||
COMPOSITION = 0
|
||||
DAYS = 1
|
||||
|
||||
|
||||
class _Era(IntEnum):
|
||||
"""Две ветви состава: ось событий и предыстория до D0."""
|
||||
|
||||
AXIS = 0
|
||||
PREHISTORY = 1
|
||||
|
||||
|
||||
def _stream(seed: int, position: Sequence[int]) -> np.random.Generator:
|
||||
"""Подпоток на позиции `position` дерева зерна `seed`."""
|
||||
sequence = np.random.SeedSequence(entropy=seed, spawn_key=position)
|
||||
return np.random.Generator(np.random.PCG64(sequence))
|
||||
|
||||
|
||||
def cohort_stream(seed: int, day: int) -> np.random.Generator:
|
||||
"""Случайность когорты дня `day`; отрицательный день — предыстория."""
|
||||
era, index = (_Era.AXIS, day) if day >= 0 else (_Era.PREHISTORY, -day - 1)
|
||||
return _stream(seed, (_Branch.COMPOSITION, era, index))
|
||||
|
||||
|
||||
def day_stream(seed: int, day: int, component: Component) -> np.random.Generator:
|
||||
"""Случайность одного компонента дня `day` — дня оси, не предыстории."""
|
||||
if day < 0:
|
||||
raise ValueError(f"события начинаются в D0: дня {day} на оси нет")
|
||||
return _stream(seed, (_Branch.DAYS, day, component))
|
||||
Reference in New Issue
Block a user