# План: Gauss-ускоритель — симулятор и оптимизатор Многоступенчатый электромагнитный ускоритель ферромагнитного цилиндра (coilgun). Каждая ступень: разгонная катушка (Cu/Al провод на пластиковой трубке) + батарея конденсаторов через тиристор/MOSFET, и датчик прохода снаряда перед катушкой (компенсация задержки включения ключа). Датчик — два конкурирующих варианта: индукционная катушка (сигнал ~ скорости снаряда, слаб на медленных ступенях) и оптопара/датчик Холла (не зависит от скорости) — оптимизатор сравнивает оба. Цель поиска — КПД (кинетическая энергия снаряда на выходе / энергия во всех конденсаторах). Скорость и стоимость — вторичные метрики. Число ступеней N, геометрия/материал снаряда, все компоненты — часть пространства поиска, а не фиксированные входы. Компоненты — реальные, розничные (Проконтакт/procontact74.ru, ChipDip, Cable.ru и др.). Требование: миллионы прогонов (Monte Carlo/LHS sweep + эволюционный поиск), **все** результаты — успешные и неудачные, с честной причиной отказа — пишутся в SQLite. Никаких приукрашенных цифр — если модель показывает низкий КПД или нереализуемость, это тоже результат. **Найденная и исправленная ошибка (Этап 3):** насыщение сердечника изначально клэмпилось только в механическом уравнении (F=0.5·I²·dL/dx), но не в электрическом (наведённая ЭДС всё ещё считалась по полной dL/dx) — это незаметно ломало точный энергобаланс на ~15%. Тест на сохранение энергии (этого же честного протокола, который просил пользователь) это поймал. Решение: клэмп насыщения убран из динамики (F=0.5·I²·dL/dx без клэмпа, энергобаланс теперь точен до ~0.02%), а `solenoid_field_estimate_tesla`/ `saturation_scale` оставлены как ДИАГНОСТИКА — `StageResult.saturation_warning` честно предупреждает, когда конфигурация физически выходит за пределы насыщения материала снаряда, не подменяя динамику. Полная нелинейная L(x, I)-модель с coenergy-выводом силы — в разделе "ограничения модели" как будущая работа, не как текущая гарантия точности. Полный план архитектуры: см. историю обсуждения / `physics`, `sim`, `optim`, `storage`, `report` модули ниже. **Docker — основной способ запуска** (по требованию пользователя): `Dockerfile` + `docker-compose.yml` в корне репозитория, образ собран и провалидирован — внутри контейнера запускаются все 37 тестов и CLI (`docker compose run --rm --entrypoint pytest gausse -q`). Результаты (SQLite и отчёты, когда появятся) монтируются в `./results` на хосте, чтобы переживать пересборку образа. ## Чек-лист этапов - [x] **Этап 0 — Скелет проекта**: `pyproject.toml`, `README.md`, `.gitignore`, пакеты `src/gausse/*`, этот файл, первый коммит. - [x] **Этап 1 — База реальных компонентов**: провод Cu/Al, конденсаторы, тиристоры/MOSFET/IGBT, датчики (Hall/оптика реальные, индукционный — оценка), материалы снаряда → `components/data/*.json`. Каждая запись честно помечена REAL (с URL источника) или ОЦЕНКА (с указанием основания); алюминиевый провод и ёмкости 1000/2200мкФ — низкая уверенность в цене, явно отмечено. - [x] **Этап 2 — Физическое ядро**: `physics/constants.py`, `inductance.py` (Уилер + ферромагнитный сердечник + размагничивание), `force.py`, `circuit.py` (ОДУ RLC). Юнит-тесты: аналитическое RLC-решение, согласованность dL/dx. - [x] **Этап 3 — Датчики и одна ступень**: `physics/sensors.py` (оба типа как события solve_ivp), `sim/stage.py` (полёт → триггер → разряд → энергобаланс). Тест на сохранение энергии + найден/исправлен баг насыщения (см. выше). - [x] **Этап 4 — Многоступенчатая цепочка**: `sim/coilgun.py` — сквозная координата, отбраковка нереализуемых конфигураций. Дымовой тест на реальной базе компонентов (`test_real_components_smoke.py`) подтверждает: полный путь реальные JSON → физика → цепочка работает (пример: 22.3 м/с, КПД 3.4% — честный неоптимизированный результат). - [x] **Этап 5 — Хранилище результатов**: `storage/schema.py` + `database.py` — SQLite (WAL), таблица `runs` со всеми прогонами (успех/провал + честная причина), однопроцессный писатель поверх многопроцессной очереди. Тест с реальными `multiprocessing.Process` (не моками) поймал реальную проблему: коллизия `run_id` роняла writer и молча останавливала осушение очереди на весь sweep — писатель теперь переживает ошибку вставки одной записи (лог в stderr) и продолжает работу. - [x] **Этап 6 — Поиск и оптимизация**: `optim/search_space.py` (геном переменной длины — число ступеней тоже эволюционирует), `objective.py` (fitness=КПД, честный мягкий штраф за нереализуемость пропорционально пройденным ступеням), `optim/sweep.py` (параллельный Monte Carlo, каждый прогон в SQLite), `optim/evolutionary.py` ((μ+λ)-ГА + Nelder-Mead полировка непрерывных параметров лучшего генома). Тест поймал реальный баг в `crossover()` — IndexError при скрещивании двух одноступенчатых геномов (пустой список зазоров), исправлено и покрыто регрессией. - [ ] **Не сделано**: отдельный "дешёвый квазистатический предфильтр" перед полным ODE не реализован (кроме уже встроенной в `sim/stage.py` дешёвой проверки порога индукционного датчика). Если миллионы прогонов на сервере окажутся слишком медленными, это первое место для ускорения. - [x] **Этап 7 — Отчётность**: `report/plots.py` (ток/поле/скорость по ступеням), `summary.py` (честная сводка + раздел "ограничения модели"), `bom.py` (спецификация деталей с ценами), `animate.py` (GIF: снаряд летит по трубе, катушки светятся пропорционально току — по запросу пользователя), `cli.py` (`gausse sweep/evolve/simulate/report`, все 4 команды реально вызывают соответствующие модули, не заглушки). Проверено сквозным прогоном через `docker compose run`: sweep → report с анимацией на реальной базе компонентов, лучший найденный результат — КПД 32.8% за 982₽ (не выдумано, из реального SQLite). - [x] **Этап 8 — Сквозная проверка**: реальный прогон через `docker compose run` — sweep на 5000 конфигураций (16 воркеров, ~59с, 30.5% реализуемо), затем `evolve` (15 поколений x 40 особей = 600 оценок, ~18с) поверх той же базы, затем `report --top 3 --animate`. - **Найден и исправлен реальный баг именно на этом этапе**: КПД лучшего генома после `evolve` показал 387% — оказалось, `efficiency` считался как `exit_kinetic_energy / energy_in`, а `exit_kinetic_energy` включает фиксированный "бесплатный" толчок `initial_v_mps`, не учтённый в `energy_in`. Для лёгкого снаряда этот толчок доминировал и КПД пробивал 100%. Исправлено на `kinetic_energy_delta / energy_in` (энергия, реально добавленная катушками) — эта величина математически не может превысить 1 (следует из поэтапного энергобаланса). После исправления лучший результат на тот момент: **1 ступень, КПД 80.97%, скорость 26.4 м/с, снаряд Ст3 ⌀4мм x 30мм, стоимость 1029₽** (провод cu-petv2-1.0mm, конденсатор 22мкФ/400В, ключ IRG4PC50F, оптический датчик). *(Историческая запись: это физика v1 БЕЗ потерь в железе, скина и трения — после их добавления такие КПД невозможны, см. «хронологию честности» ниже.)* - **Честное сравнение датчиков** (запрос из первого обсуждения плана) — на 5000 прогонах: индукционный датчик (`inductive-pickup-lm393`) дал **0.2% реализуемых** конфигураций (6 из 2965, где он стоял хотя бы на одной ступени), тогда как оптический (TCST2103) и Холла (A3144E) — **~45-47%** реализуемых каждый. Это количественно подтверждает опасение, высказанное в самом начале обсуждения плана: индукционная катушка-датчик ненадёжна именно потому, что требует скорости выше её порога (здесь — фиксированный старт 3 м/с как раз ниже порога срабатывания реального компонента), тогда как оптика/Холл не зависят от скорости. - Итог: сквозная цепочка (реальные компоненты → физика → поиск → SQLite → отчёт с графиками/BOM/анимацией) работает и произвела не выдуманный, а посчитанный и перепроверенный результат. - [x] **Обогащённая БД + лог процесса + веб-морда** (по запросу пользователя при деплое): каждая запись `runs.decoded_summary_json` теперь содержит расстояния между катушками (`inter_stage_gaps_m`), абсолютные позиции катушек вдоль трубы, все номиналы деталей, геометрию намотки, вычисленную физику (индуктивность/сопротивление/пиковый ток/поле) и результат каждой ступени (вход/выход скорость, тайминги датчика, энергобаланс) — см. `optim/objective.py::build_detail`. Ход эксперимента пишется в `.log` (`optim/progress_log.py`): прогресс, доля реализуемых, скорость, ETA, отметки нового лучшего КПД. Веб-дашборд `gausse serve` (`src/gausse/web/`, только stdlib `http.server`, self-contained HTML) — счётчики, гистограмма КПД, топ конфигураций с drill-down, причины отказа, хвост лога, автообновление 3с. В `docker-compose.yml` сервис `web` на порту 8000. 77 тестов, проверено в Docker. ## Батареи конденсаторов + графики в веб-морде (по запросу пользователя) - **Батареи конденсаторов** (`components/schema.py::CapacitorBank`): каждая ступень собирает банку из N одинаковых конденсаторов — `n_series` (1-6, складывает напряжение и ESR, делит ёмкость) × `n_parallel` (1-8, складывает ёмкость и макс.ток, делит ESR). Цена и количество масштабируются. Параметры банки — в геноме (`cap_series`, `cap_parallel` на ступень), в BOM ("N шт (Sпосл × Pпар)") и в подробной БД (`capacitor_bank`: состав, суммарные C/V/ESR/макс.ток). Диагностика по току: `bank_current_over_limit` / `switch_current_over_limit` (предупреждение, не жёсткий отказ — в базе паспортный, а не импульсный ток; параллельные конденсаторы дают реальный выигрыш через сниженный ESR). - **Уже было (тоже запрашивалось)**: разная толщина провода на разных катушках (`wire_idx` на ступень) и длина провода (`winding_geometry.total_wire_length_m` → сопротивление/цена/БД). - **Графики в дашборде**: клик по прогону → графики скорость/поле/ток строятся на лету (`web/render.py`, `/api/run//plot/.png`) + GIF пролёта по кнопке (`/api/run//anim.gif`). Рендер matplotlib сериализован локом (Agg не потокобезопасен). 86 тестов. ## Развёртывание на сервере (Proxmox 192.168.20.254 / VM 106 "test-math" = **192.168.20.47**) **СТАТУС: развёрнуто, работает АВТОНОМНО (CPU-эволюция под systemd), GPU проброшен и рабочий.** - VM 106: Ubuntu 24.04, 5 ядер, 12ГБ RAM. После перевода на q35 (нужен для PCIe-проброса) интерфейс переименовался и IP сменился **.45 → .47** (netplan починен привязкой по MAC). - [x] Пуш на gitea (`gitea.jze9.ru/jze9/gausse.git`) — рабочий процесс: пуш с локальной машины → `git pull` на VM. - [x] GPU: `qm set 106 --machine q35 --hostpci0 0000:01:00,pcie=1`; в VM драйвер NVIDIA 580 + CUDA, `cupy-cuda12x[ctk]` в `~/gausse/.venv`. `nvidia-smi` видит GTX 1070 без Code 43. - [x] Веб-морда: **http://192.168.20.47:8000** (Docker-сервис `web`). - [x] **Автономная эволюция — systemd-сервис `gausse-evolve`** (пользователь явно потребовал: «алгоритм должен считать сам, годами, без тебя»): `/etc/systemd/system/gausse-evolve.service` крутит `evolve_forever.sh` — вечный цикл `run_evolution(80 поколений × 300 особей, polish=True)` с новым случайным семенем на каждом заходе (свежая популяция = «революция» против застревания в локальном оптимуме). `Restart=always` + `enabled` → переживает и падение процесса, и перезагрузку VM. Всё пишет в одну базу `~/gausse/results/gausse.sqlite3`; «запланировано N» на дашборде — план ТЕКУЩЕГО цикла, сам поиск бесконечен. Управление: `sudo systemctl status|stop|restart gausse-evolve`, лог `journalctl -u gausse-evolve`. - Обновление кода на VM: `cd ~/gausse && git pull && docker compose build web && docker compose up -d web && sudo systemctl restart gausse-evolve`. - [x] **Этап 9 — GPU-ускорение массового sweep — СДЕЛАН**: `gpu/batch_integrator.py` — батч-RK4 фиксированного шага [Q,I,x,v] на N конфигураций одним тензором (numpy/cupy через `xp`), та же физика, что CPU-путь (общий `sim/stage.build_stage_physics` — один источник истины). Весь RK4-шаг слит в ОДНО `cupy.fuse`-ядро → **GTX 1070: 740 конф/с vs 193 на numpy = 3.8x** (наивный порт был 1.3x — упирался в запуск ядер); SYNC_EVERY=256 убирает device→host синхронизацию на каждом шаге. `gpu/batch_sweep.py` — МНОГОСТУПЕНЧАТЫЙ sweep раунд-за-раундом (подлёт аналитически, состояние снаряда переносится между раундами), CLI `gausse sweep --gpu`. Требование честности выполнено: GPU сверен с CPU-эталоном — расхождение exit_v **0.084%** (tests/test_batch_sweep.py, test_batch_integrator.py). Стиффные конфиги, переполняющие фикс-шаг, честно бракуются (blew_up), а не записываются мусором. - [ ] (опция) дослить cut-логику в ядро (>3.8x); GPU-батч внутрь эволюции. ## Физика: найденные завышения КПД и их исправления (хронология честности) Пользователь ловил нереальные цифры; каждое исправление РОНЯЛО КПД к реальности. Версия модели пишется в каждую запись БД (`model_version`), прогоны разных версий не смешиваются. 1. **Поле 13 Тл** → насыщение железа через единое потокосцепление λ(x,I)=L_air·I+L_iron·overlap(x)·g(I), g=I_sat·tanh(I/I_sat); сила и ЭДС из одного λ (coenergy) → энергия сохраняется, поле упирается в B_sat. 2. **Ток сверх рейтинга ключа** → жёсткая отбраковка по импульсному пределу. 3. **КПД 83.9%: не было потерь в железе** → вихревые токи: снаряд = короткозамкнутый виток, отражённое R_eddy=(ωM)²/R_e × overlap(x) (physics/losses.py). Лучший упал 83.9% → ~47%, медиана ~0%. 4. **Три горба тока за выстрел** (физически невозможно — конденсатор не перезарядить за мкс) → одиночный импульс: обрыв на первом нуле тока ИЛИ первом локальном минимуме; остаточная энергия катушки → freewheel-диод, считается точно через magnetic_energy (с насыщением). Энергобаланс сошёлся. 5. **gausse-physics-v2 (2026-07-08)**: скин-эффект + эффект близости обмотки (метод Доуэлла на ω=1/√(LC), `physics/ac_resistance.py`) — для толстого провода во многих слоях R_AC в разы выше DC; **трение о трубку** (0.35·m·g) и **сопротивление воздуха** (½ρ·Cd·A·v²) во всей динамике (разряд, подлёт с событием «снаряд остановлен трением», GPU-ядро, аналитический межступенчатый пролёт — точная квадратура); паспортные импульсные токи ключей из даташитов (`pulse_current_a`: ITSM/IDM/ICM) вместо generic-множителей + починен вводивший в заблуждение флаг (`switch_current_over_continuous_rating` — норма для импульса, `switch_current_over_pulse_limit` — брак). КПД ступени теперь может быть слегка отрицательным (трение съело больше слабой катушки) — это честно. Для сравнения на одной и той же эволюции: v1 давал «38%» там, где v2 даёт ~8% — столько стоили неучтённые скин и трение. ## Требования пользователя, закрытые после Этапа 8 - **Снаряд до 200 г** (⌀ до 28мм, длина до 150мм, длина зажата по плотности). - **Труба — часть поиска**: геном несёт внутренний диаметр (бор) + толщину стенки; внешний диаметр трубы = внутренний диаметр катушки (влияет на поле); снаряд обязан влезать в бор с зазором. - **До 10 ступеней**; у каждой катушки СВОЙ датчик на своём расстоянии (sensor_to_coil_distance_m на ступень; datчик не может попасть в предыдущую катушку — repair()). - **Ссылки на магазины** (`url`) у каждого компонента: в JSON-базе, BOM («Купить») и в детализации прогона. - **Максимально детальная запись** в БД (`build_detail`): труба внутр/стенка/внеш, масса снаряда в граммах и влезает-ли-в-бор, внутр/внеш диаметры катушки, состав банки конденсаторов, DC/AC сопротивление обмотки, все позиции датчиков и катушек вдоль трубы, энергобаланс каждой ступени. - **Анимация переделана** (была «время вырезается, результата не видно»): кадры на равномерной сетке ФИЗИЧЕСКОГО времени (а не по индексам массива, из-за чего плотные точки разряда съедали все кадры и подлёт выпадал), множитель замедления в кадре; в кадре сам результат эксперимента — шапка (выход м/с, КПД, ступени, энергия, масса), панель тока каждой катушки во времени, панель скорости снаряда, бегущий курсор, датчики на трубе. ## Ограничения модели / следующие шаги по реализму (если потребуются) - Поршневой эффект воздуха в трубе (учтено только лобовое сопротивление). - Диффузия поля в снаряде (наш r_eddy — сосредоточенная оценка 1-го порядка). - Спектр импульса для Доуэлла (взята одна характерная частота). - Нагрев провода за импульс/серию; коэффициент трения не измерен (0.35). - Гистерезис посчитан (losses.hysteresis_energy_j) и СОЗНАТЕЛЬНО не в динамике: <0.1% энергии выстрела — включение изображало бы ложную точность. - Индуктивность — Уилер+размагничивание, не МКЭ/FEA. - Осторожно: не тюнить модель под «желаемую» цифру КПД — только физика.