Разбор реальных кодов
Восемь программ физики ускорителей: задача, устройство, ключевое решение, что сделано хорошо и что требует доработки
Вячеслав Федоров
Лекция 15
Разработка и применение ПО
в физических исследованиях
Изложение. Пятнадцатая лекция завершает курс и посвящена разбору реальных программ из части книги «Разбор реальных кодов». Рассматриваются восемь программ физики ускорителей: расчёт огибающей пучка KENV, подбор полей соленоидов генетическим алгоритмом, расчёт динамики макрочастиц REDPIC, коррекция равновесной орбиты, трёхмерная схема установки на CadQuery, цифровой двойник ACCUMULATOR вместе с библиотекой оркестрации экспериментов SCAUT, мультиагентный помощник оператора SAVA и автономный исследователь на языковых моделях. Ни одна из этих программ не является идеальной: каждая имеет свою историю и свои компромиссы, продиктованные задачей, поэтому читать их необходимо критически. Для каждой программы рассматриваются физическая задача, устройство, ключевое решение, приёмы курса, заметные в коде, и то, что требует доработки; такой разбор позволяет увидеть, из чего состоит чужой рабочий код. Надпись на титуле — diff: чтение чужого кода сравнивает сделанное с тем, чего требует задача.
Демонстрации. Слайды с розовым выделением слова «Демонстрация» отмечают места, где изложение прерывается для работы в терминале виртуальной машины lab с Python 3.12, KENV 0.3.0.5, REDPIC 0.11.8, DEAP 1.4, scikit-learn 1.9 и caproto 1.3, далее называемой стендом: на слайде проигрывается запись тех же команд. Расчёты уменьшены так, чтобы каждый занимал секунды: популяция генетического алгоритма, число макрочастиц и длина тракта меньше, чем в книге. Цифровой двойник на стенде представлен моделью его устройства на caproto с каналами только на петлевом интерфейсе 127.0.0.1, а не двойником института; языковые модели не вызываются, показываются сохранённые результаты их работы. Рисунок трёхмерной схемы построен на рабочем компьютере: CadQuery 2.8 с VTK на стенд не устанавливался.
Содержание
Восемь программ и общие уроки
Изложение. Лекция состоит из шести частей. Первая посвящена огибающей пучка: библиотеке KENV и генетическому алгоритму, подбирающему поля соленоидов и решающему обратную задачу. Вторая — динамике макрочастиц в REDPIC: алгоритму, устройству пакета, его окружению и поведению как библиотеки. Третья — коррекции равновесной орбиты по матрице отклика с сингулярным разложением и нейросетью, обученной на остатках. Четвёртая объединяет установку и её двойник: схему установки, построенную кодом на CadQuery, цифровой двойник ACCUMULATOR и библиотеку SCAUT. Пятая посвящена языковым моделям в пультовой: помощнику оператора SAVA и автономному исследователю. Шестая собирает сквозные уроки и связывает их с итоговым проектом курса.
Цель лекции
Читать чужой рабочий код критически и проверять его результат независимым способом
Курс давал приёмы по отдельности: структура проекта, тесты, CI, типизация, производительность, упаковка, документация, воспроизводимость. В реальной программе они встречаются вместе и в разной степени. Разбор показывает, какие из них окупились, каких не хватило и как недостающее обнаруживается простой проверкой: формулой, сравнением с независимым кодом, повторным запуском.
Изложение. Цель лекции — научиться читать чужой рабочий код критически и проверять его результат независимым способом. В предыдущих лекциях приёмы разработки рассматривались по отдельности: структура проекта и упаковка, тесты и непрерывная интеграция, типизация, профилирование и ускорение, документация, воспроизводимость окружения и результатов. В реальной программе они встречаются вместе и в разной степени: у одной программы есть пакет на PyPI, но нет тестов, у другой — десятки тестов и конвейер проверок, но и сложность, которую приходится сопровождать. Разбор позволяет увидеть, какие приёмы окупились, каких не хватило и как недостающее обнаруживается простой проверкой — формулой для частного случая, сравнением с независимым кодом, повторным запуском на другой машине. Именно такие проверки находят в рассматриваемых программах настоящие ошибки, и это главный предмет лекции.
Восемь
программ
Программа Задача Форма Регистрация
KENV огибающая пучка пакет PyPI № 2024611244
ГА на DEAP подбор полей, обратная задача код главы —
REDPIC динамика макрочастиц пакет PyPI № 2023688768
Коррекция орбиты SVD и сеть на остатках код главы —
CadQuery схема установки кодом код главы —
ACCUMULATOR, SCAUT двойник, оркестрация сервис, библиотека № 2026668488, № 2025619977
SAVA помощник оператора 33 сервиса не оформлена
AI Scientist автономный исследователь форк с шаблоном —
01
Задача
физика и постановка
02
Устройство
модули и границы
04
Курс в коде
тесты, CI, пакет, типы
Изложение. Таблица перечисляет восемь программ в порядке изложения. KENV и REDPIC оформлены пакетами на PyPI и зарегистрированы в Реестре программ для ЭВМ, ACCUMULATOR и SCAUT зарегистрированы как программа-двойник и библиотека оркестрации экспериментов, свидетельство на SAVA пока не оформлено. Генетический алгоритм, коррекция орбиты и схема установки представляют собой код глав книги, а автономный исследователь — форк открытой системы AI Scientist с шаблоном для ускорительной задачи. Каждая программа разбирается по одной схеме из пяти вопросов: какую физическую задачу она решает, как устроена, какое решение в ней ключевое, какие приёмы курса заметны в коде и что требует доработки. Последний вопрос важнее остальных: ответ на него даёт не чтение, а проверка, которая позволяет отличить работающий код от правильного — запуск на другом окружении, сравнение с формулой или с другой программой.
Огибающая
KENV: уравнение Капчинского—Владимирского, пакет и проверка результата; генетический алгоритм и обратная задача
01
Огибающая
Динамика частиц
Коррекция орбиты
Установка и двойник
ИИ в пультовой
Уроки
Изложение. Первая часть посвящена огибающей пучка: библиотеке KENV и генетическому алгоритму, который на её основе подбирает поля соленоидов и восстанавливает параметры пучка по измерениям.
Уравнение
огибающей
P = 2 * self.beam.current / \ (consts.alfven_current * (g * beta)**3) dxdz = xp dxpdz = 2 * P / (x + y) + emitt_x * emitt_x / x**3 \ - K_x * x - dgdz * xp / (beta * beta * g) \ - d2gdz2 * x / (2 * beta * beta * g)
Для физика
ВКР: тракт 15 м, 1,5 кА — ASTRA около 4 ч на 10⁶ частиц, UltraSAM трое суток, KENV — секунды при совпадении огибающих.
01
2P/(x + y)
пространственный заряд расталкивает пучок
02
ε²/x³
эмиттанс не даёт сжать пучок в точку
03
K·x
соленоиды и квадруполи фокусируют
04
γ′, γ″
ускорение: адиабатическое затухание
Изложение. KENV решает уравнение огибающей для пучка с распределением Капчинского—Владимирского: все частицы лежат на поверхности четырёхмерного эллипсоида, поле пространственного заряда внутри такого пучка линейно, и пучок целиком описывается полуосями x и y. На слайде приведён фрагмент правой части уравнения из файла kenv/solver.py. Рассмотрим его члены. Первый, 2P/(x + y), описывает расталкивание пространственным зарядом; первеанс P = 2I/(I_A(βγ)³) с альфвеновским током I_A = 17 кА быстро убывает с энергией. Второй, ε²/x³, отражает конечный эмиттанс и не позволяет сжать пучок в точку. Третий, K·x, представляет собой фокусировку: жёсткость соленоида пропорциональна квадрату поля, квадруполь фокусирует в одной плоскости и дефокусирует в другой. Последние два члена с производными γ описывают ускорение и адиабатическое затухание. Таким образом, вместо миллионов частиц решаются четыре обыкновенных дифференциальных уравнения первого порядка, и именно это позволяет вызывать KENV тысячи раз в цикле оптимизации. Цена такого упрощения — применимость только там, где распределение близко к однородному, а поля линейны. Выигрыш показан в ВКР: на тракте длиной 15 м при токе 1,5 кА расчёт ASTRA на миллионе частиц занимал около четырёх часов, UltraSAM — трое суток, а KENV — несколько секунд, и огибающие трёх программ совпали.
Устройство
KENV 0.3.0.5
Beam
энергия, ток, r, r′, εn
Accelerator
Element × N, compile() Bz(z), Ez(z), Gz(z)
Equations
envelope_prime(z, X)
Simulation.track()
solve_ivp, RK23 → envelope_x(z)
01
Пакет
PyPI, сайт документации, свидетельство
02
Интерфейс
параметры Beam только по имени, единицы в docstring
03
Скорость
десятки миллисекунд на огибающую
Для физика
Скорость расчёта позволила построить интерфейс настройки проводки пучка в реальном времени: меньше тестовых импульсов на настройку.
Изложение. Пакет KENV состоит из трёх основных модулей. Модуль beam.py описывает пучок классом Beam: энергия, ток, радиус, угловой разлёт и нормализованный эмиттанс, причём все параметры передаются только по имени, а единицы указаны в строках документации. Модуль accelerator.py собирает тракт: каждый элемент Element хранит положение центра, максимум поля и имя файла с профилем поля вдоль оси, а метод compile() сдвигает профили, масштабирует их и суммирует в функции Bz(z), Ez(z) и Gz(z) на общей сетке. Модуль solver.py содержит правую часть уравнения в классе Equations и класс Simulation, метод track() которого интегрирует уравнения функцией solve_ivp методом Рунге—Кутты RK23 с относительной точностью 10⁻⁴ и шагом не крупнее шага сетки, а результат сохраняет в атрибутах envelope_x и envelope_y в виде интерполирующих функций от z. Разделение на пучок, тракт и расчёт является удачным: оно позволяет собирать тракт из типовых частей, а расчёт не знает, откуда взяты поля. Пакет опубликован на PyPI, снабжён сайтом документации и зарегистрирован в Реестре программ для ЭВМ, № 2024611244 от 18.01.2024. Скорость расчёта позволила построить поверх KENV графический интерфейс для настройки проводки пучка в реальном времени, что сокращает число тестовых импульсов и бережёт ресурс установки.
Демонстрация:
огибающая KENV
01
Тракт главы: 9 соленоидов, пучок 2 МэВ, 2 кА
02
Все поля 0,02 Тл: огибающая волной
03
0,1 Тл: перетяжка до долей миллиметра
04
0,2 Тл: тот же вызов без исключений
05
pip show kenv: объявленные зависимости
Что наблюдается
0,02 Тл: 54 мс, r max 131,2 мм. При 0,2 Тл r min = −11 895 891,7 мм без исключений; Requires пуст.
root@lab — запись со стенда
Демонстрация. Скрипт env.py собирает тракт главы о генетическом алгоритме: девять соленоидов через 0,5 м на отрезке 0–5 м и пучок 2 МэВ, 2 кА, радиус 50 мм, разлёт 50 мрад, эмиттанс 1000 мм·мрад; профиль поля соленоида задан аналитически, а с одноимённым профилем Bz.dat из тестовых данных репозитория REDPIC при 0,2 Тл возникает та же ошибка. При поле 0,02 Тл во всех соленоидах 500 точек огибающей рассчитываются за 54 мс: пучок проходит тракт волной с максимумом 131,2 мм при z = 2,01 м и выходит с радиусом 18,6 мм. При 0,1 Тл фокусировка сильнее, и пучок сжимается до 0,7 мм уже при z = 0,73 м. При 0,2 Тл тот же вызов завершается без исключения и без предупреждения, но огибающая уходит в отрицательную область: r min = −11 895 891,7 мм, а проверка r > 0 нарушена в 193 точках из 500. Причина состоит в том, что при таком поле угол огибающей далеко выходит за пределы параксиального приближения, а перетяжка, где члены ε²/x³ и 2P/(x + y) неограниченно растут, много уже шага интегрирования: решатель перешагивает через ноль и при этом сообщает об успешном завершении. Статус решателя этой ошибки не показывает, а физическую допустимость результата библиотека не проверяет. Проверка r > 0 в скрипте занимает одну строку и позволяет обнаружить ошибку сразу. Команда pip show показывает вторую особенность: у пакета не объявлено ни одной зависимости, поэтому pip install kenv не устанавливает ни numpy, ни scipy.
KENV:
что улучшить
beam_envelope = solve_ivp( equations.envelope_prime, t_span=[z_start, z_finish], y0=X0_beam, max_step=self.accelerator.dz, t_eval=self.accelerator.parameter, method=method, rtol=rtol, atol=atol ).y # статус и r > 0 не проверяются dz = (z_data[-1] - z_data[0]) / len(z_data) # шаг профиля: делитель len - 1
01
Результат
статус решателя и r > 0 не проверяются
02
Пакет
setup.py импортирует kenv, зависимостей нет
03
Повторы
15 вызовов interp1d с одними параметрами
04
Состояние
атрибут: список → массив → функция
05
Копия
тот же read_fields в REDPIC
Изложение. Рассмотрим по порядку, что требует доработки. Первое — проверка результата. Вызов solve_ivp возвращает объект с полями status и message, однако библиотека берёт из него только массив .y; необходимо проверять статус и физическую допустимость, хотя бы r > 0, и сообщать об ошибке, а не возвращать километры отрицательного радиуса; в демонстрации solve_ivp завершился со статусом успеха, и ошибку обнаружила только проверка r > 0. Второе — упаковка: setup.py импортирует сам пакет ради номера версии, то есть требует numpy и scipy ещё до установки, и не объявляет install_requires, а файл requirements.txt в репозитории закрепляет версии для тетрадей, но не для pip install kenv. Третье — повторы: в solver.py 15 одинаковых вызовов interp1d с теми же четырьмя параметрами, которые сводятся к одной вспомогательной функции. Четвёртое — состояние объекта: атрибут envelope_x сначала является пустым списком, затем массивом и наконец интерполирующей функцией; аннотации типов с проверкой mypy позволяют обнаружить это сразу. Вторая строка кода на слайде вычисляет шаг профиля поля делением на число точек, а не на число интервалов, и завышает производную поля на n/(n − 1), то есть на 0,6 % для профиля из 161 точки. Пятое — копирование кода между пакетами: функция read_fields вместе с классом Element почти без изменений повторена в REDPIC, и исправление в одной копии не попадает в другую, поэтому общий код выносят в отдельную библиотеку описания тракта.
Генетический
алгоритм
creator.create("FitnessMin", base.Fitness, weights=(-1.0,)) creator.create("Individual", list, fitness=creator.FitnessMin) toolbox.register("attr_float", random.gauss, Sol_B, (Sol_B_max - Sol_B_min) / 2) toolbox.register("evaluate", evalution_envelope) toolbox.register("mate", tools.cxUniform, indpb=CX_INDPB) toolbox.register("mutate", tools.mutGaussian, mu=0, sigma=(Sol_B_max - Sol_B_min) / 2, indpb=MUTPB) toolbox.register("select", tools.selTournament, tournsize=TOURN) toolbox.decorate("mate", clip) toolbox.decorate("mutate", clip)
01
Особь
9 полей соленоидов, 5–50 мТл
02
Приспособленность
средний |r − r_зад| по тракту
03
Отбор
турнир из 3; скрещивание 0,4; мутация 0,6
04
Цена
не больше 100 × 100 = 10⁴ огибающих
Для физика
Градиента нет, расчёт дёшев: 54 мс на стенде, до 10⁴ огибающих — до 9 мин в одном процессе.
Изложение. Обратная к расчёту задача встречается чаще: известна желаемая огибающая, а подобрать необходимо поля соленоидов. Генетический алгоритм работает по аналогии с естественным отбором: популяция особей оценивается, лучшие отбираются, скрещиваются и мутируют, и цикл повторяется. На слайде — код главы на библиотеке DEAP. Особь представляет собой список из девяти полей соленоидов, гены разыгрываются гауссовыми случайными величинами около 0,02 Тл, скрещивание равномерное, мутация добавляет гауссов шум, отбор турнирный из трёх особей, а функция приспособленности пересобирает тракт, рассчитывает огибающую KENV и возвращает среднее отклонение от заданной кривой. Декоратор clip возвращает поля в коридор 5–50 мТл после скрещивания и мутации. Градиентный спуск здесь неудобен: целевая функция получается численным интегрированием, аналитической производной по полям у неё нет, а численная потребовала бы девяти дополнительных расчётов на шаг. Вместе с тем один расчёт дёшев: оценим цену прогона главы — сто особей на сто поколений дают не больше 10⁴ огибающих, поскольку eaSimple пересчитывает только изменённых особей, в журнале главы около 76 за поколение, и при 54 мс на стенде это 7–9 минут в одном процессе. Таким образом, метод оптимизации выбирается не только по виду целевой функции, но и по цене её вычисления. В главе за 80 поколений приспособленность лучшей особи снизилась с 0,034 до 0,0108 м.
Демонстрация:
генетический поиск
01
Код главы: популяция 50, 15 поколений
02
Расчёт особей в двух процессах: Pool.map
03
Два запуска: seed 1 и seed 2
04
Рисунок: огибающие и поля двух запусков
Что наблюдается
Вне коридора 30–37 % начальных генов. Лучшая в последнем поколении 17,1 мм, за прогон — 13,0 мм; поля расходятся до 39,3 мТл.
root@lab — запись со стенда
Демонстрация. Скрипт ga.py повторяет код главы с уменьшенной популяцией, 50 особей на 15 поколений, и распределяет расчёт особей по двум процессам: DEAP позволяет подменить функцию map пулом процессов одной строкой. Первая строка вывода показывает особенность кода главы: гены начальной популяции разыгрываются гауссом со стандартным отклонением 22,5 мТл около 20 мТл и через clip не проходят, поэтому вне коридора 5–50 мТл оказывается 137 генов из 450 при seed 1 и 166 при seed 2, то есть 30 и 37 %. Журнал первого запуска показывает лучшую невязку 16,7 мм в нулевом поколении, 13,0 мм в пятом и 17,1 мм в пятнадцатом: схема eaSimple не сохраняет элиту, и лучшая особь теряется. Код главы берёт лучшую особь из последнего поколения, а объект HallOfFame, добавленный в скрипт, хранит лучшую за весь прогон — 13,0 мм против 17,1 мм. Второй запуск даёт 13,9 мм за прогон. Расчёт 618 огибающих занял 21 с в двух процессах. Поля двух лучших решений различаются до 39,3 мТл при близкой невязке: короткие прогоны останавливаются в разных точках пространства полей, поэтому единственность решения проверяется несколькими запусками, что существенно для обратной задачи.
Результат:
два запуска ГА
Что наблюдается
От 2,5 до 4,4 м обе огибающие идут по заданной, у выхода первая уходит к 56 мм; невязка 13,0 и 13,9 мм; часть полей прижата к границам 5 и 50 мТл.
Демонстрация. Слева серая кривая — огибающая при поле 20 мТл во всех соленоидах: пучок проходит тракт волной с максимумом 131,2 мм, как в демонстрации KENV. Пунктир — заданная парабола: 150 мм на входе, 50 мм в середине и 30 мм на выходе; физического смысла она не имеет и в начале тракта недостижима, поскольку пучок входит с радиусом 50 мм. Синяя и оранжевая кривые — лучшие решения двух запусков со средней невязкой 13,0 и 13,9 мм: от 2,5 до 4,4 м обе идут по заданной кривой, а у выхода синяя поднимается к 56 мм при заданных 30 мм. Справа — поля девяти соленоидов: у двух решений они различаются до 39,3 мТл, а часть значений прижата к границам коридора 5 и 50 мТл. Близкую невязку дают сильно различающиеся поля, а сами огибающие расходятся до 29 мм у z = 1 м: два коротких прогона останавливаются в разных точках пространства полей, и единственность решения необходимо проверять несколькими сходящимися запусками.
Обратная
задача
Начальная популяция μ σ
поля соленоидов 0,04 Тл 0,02 Тл
радиус r 48 мм 24 мм
разлёт r′ 38 мрад 20 мрад
эмиттанс εn 1150 мм·мрад 500 мм·мрад
Для физика
Ток 1,5 кА: r = 42–49 мм, r′ = 35–38 мрад, εn = 1125–1215 мм·мрад — область решений, а не точка.
01
Особь
12 полей и 3 начальных условия
02
Популяция
1500 особей, 100 поколений
03
Код главы
начальная популяция мимо clip, итог — последнее поколение
04
Исправление
границы в генераторе особи, HallOfFame, seed
Важно
Оптимизатор возвращает одну точку; единственность проверяется запусками из разных начальных условий.
Изложение. Реальная задача является обратной: на линейном индукционном ускорителе диагностики внутри тракта нет, поэтому измеряется диаметр пучка на выходном экране при разных токах квадрупольной линзы, и по этой кривой требуется восстановить начальные радиус, разлёт и эмиттанс. Постановка для генетического алгоритма меняется незначительно: особью становятся двенадцать полей и три начальных условия, приспособленностью — отклонение расчётной кривой от измеренной, а популяция вырастает до 1500 особей, поскольку размерность поиска выросла с 9 до 15. Для пучка с током 1,5 кА получена область эквивалентности: r = 42–49 мм, r′ = 35–38 мрад, εn = 1125–1215 мм·мрад. Это не погрешность измерения, а множество начальных условий, одинаково хорошо согласующихся с экспериментом, и результатом обратной задачи необходимо приводить область, а не точку. Код главы требует трёх правок. Ограничение коридора задано декоратором только на скрещивание и мутацию, поэтому треть генов начальной популяции выходит за пределы источника питания; ограничение должно задаваться в генераторе особи. Итог берётся из последнего поколения, а eaSimple не сохраняет элиту; достаточно передать объект HallOfFame. Наконец, генератор random в главе не инициализируется, и прогон невоспроизводим; seed необходимо задавать и сохранять вместе с результатом.
Динамика частиц
REDPIC: релятивистская разностная схема на Numba, устройство пакета, окружение и поведение библиотеки
02
Огибающая
Динамика частиц
Коррекция орбиты
Установка и двойник
ИИ в пультовой
Уроки
Изложение. Вторая часть посвящена REDPIC — расчёту динамики пучка как ансамбля макрочастиц по релятивистской разностной схеме: алгоритму, устройству пакета, его окружению и поведению как библиотеки внутри чужой программы.
Разностная
схема
# F = H/γ в безразмерных единицах b2 = 1 + Fx[i] ** 2 + Fy[i] ** 2 + Fz[i] ** 2 b1 = 2 - b2 b3 = 2 * (vx * Fx[i] + vy * Fy[i] + vz * Fz[i]) fx = 2 * (vy * Fz[i] - vz * Fy[i]) fy = 2 * (vz * Fx[i] - vx * Fz[i]) fz = 2 * (vx * Fy[i] - vy * Fx[i]) vx = (vx * b1 + fx + Fx[i] * b3) / b2
Пояснение
Поле пучка — прямая сумма по парам частиц: O(N²), при 10 000 частиц около 10⁸ пар на каждом шаге.
01
Поле E
в точках частиц: p += 2E за 2δt
02
Сдвиг
v = p/γ, r += v за δt
03
Поле H
в новых точках: поворот v, |v| сохраняется
04
Сдвиг
r += v, затем p = vγ
Изложение. Уравнение огибающей описывает пучок одной кривой; там, где существенно поведение отдельных частиц — расслоение по энергии, гало, структура фазового портрета, — пучок рассчитывают как ансамбль макрочастиц. REDPIC реализует релятивистскую разностную схему, возникшую в работах по транспортировке сильноточного пучка в ЛИУ-5. Величины обезразмериваются: координаты делятся на cδt, поля умножаются на eδt/(mc). Один цикл схемы занимает 2δt: по электрическому полю в точках частиц импульс получает приращение 2E, по новым импульсам вычисляются скорости и координаты сдвигаются на δt, затем в новых точках вычисляется магнитное поле и скорость поворачивается, координаты сдвигаются ещё раз, а импульс восстанавливается умножением скорости на γ. На слайде — поворот скорости из кода: все члены построены на векторе F = H/γ, и такая запись обеспечивает точное сохранение модуля скорости, поэтому чисто магнитное поле не меняет энергию частицы. Поле пучка вычисляется прямым суммированием по парам частиц: оценим цену — при десяти тысячах частиц это около 10⁸ пар на каждом шаге, поэтому ядро написано на Numba с параллельным циклом.
Устройство
REDPIC 0.11.8
Генераторы
RadialUniform, Uniform, Astra
BaseSimulation
track(n_files)
REDSimulation
_first_step_red _second_step_red
Accelerator
Element, compile()
utils.field
@jit, prange cuda.jit
вверх — наследование, вправо — вызов полей
01
Пакет
PyPI, зависимости объявлены
02
CI
flake8, pylint, pytest на 3.10–3.12
03
Классы
абстрактные базы, инверсия зависимостей
04
Скорость
Numba: JIT, параллельные циклы, CUDA
Пояснение
Выпуск в GitHub Actions публикует пакет на PyPI и документацию; Dockerfile фиксирует Python 3.10, но не версии библиотек.
Изложение. REDPIC устроен по принципам, которые разбирает его ВКР. Принцип единственной ответственности даёт три сущности: пучком занимается класс пучка, трактом — Accelerator, запуском счёта — Simulation. Инверсия зависимостей проведена через абстрактные классы BaseBeam и BaseSimulation, что позволяет расчёту работать с любым генератором пучка — радиально-однородным, однородным или распределением, прочитанным из файла ASTRA, — а новый генератор не требует правок расчёта. Частицы хранятся в DataFrame pandas, а результат — в словаре кадров по ходу трекинга. Ядро вычислений — функции с декоратором jit из Numba с параллельными циклами prange, и для каждой есть вариант для графического процессора. Инфраструктура проекта образцовая для научного кода: пакет на PyPI с объявленными зависимостями, Dockerfile, конвейер GitHub Actions из трёх рабочих процессов — проверка flake8 и pylint, функциональные тесты pytest на Python 3.10–3.12 и выпуск с публикацией пакета и документации. Функциональный тест построен по сценарию из ВКР: создать пучок и тракт, запустить расчёт, сравнить распределение частиц с эталонным критерием Колмогорова—Смирнова. Программа зарегистрирована, № 2023688768 от 25.12.2023. Дальше рассмотрим, как этот пакет ведёт себя на свежем окружении и внутри чужой программы.
Демонстрация:
окружение REDPIC
01
Пример из главы на SciPy 1.18
02
Объявленные и установленные версии
03
Тот же расчёт с заменой функции
04
Рисунок: макрочастицы и край пучка
Что наблюдается
scipy.misc.derivative удалена, а scipy объявлена без верхней границы. С заменой функции 6000 частиц рассчитаны за 4,4 с.
root@lab — запись со стенда
Демонстрация. Скрипт pic.py строит начало тракта главы, три соленоида на отрезке 0,7–2,5 м, и пучок 1,32 МэВ, 500 А длиной 2 м из 6000 макрочастиц; профиль поля задан аналитически, а поля главы умножены на 0,6, так как этот профиль шире профилей главы — файлов Bz.dat и Ez.dat из тестовых данных репозитория REDPIC. На стенде с SciPy 1.18 расчёт завершается исключением AttributeError: в модуле scipy.misc больше нет функции derivative, которой REDPIC вычисляет вторую производную поля. Модуль scipy.misc объявлен устаревшим ещё в SciPy 1.10, а в SciPy 1.15 функция удалена; пакет же требует scipy>=1.11.2 без верхней границы, как и остальные четыре зависимости, — скрипт req.py показывает объявленные требования рядом с установленными версиями. Таким образом, установка на свежее окружение проходит без ошибок, а первый же расчёт завершается исключением. С ключом --shim скрипт до импорта REDPIC подставляет ту же центральную разность, которую вычисляла удалённая функция, и расчёт проходит: 6000 частиц, из них 4965 в тракте, 21 кадр результата, 4,4 с. Край пучка 2σx растёт от 68,3 мм при z = 0,95 м до 111,0 мм при z = 2,15 м. Последний шаг строит рисунок по сохранённым частицам.
Результат:
запуск REDPIC
Что наблюдается
Край пучка 2σx растёт от 49 до 111 мм; голова сгустка к концу счёта у 2,43 м, поэтому слои взяты до 2,2 м.
Демонстрация. Серые точки — радиусы 4965 макрочастиц, находящихся в тракте в последнем кадре расчёта, оранжевые кружки — край пучка 2σx по слоям длиной 10 см. Пучок входит с радиусом 48 мм и расходится, а ослабленные соленоиды лишь замедляют рост: край пучка увеличивается от 49 до 111 мм. Сгусток длиной 2 м движется со скоростью 0,96c, и к концу счёта его голова находится у 2,43 м, поэтому слои взяты только до 2,2 м. Все кадры результата REDPIC хранит в памяти как словарь таблиц pandas, и рисунок строится по последнему из них несколькими строками.
Четыре строки
с последствиями
# redpic/utils/jit.py def jit(func): def wrapper(*args, **kwargs): ... return numba_jit(...)(func)(*args, **kwargs) return wrapper # redpic/solver/red.py if bool(beam.total_charge for beam in self.beams): print("\r%.1f %% " % progress, end="") # redpic/core/config.py logging_config.dictConfig(LOGGING)
01
jit
новый диспетчер Numba при каждом вызове
02
bool(генератор)
всегда True: поле пучка и при нулевом токе
03
print
прогресс в stdout без отключения
04
dictConfig
импорт настраивает корневой журнал
Важно
По отдельности строки не меняют результата; вместе они замедляют расчёт и вмешиваются в чужую программу.
Изложение. Рассмотрим четыре строки REDPIC, которые не меняют результата расчёта, но заметны в работе. Первая — собственный декоратор jit: он не компилирует функцию один раз, а внутри обёртки при каждом вызове создаёт новый диспетчер Numba и только благодаря кешу на диске не компилирует её заново; накладные расходы ложатся на каждый вызов. Вторая — проверка заряда в трекинге: выражение bool(генератор) всегда истинно, поскольку объект генератора является истинным независимо от содержимого, и поле пучка за O(N²) вычисляется даже при нулевом токе; функция any() проверяла бы именно то, что задумано. Третья — печать прогресса через print в стандартный вывод, которую нельзя отключить средствами журнала. Четвёртая — вызов dictConfig в модуле конфигурации: импорт библиотеки настраивает корневой журнал всей программы. Результата ни одна из строк не искажает, однако проверка через bool() является логической ошибкой, а вместе строки делают расчёт медленнее, чем позволяет Numba, и меняют поведение программы, импортировавшей пакет. Следующая демонстрация измеряет первые две, а последствия четвёртой показаны в шестой части.
Демонстрация:
скорость REDPIC
01
Исходный текст декоратора jit
02
Проверка заряда в трекинге
03
Поле пучка: обёртка против njit
04
Трекинг при нулевом токе
Что наблюдается
300 частиц: 1,36 мс на вызов через обёртку против 0,15 мс. При токе 0 А поле пучка вызвано 180 раз и заняло 1,5 с из 2,0 с.
root@lab — запись со стенда
Демонстрация. Скрипт src.py печатает исходный текст установленного пакета: декоратор jit с вызовом numba_jit(...)(func) внутри обёртки и строку трекинга с проверкой bool(beam.total_charge for beam in self.beams). Скрипт perf.py измеряет их последствия. Функция суммирования поля по парам частиц вызывается через обёртку REDPIC и через диспетчер, созданный один раз: при 300 частицах вызов обходится в 1,36 мс против 0,15 мс, то есть накладные расходы обёртки в восемь раз больше самой работы, а при 3000 частиц — 16,11 мс против 14,92 мс, и разница около 1,2 мс на вызов сохраняется. Затем запускается трекинг 4000 частиц с нулевым током: функция поля пучка вызвана 180 раз — по два раза на каждый из 90 шагов — и заняла 1,5 с из 2,0 с всего трекинга, хотя при нулевом заряде её результат равен нулю. Таким образом, три четверти времени такого расчёта уходят на работу, которую проверка заряда должна была отменить; исправление занимает одно слово.
REDPIC:
что улучшить
01
Окружение
верхние границы версий, CI по расписанию
02
Журнал
NullHandler вместо dictConfig, без print
03
Пакет
find_packages без каталога tests
04
Скорость
диспетчер один раз, any() вместо bool
05
Воспроизводимость
seed у генератора пучка
# setup.py packages=find_packages(exclude=["tests", "tests.*"]) # redpic/__init__.py logging.getLogger("redpic").addHandler( logging.NullHandler()) # redpic/utils/jit.py def jit(func): return func if cfg.DISABLE_JIT else numba_jit( nopython=True, parallel=True, fastmath=True, cache=True, nogil=True)(func) # redpic/solver/red.py if any(beam.total_charge for beam in self.beams):
Изложение. Доработки REDPIC касаются окружения и поведения библиотеки. Первое — окружение: верхние границы версий или файл блокировки для воспроизводимой установки и запуск тестов CI по расписанию: конвейер REDPIC и так ставит новейшие версии зависимостей, поскольку они не закреплены, но запускается только при изменении кода, а запуск по расписанию обнаружил бы удаление scipy.misc.derivative раньше пользователей. Второе — журнал: библиотека добавляет к своему журналу NullHandler и не трогает корневой, а прогресс сообщает через журнал или функцию обратного вызова, а не через print. Третье — упаковка: find_packages с исключением каталога tests не кладёт посторонний пакет в site-packages. Четвёртое — скорость: декоратор создаёт диспетчер Numba один раз, при определении функции, а проверка заряда использует any(). Пятое — воспроизводимость: генераторы пучка пользуются глобальным генератором np.random и не принимают параметра seed, поэтому повторить пучок можно только через глобальное состояние; явный генератор numpy.random.Generator делает воспроизводимость свойством вызова. На слайде справа — набросок первых четырёх исправлений, каждое из которых занимает одну-две строки.
Коррекция орбиты
Матрица отклика и SVD с отсечением мод, нейросеть на остатках и повторный запуск кода главы
03
Огибающая
Динамика частиц
Коррекция орбиты
Установка и двойник
ИИ в пультовой
Уроки
Изложение. Третья часть посвящена коррекции равновесной орбиты: линейной алгебре матрицы отклика, нейросети, обученной на остатках, и тому, что показывает повторный запуск кода главы на другой машине.
Матрица отклика
и SVD
def svd_correction(R, x, n_modes): """Коррекция орбиты с обрезанием SVD-спектра.""" U, s, Vt = np.linalg.svd(R, full_matrices=False) s_inv = np.zeros_like(s) s_inv[:n_modes] = 1.0 / s[:n_modes] return -(Vt.T * s_inv) @ (U.T @ x)
Пояснение
x = Rθ, R = UΣV⊤, θ = −VΣ⁺U⊤x; Rij = √(βiβj)·cos(|φi − φj| − πν) / (2 sin πν).
01
Отклик
R 64 × 48: мониторы на корректоры, ν = 8,42
02
Обращение
псевдообратная матрица через SVD
03
Малые σk
усиливают шум, моды отсекают
04
Компромисс
остаток орбиты против токов
Для физика
Линак инжектора СКИФ: 7 датчиков положения, 6 больших и 8 малых корректоров; матрица измеряется.
Изложение. Рассмотрим сначала линейную постановку. Ошибки дипольного поля искажают замкнутую орбиту, и пучок, смещённый с оси, теряет частицы на стенках камеры. Для коррекции в кольце установлены мониторы положения и слабые дипольные корректоры. Пока отклонения малы, ускоритель является линейной системой: вектор отклонений в мониторах равен матрице отклика, умноженной на вектор углов корректоров, а элемент матрицы выражается через бета-функции и набеги фаз. Глава строит модельное кольцо с 64 мониторами, 48 корректорами и бетатронной частотой 8,42. Найти углы, компенсирующие измеренную орбиту, значит решить переопределённую систему в смысле наименьших квадратов, и стандартным инструментом является сингулярное разложение. Малые сингулярные числа соответствуют комбинациям корректоров, почти не влияющим на орбиту; при обращении они многократно усиливают шум мониторов, поэтому сохраняют только n сильных мод — функция на слайде делает именно это. Число мод является компромиссом между остатком орбиты и величиной токов. Матрицу можно рассчитать по модели, а можно измерить, отклоняя корректоры по очереди; на линейном ускорителе инжектора ЦКП «СКИФ» с 7 датчиками положения и 14 двухкоординатными корректорами матрицу измеряют, что заодно позволяет проверить модель оптики.
Сеть
на остатках
svd_answer = orbits @ svd_matrix.T # что предложил бы SVD residuals = -thetas - svd_answer # чего ему не хватило net = make_pipeline( StandardScaler(), MLPRegressor(hidden_layer_sizes=(64, 32), max_iter=300, early_stopping=True, random_state=0), ) net.fit(orbits, residuals) def hybrid_step(x): return svd_matrix @ x + net.predict(x[None, :])[0]
01
Линейная задача
сеть 0,151 мм против 0,028 мм у SVD
02
Насыщение
θmax·tanh(θ/θmax): отклик нелинеен
03
Гибрид
SVD плюс поправка сети на остатке
04
Итерации
решают ту же задачу: вклады разделяются
Важно
Если сеть превзошла SVD на линейной задаче, ошибка в постановке опыта.
Изложение. Глава строит свой довод в три шага. Сначала многослойный перцептрон обучается отображению «орбита → углы» на двадцати тысячах примеров и даёт 0,151 мм против 0,028 мм у SVD с тридцатью модами. Это ожидаемо: модельная машина линейна, и для неё SVD является точным решением в смысле наименьших квадратов; превзойти его сеть не может, и такой тест обязателен. Затем вводится нелинейность: суммарный угол в каждом корректоре — ошибка вместе с поправкой — проходит через θmax·tanh(θ/θmax) с θmax = 0,01 мрад. Это модельная нелинейность, а не предел источника питания: сами поправки ничем не ограничены и превышают θmax в несколько раз. Сеть обучается уже не всему отображению, а остатку — разности между истинной ошибкой и ответом SVD, — и шаг коррекции складывает линейное решение с поправкой сети. Наконец, коррекция выполняется итеративно, с новым измерением орбиты после каждого шага. Итерации решают ту же задачу, что и сеть, поэтому их вклады необходимо разделять: сравнение гибрида после трёх итераций с однократным SVD приписало бы сети выигрыш, который принадлежит итерациям. У гибрида существует оптимальное число итераций, и его необходимо измерять. Сеть работает рядом с классическим алгоритмом, а не вместо него: такая декомпозиция обеспечивает предсказуемую деградацию, и при неудачном ответе сети остаётся рабочее линейное решение.
Демонстрация:
коррекция орбиты
01
Функция svd_correction главы
02
Кольцо 64 × 48: 10, 30 и 48 мод
03
Сеть, гибрид, 100 испытаний по 6 итераций
04
Рисунок: спектр и итерации
Что наблюдается
10 и 30 мод и сеть дают числа главы; 48 мод — угол 1,1·10¹² мрад. Гибрид выигрывает 28,2 % на первой итерации и проигрывает с третьей.
root@lab — запись со стенда
Демонстрация. Скрипт orbit.py содержит код главы целиком, в порядке листингов, с тем же генератором случайных чисел и зерном 42. Орбита до коррекции имеет СКО 1,539 мм; десять мод уменьшают её до 0,297 мм при наибольшем угле 0,061 мрад, тридцать — до 0,028 мм при 0,126 мрад, что совпадает с главой до третьего знака. Все сорок восемь мод не улучшают орбиту, 0,029 мм, но требуют угла 1 107 114 103 212 мрад: ранг матрицы равен 41, и семь последних сингулярных чисел, от 6,0·10⁻¹³ до 1,5·10⁻¹⁴, являются шумом округления. Вторая часть обучает две сети и повторяет 100 испытаний по шесть итераций. Сеть на линейной задаче даёт 0,151 мм — снова как в главе. Таблица гибрида по форме повторяет прежнюю таблицу главы, а числами совпадает с исправленной: на первой итерации гибрид лучше чистого SVD на 28,2 %, 0,195 против 0,271 мм, на второй — на 17,2 %, а с третьей проигрывает, и к шестой итерации SVD доводит орбиту до 0,112 мм, а у гибрида орбита медленно растёт до 0,250 мм. Шаг SVD занимает 0,76 мкс, вызов сети — 87 мкс, более чем в сто раз дольше.
Результат:
коррекция орбиты
Что наблюдается
Ранг 41: семь сингулярных чисел на уровне шума округления. Гибрид лучше всего на первой и второй итерациях, 0,195 и 0,192 мм; SVD к шестой — 0,112 мм.
Демонстрация. Слева — спектр сингулярных чисел матрицы отклика в логарифмическом масштабе: от 183,6 до 0,057 у сорок первого, затем обрыв на одиннадцать порядков, к уровню ошибок округления. Пунктиры отмечают отсечение на десяти и тридцати модах. Справа — средняя СКО орбиты по ста испытаниям с разбросом: синие точки — итеративный SVD, монотонно уменьшающий орбиту до 0,112 мм, оранжевые — гибрид, лучший на первой и второй итерациях, 0,195 и 0,192 мм, и затем медленно растущий до 0,250 мм. Разброс между испытаниями сравним с самой разностью методов, поэтому одно среднее без разброса здесь не информативно.
Повторный
запуск
Величина Прежняя глава Стенд
СКО до коррекции 1,539 мм 1,539 мм
30 мод 0,028 мм 0,028 мм
48 мод: угол 1,34·10¹² мрад 1,11·10¹² мрад
сеть 0,151 мм 0,151 мм
до гибрида 0,306 мм 0,308 мм
1-я итерация +27,1 % +28,2 %
лучшая итерация 1-я, 0,191 мм 2-я, 0,192 мм
шаг SVD / сети 1,2 / 110 мкс 0,76 / 87 мкс
Пояснение
Столбец SVD в таблице гибрида зависит только от генератора случайных чисел: 0,306 и 0,308 мм — разные состояния генератора.
Важно
Числа текста порождает напечатанный код: скрипт в CI и свой генератор у каждой функции.
Изложение. Сведём результаты повторного запуска в таблицу и рассмотрим её построчно. Всё, что определяется линейной алгеброй и первыми выборками генератора, совпало до третьего знака: орбита до коррекции, тридцать мод, результат сети на линейной задаче. Угол при сорока восьми модах различается, 1,34·10¹² и 1,11·10¹² мрад: это деление на шум округления, и такое число на другой машине и с другой библиотекой линейной алгебры воспроизводиться не обязано. Таблица гибрида различается уже в первой строке, 0,306 и 0,308 мм. Отметим, что столбец чистого SVD зависит только от случайных выборок: умножения матриц детерминированы. Следовательно, таблица прежней версии главы получена при ином состоянии генератора, чем у напечатанного листинга, — например, после повторного выполнения ячеек тетради; в исправленной главе напечатана таблица, совпадающая со стендом. Отсюда и различие в лучшей итерации гибрида: в прежней версии первая, на стенде вторая; однако разность соседних средних, 0,007 мм в прежней версии и 0,003 мм на стенде, не превышает стандартной ошибки среднего по ста испытаниям, около 0,007 мм, так что оба прогона указывают лишь на первую-вторую итерацию. Форма кривой при этом повторяется: выигрыш на первых шагах, затем проигрыш. Выводов два. Числа в тексте должен порождать тот код, что напечатан: скрипт, запускаемый в CI, обеспечивает их совпадение с кодом. Каждая функция, использующая случайность, должна получать собственный генератор, иначе результат зависит от порядка вызовов.
Установка и двойник
CadQuery: геометрия из описания структуры; ACCUMULATOR: двойник по протоколу EPICS; SCAUT: оркестрация экспериментов и ферма двойников
04
Огибающая
Динамика частиц
Коррекция орбиты
Установка и двойник
ИИ в пультовой
Уроки
Изложение. Четвёртая часть объединяет программы, работающие с установкой как с целым: трёхмерную схему, построенную кодом из описания структуры, цифровой двойник ACCUMULATOR, неотличимый от машины для остальных программ, и библиотеку SCAUT, оркеструющую эксперименты на линаке инжектора СКИФ.
Схема установки
кодом
def build(lattice): assembly, pos, heading = cq.Assembly(), np.zeros(3), 0.0 for i, (kind, length, param) in enumerate(lattice): assembly.add(BUILDERS[kind](length, param), name=f"{kind}{i}", loc=cq.Location(cq.Vector(*pos), cq.Vector(0, 1, 0), np.degrees(heading))) step, turn = local_step(kind, length, param) c, s = np.cos(heading), np.sin(heading) pos = pos + np.array([c * step[0] + s * step[2], 0.0, -s * step[0] + c * step[2]]) heading += turn return assembly, pos, heading
01
Данные
LATTICE — тот же файл, что у расчёта
02
Конструкторы
функция на тип, словарь BUILDERS
03
Порядок
поставить, сдвинуть, довернуть
04
Проверка
поворот 90°, габарит накрывает путь
Для физика
Ошибка оси вращения найдена по габариту: Z 1,83 м при конце траектории 2,002 м.
Изложение. Трёхмерная модель установки обычно создаётся вручную в CAD-редакторе и устаревает при каждом изменении магнитной структуры. Библиотека CadQuery позволяет описать геометрию кодом на Python, и тогда модель хранится в репозитории рядом с расчётом, различия видны в git diff, а сборка чертежа входит в конвейер тестов. Структура задаётся данными: список элементов с длинами и параметрами, в реальном проекте читаемый из того же файла магнитной структуры, по которому работает расчётный код. На каждый тип элемента заведена функция-конструктор, знающая только о своём теле, а словарь BUILDERS заменяет цепочку условий: новый тип элемента добавляется функцией и одной строкой. Функция build на слайде расставляет элементы вдоль траектории, и порядок трёх действий в цикле существен: элемент ставится в текущее положение с текущим направлением, затем сдвигается положение, и лишь после этого доворачивается направление. Проверка геометрии выполняется арифметикой: два поворота по 45° обязаны дать 90°, а габарит сборки — накрыть траекторию. При написании главы магнит строился вращением вокруг неверной оси, изображение выглядело правдоподобно, и ошибку обнаружил именно габарит: 1,83 м по Z при конце траектории на 2,002 м. Идея строить схему ускорителя из файла структуры принадлежит А. В. Петренко (ИЯФ СО РАН).
Результат:
схема установки
Что наблюдается
8 элементов, габарит X 1,32 м и Z 2,10 м, конец траектории X 1,202 м и Z 2,002 м, поворот 90,0°: числа главы получены заново на CadQuery 2.8.
Демонстрация. Код главы запущен без изменений на рабочем компьютере с CadQuery 2.8, а рисунок построен внеэкранным рендером VTK. Слева направо: дрейф, фокусирующий квадруполь со скруглёнными рёбрами, дрейф, поворотный магнит на 45°, дрейф, дефокусирующий квадруполь без скруглений, дрейф и второй поворотный магнит; красная линия — ось пучка, по которой расставлены элементы. Скрипт печатает те же числа, что в главе: восемь элементов, габарит 1,32 м по X и 2,10 м по Z, конец траектории в точке X = 1,202 м, Z = 2,002 м и поворот 90,0°, а проверка assert проходит. Глава воспроизводится полностью, поскольку в ней нет ни случайности, ни внешних данных.
Цифровой
двойник
Клиент
скрипт, CS-Studio, SAVA
EPICS Channel Access
caget, caput по имени PV
ElegantIOC на pcaspy
write, read, масштабы
Elegant
eleput, eleget, файлы SDDS
01
Каналы
из config.lte и таблицы обработчиков
02
116 PV
7 × 8 + 5 × 4 + 5 × 4 + 4 × 2 + 12
03
caput
полный пересчёт Elegant
04
Клиент
не отличает двойник от машины
Для физика
Пучковое время — самый дефицитный ресурс: ошибка скрипта на двойнике обходится перезапуском контейнера.
Изложение. Цифровой двойник представляет собой программу, неотличимую от настоящей машины для всех остальных программ. ACCUMULATOR собран из четырёх частей: физическим ядром служит Elegant, оболочкой — SCAUT, внешним интерфейсом — сервер EPICS Channel Access на библиотеке pcaspy, а тиражирование обеспечивают контейнеры Docker. Двойник работает по тому же протоколу, с теми же именами каналов и в тех же единицах, что и линак, поэтому скрипт коррекции, операторский экран и помощник оператора вызывают caget и caput и не различают, с чем работают. Список каналов нигде не задаётся вручную: он выводится из файла структуры config.lte и таблицы обработчиков, где для каждого типа элемента перечислены параметры, выставляемые наружу, и отмечено, какие из них только для чтения. Для линака из пяти секций получается 116 каналов: семь квадруполей по восемь, пять корректоров по четыре, пять пикапов по четыре, четыре резонатора по два и двенадцать каналов начальных условий пучка; новый квадруполь в структуре добавляет восемь каналов без правки кода. Таким образом, таблица описывает предметную область, а программа её интерпретирует. Каждая запись в канал запускает полный пересчёт Elegant, а чтение монитора извлекает рассчитанные величины из файлов результатов. Программа зарегистрирована, № 2026668488 от 07.07.2026.
Граница
единиц
def write(self, reason, value): element_info = self.element_map[reason] if element_info['read_only']: logger.warning( f"Attempt to write to read-only monitor {reason}") return False sim_name = get_sim_name(element_info['name'], element_info['param']) scale = element_info.get('scale', 1.0) val_to_write = value / scale eleput(sim_name, val_to_write) self.setParam(reason, value) return True
01
Расчёт
K1 — градиент, нормированный на Bρ
02
Пульт
ток источника: k1 = 0,2998·G/p
03
Масштаб
0,1207: K1 = −10,115 → −1,22 А
04
Имена
PV_MAPPING: MG-LA1:QLF1-Iadd:Set
Для физика
На машине тот же канал держит −1,3 А: 6 % при пересчёте по одной точке 6 А ~ 6,3 Тл/м.
Изложение. Рассмотрим, как переводятся единицы. Физика и система управления оперируют разными величинами: в расчёте квадруполь описан параметром K1, градиентом, нормированным на магнитную жёсткость, а на пульте задаётся ток источника питания в амперах. Перевод выполняется по формуле k1 = 0,2998·G/p с градиентом в теслах на метр и импульсом в ГэВ/c, а коэффициент записан в конфигурации вместе с выводом. Для первой секции с импульсом 38 МэВ/c масштаб равен 0,1207, и значение K1 = −10,115 соответствует −1,22 А. В скане, снятом на машине, тот же канал держит −1,3 А, то есть двойник и оборудование расходятся на 6 %; для пересчёта, построенного по одной опорной точке «6 А ~ 6,3 Тл/м», такая точность ожидаема. Здесь же решается задача имён: таблица PV_MAPPING переименовывает канал MG-LA1:QLF1:K1 в MG-LA1:QLF1-Iadd:Set, как источник назван на пульте. Метод write на слайде проверяет право записи, делит значение на масштаб, записывает параметр в Elegant и только после этого обновляет канал. Граница проведена удачно: единицы и имена переводятся в одном месте, на входе сервера, и ни расчёт, ни клиент о них не знают.
Демонстрация:
двойник на caproto
01
Модель устройства на caproto, 127.0.0.1
02
16 каналов из двух секций структуры
03
caget и caput: ток корректора CLX2
04
Пикапы после пересчёта модели
Что наблюдается
QLF1 = −1,22104 А. После caput 0,05 А: PK3 — 0,271 мм, PK4 — 2,223 мм; запись в PK3 отклонена.
root@lab — запись со стенда
Демонстрация. На стенде нет ни Elegant, ни pcaspy, поэтому показывается модель устройства двойника, а не двойник института: сервер на библиотеке caproto, реализующей Channel Access на чистом Python, строит каналы так же, как ACCUMULATOR, — из двух первых секций структуры линака и сокращённой таблицы обработчиков, по два параметра на тип элемента, — но вместо Elegant рассчитывает орбиту в приближении тонких линз. Сервер и клиенты работают только на петлевом интерфейсе 127.0.0.1, а команды caget и caput являются псевдонимами caproto-get и caproto-put. Журнал сервера сообщает о 16 элементах, из которых восемь имеют обработчик, и о 16 каналах. Ток квадруполя QLF1 читается как −1,22104 А: на границе K1 = −10,115 умножен на масштаб 0,1207, как в главе. Запись 0,05 А в корректор CLX2 переводится в угол 0,0004997 рад с масштабом 100,1, модель пересчитывается за 0,06 мс, и пикапы показывают 0,271 мм на PK3 и 2,223 мм на PK4. Попытка записать значение в пикап отклоняется сервером: монитор только для чтения, и в журнале появляется строка об отказе. Клиенту безразлично, что находится за каналом, — модель, двойник или машина, и в этом состоит смысл двойника.
SCAUT:
скан с откатом
try: for step_index, combination in enumerate( all_combinations * repeat): set_motors_values(motor_names, combination, ...) meter_data, meter_errors = get_meters_data(...) data["steps"].append(step_data) finally: for call in callback: call(data) if save_original_motor_values: set_motors_values(original_motor_values.keys(), original_motor_values.values(), ...) if save: save_data(path, data)
01
Интерфейс
что сканируем и в каких пределах
02
Скан
планировщик, обработчики, аварии
03
Оборудование
мотор и датчик вместо прибора
04
Коммуникация
EPICS, Tango или расчётная модель
05
Данные
метаданные и версия кода
Важно
В finally — обработчики, возврат моторов и сохранение: и после ошибки, и после прерывания.
Изложение. SCAUT, Scan Accelerator Utilities, оркеструет эксперименты на линейном ускорителе инжектора ЦКП «СКИФ»: выставить токи, дождаться стабилизации, снять показания десятка датчиков, вычислить результат и повторить, ночью и без человека у пульта. Ключевая идея библиотеки — разделение на слои: процедура сканирования обращается к оборудованию только через абстракцию «мотор» и «датчик», а та — к протоколу только через слой коммуникации, где протокол задают функции чтения и записи. Слой коммуникации позволяет подставить на место EPICS или Tango расчётную модель, и тот же сценарий проверяется сначала на модели, а затем на установке. Процедура скана проходит шесть этапов: инициализацию, планирование, выполнение, обработку событий, сериализацию и завершение. На слайде — сокращённый фрагмент листинга из ВКР магистра: обработчики, возврат моторов к исходным значениям и сохранение данных стоят в блоке finally и выполняются и после ошибки, и после прерывания пользователем; иначе магниты остались бы на токах последней точки скана. Отметим и то, что видно в том же листинге: в ветке обработки ошибки начального чтения используется имя motor_name, не определённое в этой области, — код обработки ошибок тоже необходимо покрывать тестами. Программа зарегистрирована, № 2025619977 от 21.04.2025.
Ферма
двойников
INSTANCE_POOL = queue.Queue() for _p in PREFIXES: # ACC1:, ACC2:, ... ACC89: INSTANCE_POOL.put(_p) def objective(trial: optuna.Trial) -> float: prefix = INSTANCE_POOL.get() # ждём свободный экземпляр try: values = [trial.suggest_float(p, lo, hi) for p, (lo, hi) in zip(actuator_pvs, bounds)] apply_actuators(prefix, actuator_pvs, values, strip=REAL_PREFIX) ... return float(np.mean(step_losses)) finally: INSTANCE_POOL.put(prefix) # возвращаем при любом исходе
01
Скан машины
10 корректоров, 17 каналов, 28 шагов
02
Подгонка
26 параметров, Optuna, 89 экземпляров
03
Коррекция
бета-функции на двойнике, ±0,2 А
04
Перенос
снимок, таблица, dry_run=False
05
Проверка
новый скан машины
Изложение. Подгонка двойника является задачей оптимизации с дорогой целевой функцией: шаг требует десятков запусков Elegant, а сам Elegant однопоточный и обменивается данными через файлы. Поэтому запускается множество его копий: девяносто контейнеров из одного образа с разными переменными окружения на трёх компьютерах, а файл Compose записывает скрипт generate_compose.py с заданным шагом портов. Оркестратор распределяет испытания Optuna через очередь, которая передаёт не данные, а право пользоваться экземпляром: восемьдесят девять префиксов помещаются в пул, блок finally обеспечивает возврат префикса при любом исходе испытания, а девяностый экземпляр остаётся нетронутым для сверки. Цикл, ради которого всё строилось, состоит из пяти шагов. Скан на машине: десять корректоров, семнадцать каналов, 28 шагов. Подгонка двойника: Optuna перебирает 26 параметров — девятнадцать смещений и углов и семь токов квадруполей. Коррекция на подогнанном двойнике: подбор токов в пределах ±0,2 А под номинальные бета-функции. Перенос на машину: функция сначала читает текущие значения, сохраняет снимок, печатает таблицу «было, станет, разница» и пишет только при явном dry_run=False. Наконец, проверочный скан. После подгонки двойник моделирует конкретную машину в конкретный день — в этом и состоит отличие двойника от модели.
Демонстрация:
ферма и невязка
01
generate_compose.py --count 90
02
Порты последнего сервиса
03
Проверка диапазона портов
04
Невязка nanmean без одного пикапа
Что наблюдается
У accelerator-90 порты 94064 и 94065; за пределом 65535 — 29 сервисов, с 62-го по 90-й. Без PK5 nanmean даёт 0,053 вместо 0,302.
root@lab — запись со стенда
Демонстрация. Скрипт generate_compose.py вызывается на девяносто экземпляров одним файлом, как предлагала прежняя версия главы. Оценим заранее, какие порты он назначит. Порты веб-панели идут подряд от 8501, а порты Channel Access — с шагом 1000 от 5064 и 5065, поэтому у девяностого сервиса они равны 5064 + 89·1000 = 94064 и 94065. Проверка ports.py показывает, что за пределом 65535 оказываются порты 29 сервисов, с accelerator-62 по accelerator-90: такой файл Compose не запустится. На практике файлы генерируются по тридцать экземпляров на компьютер, и ошибка не проявляется, но генератор её не предотвращает: проверка диапазона заняла бы одну строку. Скрипт nanloss.py повторяет на условных числах функции read_many и compute_loss тетради подгонки: ошибка чтения превращается в NaN, а невязка считается функцией nanmean. Пока отвечают все пять пикапов, невязка равна 0,302; если пикап PK5 с наибольшим расхождением не ответил, nanmean исключает его, и невязка падает до 0,053 без изменения уставок. Поэтому целевые функции подгонки и коррекции в тетради проверяют NaN до вызова compute_loss и назначают такому испытанию штраф; без этой проверки nanmean применяется только в итоговой сверке подогнанного двойника, где пропавший канал занизил бы невязку.
Двойник:
что улучшить
Сделано хорошо
01
Каналы из структуры
новый элемент — без правки кода
02
Граница единиц
масштабы и имена в конфигурации
03
Перенос на машину
dry_run по умолчанию и снимок
04
Скан
откат моторов в finally
Требует доработки
01
Тесты и CI
в репозитории двойника их нет
02
Окружение
версии не закреплены, Elegant по wget
03
Ошибки чтения
старое значение без тревоги и статуса
04
Конфигурация
правит библиотеку: «FIX in SCAUT!»
05
Порты
генератор не проверяет 65535
Изложение. Сделано хорошо главное: каналы выводятся из структуры, единицы и имена переводятся на одной границе в конфигурации, перенос на машину по умолчанию работает вхолостую и сохраняет снимок, а скан возвращает моторы в блоке finally. Доработки требует инфраструктура. В репозитории двойника нет ни тестов, ни конвейера CI, хотя разбор структуры и таблица обработчиков проверяются модульными тестами без Elegant. Файл requirements.txt не закрепляет версий, а образ скачивает Elegant и SDDS командой wget без проверки контрольных сумм и собирается только для x86_64. Ошибки чтения обрабатываются молча: при неудачном вызове eleget сервер записывает ошибку в журнал и возвращает прежнее значение канала, не выставляя тревогу, и тетрадь подгонки, назначающая испытанию штраф за NaN, получает вместо отказа устаревшее значение. Конфигурация config.py дописывает список параметров в саму библиотеку SCAUT при импорте, с комментарием «FIX in SCAUT!», то есть исправление находится не там, где ошибка. Генератор Compose не проверяет диапазон портов. В EPICS для нестабильного значения предусмотрены статус и тревога канала, и двойник, неотличимый от машины, должен их выставлять.
ИИ в пультовой
SAVA: помощник оператора и конверт записи; AI Scientist: автономный исследователь и проверка его чисел
05
Огибающая
Динамика частиц
Коррекция орбиты
Установка и двойник
ИИ в пультовой
Уроки
Изложение. Пятая часть посвящена двум системам на языковых моделях: помощнику оператора SAVA, который работает рядом с машиной и пишет в оборудование только с подтверждения оператора, и автономному исследователю, который работает без человека в контуре.
SAVA:
устройство
Telegram, веб, CLI
запрос оператора
Консультант
FileSearch, RAG
команды через Kafka
01
Реестр
ToolSpec и AgentSpec — один источник
02
Инструменты
15: десять видны модели
03
Роли
knowledge, observer, operator
04
Состав
33 сервиса: Kafka, PostgreSQL, Redis
Для физика
Сторож порогов отправляет тревогу в обход модели: факт доходит и при недоступном провайдере.
Изложение. Оператор в пультовой работает с тремя несвязанными массивами сведений: тысячами каналов EPICS с именами вида BI-LA1:PK3-fastAvX:Mea, документацией, разбросанной по вики, статьям и регламентам, и журналом смены, который заполняется вручную. SAVA представляет собой разговорный интерфейс, связывающий эти источники в одном окне. Запрос из Telegram, веб-интерфейса или терминала поступает в роутер: языковая модель по истории разговора и списку инструментов выбирает нужный, а роутер рассылает команды агентам через очереди Kafka. Каждый агент выполняет одну задачу и владеет одним хранилищем: контроллер опрашивает и пишет каналы EPICS, консультант ищет по документации гибридным поиском BM25 и векторов и отвечает с указанием источника, секретарь ведёт журнал смены в PostgreSQL. Состав системы объявлен один раз в реестре: ToolSpec описывает инструмент, AgentSpec — сервис, и реестр обеспечивает единый источник, из которого выводятся схемы для модели, маршруты, очереди и цели мониторинга. Инструмент без схемы модели не показывается: график и голос заказывает сама система. Поле min_role задаёт наименьшую роль, которой инструмент доступен. Разговор начинает не только человек: сторож порогов отправляет тревогу оператору в обход языковой модели, и факт доходит даже при недоступном провайдере.
Конверт
записи
@dataclass(frozen=True) class WriteEnvelope: envelope_id: str command_id: str purpose: str # «матрица отклика орбиты» bounds: tuple[WriteBound, ...] max_writes: int ttl_s: float = 1800.0 def check(self, pv: str, value: float) -> None: b = self.bound_for(pv) if b is None: raise PolicyError(f"{pv} не входит в состав …") if value < b.lo or value > b.hi: raise PolicyError(f"{pv}={value:g} вне границ …")
01
Подтверждение
операция целиком, одним «да»
02
Отказ
а не обрезка значения до границы
03
Пределы
лимит записей и срок действия
04
Журнал
каждый вердикт с sha256 политики
Для физика
На двойнике: наибольшее отклонение, 207 мкм на PK5, уменьшено до 77 мкм за три шага.
Изложение. Главное решение SAVA касается записи в оборудование. Флаг writes в реестре делит инструменты на читающие и пишущие. Читающему достаточно проверки политики, а пишущему выдаётся конверт: описание операции, составленное до разговора с оператором и подтверждаемое им целиком, одним «да». В конверте перечислены каналы, границы для каждого, предельное число записей и срок действия. Подтверждается операция, а не каждая запись: коррекция орбиты складывается из десятков caput, и оператор, которому задают вопрос на каждую, перестаёт их читать. Значение за границей приводит к отказу, а не к обрезке: обрезав, процедура сочла бы уставку применённой и построила бы следующие шаги на ложной посылке. Каждый вердикт, включая отказы, попадает в журнал решений вместе с контрольной суммой файла политики, что позволяет через месяц установить, по какой версии политики была разрешена запись. Таким образом, правило «ни одной неподтверждённой записи» становится свойством системы, а не дисциплиной. На цифровом двойнике такой цикл уменьшил наибольшее отклонение орбиты с 207 мкм на пикапе PK5 до 77 мкм за три шага с обратной связью.
Демонстрация:
конверт записи
01
Реестр: инструменты, роли, агенты
02
Конверт: CLX2 в ±0,01 А, две записи
04
Тесты конверта без EPICS и модели
Что наблюдается
15 инструментов: 10 видны модели, 3 пишут. Из шести записей прошли две; отказы — граница, состав, чтение, лимит. Тесты прошли.
root@lab — запись со стенда
Демонстрация. Код SAVA запускается без языковой модели, Kafka и EPICS: реестр и конверт являются обычными классами Python, что позволяет проверять их модульными тестами. Скрипт tools.py печатает реестр: 15 инструментов, из них десять видны модели, а пишут в оборудование три — SetPV, OrbitCorrection и MachineScript, все с ролью operator; агентов в реестре тоже 15. Скрипт envelope.py создаёт политику с пределом ±0,05 А для двух корректоров и конверт операции «матрица отклика орбиты»: только CLX2, границы ±0,01 А и не более двух записей; оборудование подменено словарём. Из шести попыток проходят две: 0,005 и −0,005 А. Запись 0,020 А отклоняется как выходящая за границы конверта, хотя политика её разрешила бы, запись в CLX3 — как не входящая в подтверждённый состав, запись в пикап — политикой, поскольку канал только для чтения, а шестая — по исчерпанному лимиту. В оборудовании остаётся −0,005 А, а журнал решений содержит все шесть вердиктов: allow, deny, deny, deny, allow, deny. Наконец, запускаются модульные тесты конверта из репозитория, и все они проходят: гарантию «отказ, а не обрезка» проверяют тесты, а не модель.
AI Scientist:
цикл без человека
Идея и план
модель по шаблону
Прогон run_i
числа в notes.txt
01
Шаблон
orbit: VEPP-5, 16 мониторов, ±7 А
02
Цикл
до 4 неудач подряд, до 10 прогонов
03
Ошибка
1500 знаков stderr — обратно модели
04
Итог
статья и рецензия: 3 из 10, Reject
Изложение. Автономный исследователь выполняет весь цикл научной работы без человека: формулирует идею, ставит эксперимент, анализирует числа и пишет статью. Основой служит открытая система AI Scientist (Lu et al., 2024), а адаптация к ускорителям свелась к одному шаблону closed_orbit_correction: постановка задачи для модели, рабочий код коррекции орбиты по SVD для модели накопителя ВЭПП-5 с шестнадцатью мониторами и пределом корректоров ±7 А, пять затравочных идей и каркас статьи. Работа делится на три стадии. Сначала модель формулирует идею, проверяет её новизну поиском по литературе и помещает в архив с оценками. Затем редактор aider, управляемый моделью, правит experiment.py, обвязка запускает прогон и возвращает модели числа или последние полторы тысячи знаков потока ошибок; на прогон отводится лимит попыток, а каталог неудачного захода удаляется. Наконец, статья собирается из каркаса LaTeX и рецензируется отдельным проходом модели по форме отзыва NeurIPS. Каждая идея отрабатывается в отдельном каталоге, где рядом с числами сохраняются код и полная переписка с моделью; это обеспечивает проверяемость результата, и именно такая проверка выполняется в демонстрации. Статья о гибридной коррекции SVD и сети — по одной из затравочных идей шаблона, описание эксперимента к которой перед запуском было дополнено вручную, — получила от собственного рецензента 3 из 10 и решение Reject.
Демонстрация:
артефакты прогона
01
Архив идей: затравочные и модели
03
seed в обучении и в тесте
04
Утверждения против чисел
Что наблюдается
Интересность 8–10 у всех десяти идей, у пяти идей модели — 9–10. В каждом прогоне 9 различных seed на 100 расстроек, те же в обучении. Три утверждения и рецензия неверны; статья — $0,85.
root@lab — запись со стенда
Демонстрация. Все файлы взяты из каталога примера статьи в открытом форке. Архив идей содержит десять идей: первые пять — затравочные идеи шаблона с оценками его автора, в том числе гибридная коррекция, взятая затем в работу, остальные пять модель придумала и оценила сама. Интересность — от 8 до 10 у всех, у собственных идей модели — 9 и 10, а новыми признаны девять из десяти, — такие оценки идей не различают. Скрипт runs.py читает final_info.json одиннадцати прогонов: чистый SVD даёт после коррекции 4,84·10⁻⁵ м², три итерации сети — 5,92·10⁻⁶, то есть снижение на 87,8 %, а дальше снижение уменьшается до 27,4 % к десятой итерации. В каждом прогоне сто расстроек, но различных значений seed у них всего девять: команда grep показывает строку randint(1, 10) и в обучении, и в тесте, поэтому тестовые расстройки повторяют картины смещений обучающей выборки в другом масштабе. Скрипт claims.py сверяет утверждения модели с числами: «снижение на порядок по сравнению с run 3» — на самом деле рост на 12 %, «наименьшая начальная невязка» у run 8 и «наименьшая остаточная» у run 9 — обе неверны. Рецензент, отклонивший статью, тоже ошибается: он пишет о десяти тестовых примерах на конфигурацию, а в каждом прогоне их сто. Последняя строка журнала aider относится ко второму сеансу, в котором писалась статья: он стоил 0,85 доллара; стадия эксперимента — первый сеанс того же журнала. Таким образом, каждое число, полученное моделью, необходимо проверять скриптом по сохранённым данным.
ИИ-системы:
что улучшить
SAVA AI Scientist
Сделано хорошо 67 файлов тестов; ruff, mypy и сборка образов в CI; 18 ADR; политика данными с sha256 каталог на прогон: код, числа и переписка рядом; Dockerfile для изоляции прогона
Требует доработки 33 сервиса на одну машину; модульные тесты импортируют код агентов девять seed на обучение и тест; заметки противоречат числам; рецензент ошибается
Урок запись в оборудование — свойство системы число модели проверяет скрипт
Важно
Модель пишет код и выводы быстрее человека, а проверяет их хуже: проверка остаётся на тестах и на исследователе.
Изложение. Две системы противоположны по степени доверия к модели. SAVA является наиболее зрелой программой лекции с точки зрения курса: 67 файлов модульных тестов, конвейер CI с линтером ruff, проверкой типов mypy для общей библиотеки, тестами и сборкой образов, 18 записей архитектурных решений, политика доступа, заданная данными, с контрольной суммой файла в журнале решений. Цена зрелости — сложность: 33 сервиса в конфигурации Compose обслуживают одну машину, и каждый новый компонент необходимо сопровождать; модульные тесты при этом импортируют модули агентов целиком, так что для их запуска требуется почти весь набор зависимостей. У автономного исследователя хорошо устроено хранение: каждый прогон лежит в своём каталоге вместе с кодом и перепиской, а для изоляции кода, написанного моделью, есть Dockerfile. Слабое место — проверка: девять значений seed общие для обучения и теста, заметки модели противоречат числам рядом с ними, рецензент ошибается в фактах. Урок у двух систем общий: модель пишет код и выводы быстрее человека, но проверяет их хуже, поэтому проверка остаётся на тестах, политике и исследователе.
Уроки
Сводка по восьми программам, ошибки, общие для многих из них, и итоговый проект курса
06
Огибающая
Динамика частиц
Коррекция орбиты
Установка и двойник
ИИ в пультовой
Уроки
Изложение. Шестая часть сводит восемь программ в одну таблицу, показывает ошибки, общие для многих из них, и связывает уроки с требованиями итогового проекта курса.
Сводка
по программам
Программа Тесты CI Пакет Типы Воспроизводимость
KENV нет нет нет зависимостей np.arange как тип решатель без проверки
ГА главы нет нет — нет seed не задан
REDPIC функциональные есть лишний tests np.array как тип версии без границ
Орбита против SVD нет — нет seed 42, иной прогон
CadQuery assert нет — нет числа совпали
ACCUMULATOR нет нет Docker нет версии без границ
SAVA 67 файлов есть Compose mypy образы по тегам релиза
AI Scientist нет нет без версий нет каталог на прогон
Изложение. Рассмотрим восемь программ в одной таблице по критериям курса. Проверки в коде есть у четырёх программ: у REDPIC функциональные тесты pytest в CI, код орбиты сравнивает сеть с SVD, схема CadQuery проверяет себя одним assert, и этого хватает, потому что проверка арифметическая, а у SAVA 67 файлов тестов. Конвейер CI есть у REDPIC и SAVA. Упаковка вызывает больше всего замечаний: KENV не объявляет зависимостей, REDPIC кладёт в окружение посторонний пакет tests, у двойника и исследователя версии не закреплены, у SAVA — тоже, но окружение фиксируют образы, публикуемые по тегам релиза. Аннотации типов есть у KENV и REDPIC, но не проверяются: np.arange и np.array являются функциями, а не типами, и mypy отметил бы это сразу; проверку типов выполняет только SAVA. Воспроизводимость лучше всего там, где нет случайности, — у схемы CadQuery числа главы получены заново; у генетического алгоритма зерно не задано вовсе, у орбиты таблица прежней версии главы получена иным прогоном, чем напечатанный листинг, а у KENV результат решателя не проверяется. Таким образом, ни одна программа не выполняет всех требований, а самые серьёзные ошибки найдены там, где проверок меньше всего.
Демонстрация:
пакеты и журнал
01
Что пакеты объявляют о себе
02
Журнал до и после import redpic
03
Чей пакет tests в окружении
04
Тесты SAVA рядом с REDPIC
Что наблюдается
kenv: 0 зависимостей; redpic кладёт tests в site-packages. После import redpic чужое предупреждение — в формате REDPIC; тесты SAVA не находятся.
root@lab — запись со стенда
Демонстрация. Скрипт deps.py читает метаданные установленных пакетов. У kenv 0.3.0.5 объявлено ноль зависимостей и один пакет верхнего уровня, kenv. У redpic 0.11.8 — пять зависимостей, все без верхних границ, и два пакета верхнего уровня: redpic и tests. Для сравнения deap 1.4.4 объявляет numpy и moocore и единственный пакет deap. Следующие две команды показывают, что делает с программой сам импорт библиотеки. Без REDPIC предупреждение печатается в стандартном виде WARNING:root:сообщение, а после import redpic то же предупреждение программы выходит с датой, временем и форматом REDPIC: библиотека при импорте настроила корневой журнал всей программы. Команда import tests находит пакет в site-packages, положенный туда REDPIC. Последствие видно на чужом проекте: тесты SAVA запускаются командой python -m tests.scenarios.test_write_guard, но каталог tests у SAVA не содержит __init__.py, и Python предпочитает обычный пакет tests из site-packages, где модуля scenarios нет. Тесты другого проекта перестают находиться только потому, что в том же окружении установлен REDPIC. Библиотека не должна менять ничего за пределами своего пространства имён: журнал, пакеты верхнего уровня и глобальный генератор случайных чисел принадлежат программе.
Общие
уроки
01
Физика проверяется частным случаем с известным ответом: поворот 90°, SVD на линейной задаче.
02
Результат решателя проверяется: статус, физическая допустимость, NaN и отказы каналов.
03
Окружение закрепляется и проверяется на новых версиях зависимостей в CI.
04
Библиотека не меняет чужого: корневой журнал, пакеты верхнего уровня, общий генератор.
05
Ограничения задаются в алгоритме: коридор полей, пределы токов; запись охраняет конверт.
06
Числа текста порождает код из репозитория, числа модели проверяет скрипт.
Изложение. Ошибки восьми программ разные, но сводятся к шести урокам. Первый: физика проверяется частным случаем с известным ответом — поворотом на 90° в схеме CadQuery, сравнением сети с точным решением SVD на линейной задаче, — и такие проверки находят ошибки, которых не видно на правдоподобном рисунке. Второй: результат решателя и измерения проверяется — статус интегратора, физическая допустимость, NaN и ответ каждого канала, иначе отрицательный радиус или пропавший пикап выглядят нормальным результатом. Третий: окружение закрепляется для воспроизводимой установки и отдельно проверяется на новых версиях зависимостей, чтобы удаление функции в SciPy обнаружил конвейер, а не пользователь. Четвёртый: библиотека не меняет ничего за пределами своего пространства имён. Пятый: ограничения задаются в самом алгоритме — в генераторе особи, в постановке задачи коррекции, — а не проверкой после него; конверт записи SAVA дополняет их отказом на границе с оборудованием. Шестой: числа в тексте книги или статьи должен порождать код из того же репозитория, а числа, полученные языковой моделью, проверяет скрипт по сохранённым данным.
Итоговый
проект
Этап Срок Результат
Формулировка последняя неделя ноября документ: задача, путь решения, этапы
База вторая неделя декабря базовые требования
Расширение зачётная неделя дополнительные требования
Пояснение
Команда из двух-трёх человек, задача связана с физикой, Python; всё — в системе контроля версий.
01
Сборка
без IDE, контейнер приветствуется
02
Тесты
формулы частных случаев и сверка
03
Документация
программного интерфейса и примеры
04
Проверка
пять вопросов лекции к своему коду
Изложение. Уроки лекции прямо переходят в требования итогового проекта. Задачу команда из двух-трёх человек формулирует сама; она должна быть связана с физикой, а основным языком предполагается Python. Работа проходит три этапа: к последней неделе ноября — документ с уточнённой задачей, путём решения на одну-две страницы и таблицей оставшихся этапов, ко второй неделе декабря — базовые требования, к зачётной неделе — частично или полностью дополнительные. Сдаются код со сборочными файлами, собирающийся без участия IDE, покрытие тестами, документация программного интерфейса и примеры, и всё размещается в системе контроля версий, включая документ первого этапа. Разобранные программы позволяют увидеть, во что превращаются эти требования на практике: сборка — это объявленные и закреплённые зависимости, тесты — это проверки формул частных случаев рядом со сверкой с другой программой, примеры — это скрипты, порождающие числа отчёта. Перед сдачей к собственному коду применяются те же пять вопросов, что и к программам лекции.
Итоги
лекции
01
Восемь программ решают реальные задачи, и каждая требует доработки.
02
Ошибки находят простые проверки: формула, вторая программа, повторный запуск.
03
KENV не проверяет результат решателя; у KENV и REDPIC окружение не закреплено.
04
Двойник и SCAUT: границы слоёв и единиц удачны, тестов и закреплённых версий нет.
05
SAVA защищает запись конвертом; числа AI Scientist проверяет человек или скрипт.
Изложение. Восемь разобранных программ решают реальные задачи физики ускорителей, и каждая требует доработки. Ошибки в них находят простые проверки: формула частного случая, сравнение со второй программой, повторный запуск на другом окружении. У KENV результат решателя не проверяется, у обоих пакетов окружение не закреплено, а REDPIC при импорте вмешивается в журнал и пакеты программы и тратит время на поле пучка при нулевом токе. У двойника и SCAUT удачно проведены границы слоёв и единиц, но нет тестов и закреплённых версий. SAVA превращает правило подтверждения записи в свойство системы, а результаты автономного исследователя без проверки скриптом по сохранённым данным использовать нельзя.
Упражнения
и задание
01
CI на новых версиях
задача по расписанию с новейшими версиями
02
Пакет REDPIC
NullHandler, find_packages, any()
03
Элита и зерно
HallOfFame и seed в ГА главы
04
Ревью
пять вопросов к программе своей лаборатории
Пояснение
Итоговый проект: те же пять вопросов к собственному коду перед каждым этапом.
Для физика
Вывод о программе подкрепляется запуском, а не только чтением кода.
Изложение. Упражнения служат самопроверке. Первое — добавить в конвейер CI своего проекта задачу, которая по расписанию устанавливает новейшие версии зависимостей и запускает тесты, и убедиться, что она обнаруживает удалённую функцию раньше пользователей. Второе — исправить в локальной копии REDPIC журнал, упаковку и проверку заряда и повторить демонстрации лекции. Третье — добавить в генетический алгоритм главы объект HallOfFame и задание зерна, запустить его трижды и оценить разброс найденных полей. Четвёртое — разобрать по пяти вопросам лекции программу своей лаборатории или курсовой работы и составить список доработок. В рамках итогового проекта те же пять вопросов применяются к собственному коду перед каждым этапом сдачи.
Источники
2
Федоров В. В. Создание программного комплекса для расчёта динамики частиц во внешних электромагнитных полях : ВКР бакалавра. – НГУ; Создание программного комплекса для оркестрации ускорительных экспериментов : ВКР магистра. – НГУ
3
Свидетельства о регистрации программ для ЭВМ: REDPIC № 2023688768, KENV № 2024611244, SCAUT № 2025619977, ACCUMULATOR № 2026668488
Изложение. Лекции соответствует часть книги курса «Разбор реальных кодов»: главы об уравнении огибающей, генетическом алгоритме, релятивистской разностной схеме, коррекции орбиты, схеме установки кодом, цифровом двойнике, помощнике оператора и автономном исследователе, а также описание итогового проекта. Физика KENV, REDPIC и генетического алгоритма, их архитектура, конвейер CI и ускорение на Numba изложены в ВКР бакалавра автора, а библиотека SCAUT, настройка линака инжектора СКИФ и автономный помощник — в ВКР магистра. Программы REDPIC, KENV, SCAUT и ACCUMULATOR зарегистрированы в Реестре программ для ЭВМ. Исходный код KENV и REDPIC открыт на GitHub, пакеты опубликованы на PyPI. Автономный исследователь построен на системе AI Scientist, описанной в работе Lu et al. 2024 года. Для самостоятельной работы рекомендуется документация DEAP, caproto и CadQuery.
Спасибо за внимание
Вопросы
phys-dev.github.io/soft-dev-book
Изложение. Надпись на последнем слайде — TODO, парная к надписи diff на титуле: результатом критического чтения является список доработок, и с такого списка начинается следующая версия программы — в том числе итогового проекта. Лекция и курс завершаются ответами на вопросы.