25 сентября 2026 года Anthropic опубликовал «Yes, Claude can do Nine Loops».
По сообщению участников проекта, Claude получил символ девятипетлевой шестичастичной MHV-амплитуды двумя способами. Речь идёт о специальной квантовой теории поля — планарной суперсимметричной теории Янга — Миллса. Полная функция была восстановлена отдельно; её второго независимого расчёта пока нет. Разницу между символом и полной функцией разберём в разделах 13–16.
Предыдущий лучший опубликованный результат исследователей для этой шестичастичной амплитуды — восемь петель в работе Лэнса Диксона и Юй-Тин Лю (2023).
Заявленная стоимость каждого из двух способов расчёта — около 1000–2000 долларов, а проверкой результата две недели занимался Лэнс Диксон из SLAC, один из специалистов, развивавших эти методы.
Для кого эта статья
Эта статья для тех, кто хочет понять результат Claude и разобраться, как проверять научный код, написанный с помощью ИИ. Разработчику она даст примеры проверок, студенту и любопытному читателю — последовательный путь от базовых понятий к петлевым расчётам. Текст можно использовать и как материал для ИИ-агента, которому вы поручаете научное программирование.
Я опираюсь на свой опыт преподавания вычислительной математики и выполнения практических проектов. Из потребности связать численные методы с реальными задачами вырос мой курс для МФТИ. А для этой статьи пригодился мой старый диплом бакалавра о магнитоэкситонах в графене — его историю расскажу ниже.
Почему это важно для научного кода
Меня в этой новости интересует не сам физический результат, а организация работы с ИИ. Расчёт выполнен известными методами; новизна — новый девятипетлевой результат и способ построения сложного вычисления с помощью модели. Claude создал научный код и получил результат, прошедший описанные участниками проверки. Это не доказывает отсутствия всех ошибок: в исходной публикации автор прямо говорит, что не знает, сколько ошибок модель совершала в ходе работы, тем более что во многих подобных физических задачах правильный ответ еще не означает безошибочность его вывода.
Ключевой вопрос тут — как отличить научный результат от убедительно оформленного ответа нейросети? Ниже разберём, что именно вычислялось, как это было реализовано с помощью кода и какие проверки позволяют доверять ответу в оговорённых пределах.
Если такие методы помогут исследователям быстрее создавать и проверять программы, они могут облегчить вычислительную часть крупных проектов — от физики частиц до астрофизики и физики твёрдого тела. Но из одного успешного расчёта ещё не следует, что ИИ способен безошибочно обработать любые экспериментальные данные или что исчезли ограничения на строительство новых установок.
Однако, если в будущем исследователи добьются от ИИ этой способности, обложив его правильными алгоритмами и проверками, это произведет революцию в фундаментальных и прикладных областях. Можно будет строить коллайдеры намного больше БАК, потому что критическим ограничением размера ускорителя сейчас является способность научного сообщества обрабатывать большие объемы данных с ускорительной техники. Можно будет значительно расширить программы астрономических и астрофизических исследований — там то как раз объем данных, получаемых с приборов, пока что намного превосходит способности ученых их обработать. Наконец, это всё сулит невиданный прогресс в области физики твердого тела и создания новых материалов.
Объяснения идут от простых моделей к задаче Claude. Формулы, код и контрольные результаты оставлены рядом, чтобы читатель мог повторить отдельный шаг, а не только поверить рассказу. Архив кода статьи.
Личная предыстория
Для меня эта новость тесно связана с личной историей. В 2010 году я защитил в МФТИ бакалаврский диплом «Расчет магнитоэкситонов в графене в магнитном поле с учетом экранировки в приближении хаотических фаз». Руководителем был Юрий Ефремович Лозовик. От этой работы сохранились текст, notebook Mathematica и таблица Excel — вот артефакты моего диплома.
У меня самого сложное отношение к этому всему.
С одной стороны, в определенный момент я принял решение уйти из всей этой науки и поискать что-то, что приносит больше денег.
С другой стороны, переживал, что из-за этого меньше приношу пользы для общества, в меньшей степени реализовал себя в жизни.
Но теперь я понимаю, что появление ИИ сильно переоценивает это всё. Потому что если бы я остался в науке, то многое, что сделал бы за последние 20 лет вместо зарабатывания денег, просто оказалось бы довольно бесполезным. В силу того, что с помощью ИИ мой диплом бакалавра пишется за несколько часов, диплом магистра за 3 дня, а заброшенную диссертацию, думаю можно за недели написать. Многое, чем занимался мой научный руководитель, его коллеги и чем бы занимался я — сейчас гораздо быстрее и притом надежнее делается с помощью нейросетей под контролем человека.
Подозреваю, что если бы я остался в науке, то сейчас бы испытывал фрустрацию: денег не заработал, так еще ИИ и труд моей жизни фактически обесценил.
Уйдя из науки далеко от квантовой электродинамики и физики твердого тела, я думал, что больше мне это все никогда в жизни не пригодится.
Но вот впервые в жизни мой диплом пригодился — написать полезную статью на Хабр!
Во-первых, благодаря работе над его написанием я тогда разобрался в тех вещах, которые описываю здесь, и многие объяснения, что писал в НИРах и в своих черновиках, просто перенес сюда.
Во-вторых, работа из моего диплома — хороший простейший пример подобного рода расчетов, объяснение которого позволяет понять, как это устроено в принципе. Задачу Claude с 9 петлями очень трудно разобрать в деталях не специалисту, а задачу из моего диплома покрутить, промоделировать, проанализировать в принципе может и новичок. На этом примере проще проследить отдельные шаги расчёта.
В моей задаче тоже есть квантовые состояния, взаимодействия, диаграммы и петли. Но весь путь от постановки задачи до чисел помещается в один небольшой Python-скрипт.

Комикс zinoink: оригинал автора на Пикабу. Теплотехнический диплом в комиксе работает по специальности — помогает с огнём. Мой шестнадцать лет набирался опыта на диске и наконец устроился объяснять девять петель на Хабре. Карьерный путь нелинейный, зато без мангала.
При подготовке этой статьи ИИ-агент мне помог написать код, осуществить все проверки, и нарисовать большую часть иллюстраций отсюда. Поэтому та теория и практика работы над научным кодом, которая здесь описывается, была реализована прямо при создании статьи.
При восстановлении расчёта с помощью ИИ обнаружилась ошибка в записи единицы энергии в дипломе: скорость Ферми оказалась в знаменателе вместо числителя. Ниже покажу исходную запись и размерностную проверку. Эта находка сама по себе ещё не означает, что численные результаты диплома неверны.
Вот так новость о девяти петлях привела меня к вопросу о том, как перепроверять научный код и старые результаты, даже если они уже прошли академическую проверку.
С этого и начнём: научимся понимать формулу настолько, чтобы замечать подобные расхождения. Затем разберём нуль, одну и две петли, самостоятельно получим несколько поправок и только после этого вернёмся к Claude.
Общий язык состояний, взаимодействий и диаграмм связывает эти задачи, но их теории, наблюдаемые величины и методы расчёта различаются.
Одна из самых главных идей статьи — процессная декомпозиция как средство контроля кода от ИИ. Сначала нужно описать моделируемое явление и его расчёт через небольшие процессы с контролируемыми входами и выходами, и только затем поручать ИИ реализацию отдельных блоков и проверять их соединения.
Это один из методов повышения надёжности: он работает вместе с аналитическими проверками и контрольными точками, независимыми расчётами и проверкой сходимости.
Описание квантовой физики на языке процессов с помощью картинок существует и называется квантовым пикторализмом. В этом подходе часть алгебраических преобразований можно выражать через диаграммы с точными правилами соединения. Рисунки требуют тех же определений и допущений, что и формулы. Если вам это интересно, можете почитать учебник.
Для работы с учебными скриптами достаточно базового знания Python и установки перечисленных в разделе 17 библиотек; для подробных выводов пригодятся знания производных, интегралов и правила умножения матриц.
Необходимые физические и математические понятия здесь вводятся по ходу дела и сопровождаются примерами с иллюстрациями. Текст можно использовать как последовательный курс или справочник. Его цель не в том, чтобы освоить всю квантовую теорию поля за один вечер. Она в том, чтобы научиться понимать, о чем вся эта теория, и как в ней связаны между собой объяснения, формулы, рисунки, код и что вычисляют физики.
Как читать эту длинную статью.
Сначала — законченный пример работы с ИИ и проверки научного кода.
Разделы 1–8 восстанавливают физику диплома.
9–10 объясняют счёт петель на простых моделях.
11–16 разбирают результат Claude.
В разделе 17 — семь лабораторных опытов, включая намеренную порчу программы и проверку самих тестов. Полные программы убраны под раскрывающиеся блоки, поэтому сначала можно пройти по объяснениям и рисункам.
Так как статья длинная, возможно читать последовательно будет несколько утомительно. Пользуйтесь планом: выберите интересующий раздел, а при незнакомом понятии вернитесь к его определению выше.

Как сделать научный код надежным
Для программиста здесь до боли знакомая задача: нужно перенести старую систему на другой стек, сохранив смысл происходящего.
Но у научного кода есть дополнительные трудности.
Код может запуститься, выдать конечные числа, пройти сравнение со старой программой — и всё равно реализовывать неподходящую физическую модель. Поэтому обещание «получить код без ошибок» лучше заменить проверяемой целью: перечислить допущения, назвать источники контрольных значений и показать, какие ошибки обнаруживает каждая проверка.
Алгоритм контроля надежности сгенерированного научного кода
Этот алгоритм еще не идеален. Он не гарантирует полного отсутствия ошибок, но делает многие из них обнаружимыми и создает воспроизводимые проверки.

Номера соответствуют шагам ниже. Эталонные ответы получают отдельно от проверяемой реализации. После сбоя выясняют его причину: меняют ошибочный блок, постановку или доказанно неверный тест. Подгонка допуска под полученное число проверкой не становится.
-
Сначала описание задачи.
Записать уравнение, область применимости, единицы, используемый базис из функций, начальные и граничные условия, искомую величину и допустимую погрешность. Отдельно выписать соглашения о знаках и о нормировках. Попросить ИИ перечислить неоднозначности еще до написания программы.
-
Нарисовать процесс.
Разделить расчёт на блоки с понятными входами и выходами: параметры → оператор → решение → наблюдаемая величина. Указать на всех соединениях между блоками используемые размерности, форму массивов и смысл индексов.
-
Получить опоры независимо от большого кода.
Вывести простой предел, решить маленькую задачу вручную, проверить размерности, симметрии и знаки. Написать короткую прозрачную эталонную реализацию; оптимизацию делать отдельно. Уже сейчас зафиксировать отложенные контрольные случаи, которые не будут использоваться для выбора формулы и настройки реализации.
-
Проверять и числа, и символы.
Компьютерная алгебра проверяет преобразования формул с явно заданными предположениями. Если расчёт использует символ полилогарифма, дополнительно проверять алфавит, вес, интегрируемость и ограничения на символ. Затем отдельно восстанавливать потерянные константы и выбор ветвей функции. Совпавший символ не равен доказательству совпадения полных функций, но это совпадение является необходимым признаком верности проделанных вычислений.
-
Испытывать код на прочность.
Менять масштаб, знак, базис и порядок входных данных там, где теория предсказывает результат изменения. Использовать точные аналитические решения и независимые от применяемого вычислительные методы. Очень важно то, что совпадение ответов двух разных чатов с LLM, даже если это разные LLM, независимым методом не считается и по надежности сильно уступает другим видам проверок.
-
Проводить эксперименты по устойчивости вычислений.
Раздельно уменьшать шаг сетки, расширять базис, ужесточать квадратуры и повышать разрядность используемых чисел. При этом везде смотреть на сходимость и невязку уравнения. Если вычисления неустойчивы, то доверять им не следует.
-
Проверять не только научный код, но и сами проверки этого кода.
Внести известные дефекты и убедиться, что подходящие тесты их ловят. Перепроверить сами эталонные ответы, области применимости и допуски. Одних зелёных тестов недостаточно: для важных свойств стоит показать, какой известный дефект обнаруживает проверка. Это конечный проверяемый опыт, а не требование бесконечно строить тесты для тестов.
-
Применить отложенные контрольные случаи.
Проверить заранее выбранные обычные и предельные случаи. Если результаты этого набора использовали для исправления или выбора кандидата, он уже стал частью разработки. Его стоит сохранить для регрессионных проверок — чтобы старые ошибки не вернулись, — а для новой независимой оценки подготовить другие случаи.
Случайные примеры дополняют, но не заменяют известные ответы.
-
Сохранить воспроизводимый след.
Версии среды, исходные данные, команды, параметры, результаты и ограничения вывода. Перед публикацией выполнить инструкции в чистом окружении и передать существенные физические предположения на предметную проверку человеку. ИИ может помочь быстро выписать все эти предположения, но за ним тут нужен контроль.
Для проверки исследовательской гипотезы можно заранее зафиксировать постановку, критерии оценки и возможные исходы вычислительного эксперимента. Такая предрегистрация помогает отделить исходный план от решений, принятых после просмотра результата; сама по себе она не доказывает правильность расчёта.
Часть этих ступеней реализована ниже: аналитические ориентиры, несколько независимых способов вычисления, размерности, изменение отсечения, намеренные дефекты, контроль порядка сходимости. Экспериментальной проверки модели графена и независимой экспертизы всей новой статьи все эти запуски не заменяют, разумеется. Слова «проверено» всегда должны сопровождаться ответом: чем именно и в каких пределах?
Словарь базовых терминов с примерами
Базис: как хранить одно и то же состояние
В конечномерном линейном пространстве базис — это набор линейно независимых векторов, через который каждый вектор пространства единственным образом выражается линейной комбинацией.
Линейная комбинация — это сумма векторов, умноженных на числа; эти числа — координаты в выбранном базисе. Независимость означает, что ни один базисный вектор нельзя собрать из остальных. Для пространств функций дополнительно нужно оговорить смысл сходимости бесконечных разложений.
Для повторения основ линейной алгебры могу порекомендовать этот мой экспресс-курс.
Вектор в базисе
,
равен
.
В повернутом ортонормированном базисе ,
тот же вектор записывается как
. Изменились координаты, но не сам объект.

*Зелёная стрелка на обеих панелях одна и та же. В квантовом расчёте вместо двух стрелок могут быть функции: состояния на подрешётках A и B или состояния уровней Ландау. *
Начальное условие: с какого состояния начинаем
Начальные условия задают необходимые данные в выбранный начальный момент времени. Для уравнения первого порядка по времени это само состояние системы; для дифференциальных уравнений второго порядка обычно нужны ещё и скорости. Количество данных зависит от уравнения.
Уравнение имеет семейство решений
.
Начальное условие выбирает
.
Одна лишь правая часть return -y не говорит решателю, какую из кривых строить.

На всех кривых действует один закон изменения. Их различие задаётся значением . Единицы времени выбраны так, что коэффициент затухания равен единице.
Граничное условие: что разрешено на краю области
Граничное условие ограничивает решение на пространственной границе.
Условие Дирихле задаёт значение функции, условие Неймана — её производную по нормали к границе; физический поток может быть пропорционален этой производной с дополнительным коэффициентом.
Периодические условия соединяют противоположные края.
Пусть на
.
Двойное интегрирование даёт .
При получаем
.
При ,
— уже
.
Поэтому менять края массива без изменения постановки нельзя.

Слева закреплено значение в обоих случаях. Справа в одном случае закреплено значение, в другом — нулевой наклон. Это две разных задачи с разным ответом.
Нормировка: фиксируем масштаб
Нормировка в квантовой механике — это условие, фиксирующее масштаб объекта.
Для волновой функции одного чистого состояния оно обычно имеет вид , где
— мера интегрирования.
На отрезке функция
имеет квадрат нормы
.
Поэтому нормированная амплитуда , а плотность вероятности
. Амплитуду делят на
, плотность — на
.

Площадь под нормированной плотностью равна единице.
Допуск: какое расхождение принимаем
Допуск — выбранный до просмотра результата критерий приемлемого отклонения.
Часто сравнивают вычисленное с опорным
по правилу
Абсолютный допуск имеет единицы сравниваемой величины, относительный
безразмерен. При
около нуля нужен абсолютный масштаб.
При ,
и
порог равен
. Число
проходит тест,
— нет.

Излом показывает смену доминирующего масштаба.
Невязка: подставляем кандидат обратно в уравнение
Для уравнения и кандидата
определим невязку как
. Иногда используют противоположный знак; соглашение надо назвать.
Ошибка решения — , где
— точный ответ.
Невязку можно посчитать без знания , а ошибку — не всегда.
Для пружины с
Н/м и
Н точное смещение равно 2 м.
Если программа вернула 2.01 м, ошибка равна 0.01 м, невязка — 0.03 Н.
Здесь , но это величины разных размерностей.

Горизонтальное отклонение относится к смещению, вертикальное — к равновесию сил.
Мантисса и точность представления
Число с плавающей точкой хранится как знак, значащая часть и степень основания:
.
Значащую часть часто называют мантиссой. Число её разрядов ограничивает относительную точность представления чисел данного формата.
В учебном примере с десятичными числами с четырьмя значащими цифрами округлится до
. Абсолютная ошибка —
, относительная — около
.
Эта потеря при представлении одного числа появляется еще до решения уравнений.

Порядок числа: диапазон отдельно от точности
Порядок — показатель степени основания
.
Записи ,
и
имеют одну мантиссу и разные порядки.
Диапазон порядков определяет, насколько большие и маленькие масштабы можно хранить; он не добавляет значащих цифр.

Поэтому «64 бита» не означает 64 надёжные десятичные цифры. Кроме хранения числа есть накопление округлений и чувствительность самой задачи.
При сложении чисел разных порядков значащие части выравниваются, а результат округляется до точности формата. Поэтому достаточно малая добавка может потеряться. Это следует отличать от потери относительной точности при вычитании близких приближённых чисел.
Численная устойчивость
Численная устойчивость описывает реакцию алгоритма на малые вычислительные возмущения. Еще одно полезное понятие тут — обратная устойчивость. Она в том, что полученный ответ является точным решением задачи с достаточно мало изменёнными входными данными.
Рассмотрим при малом положительном
.
При прямом вычислении округляется сумма , а затем из близкого к единице корня вычитается 1. Ошибки округления становятся велики относительно малого результата — возникает потеря значащих цифр при вычитании.
Умножим числитель и знаменатель на сопряжённое выражение:
При двойной
float в нашем запуске дал для первой записи нуль, для второй — .
Здесь сохранить точность нам помогло само изменение способа вычислений.

На левой логарифмической оси нулевой результат показан на условном нижнем уровне. Справа — относительное расхождение со второй записью.
Обусловленность: насколько чувствительна сама задача
Обусловленность характеризует изменение точного ответа при изменении входа; она не является свойством алгоритма. Локальное число относительной обусловленности скалярной функции при ненулевых можно записать как
.
Если вычитаются измеренные и
, разность равна
.
Изменение первого числа до — примерно одна миллионная его величины — удваивает ответ.

Здесь числа сравниваются в точной десятичной арифметике.
Бюджет ошибок: что ещё осталось неизвестным
Бюджетом ошибок называют список источников ошибок вместе с оценками, единицами, предположениями и способами уменьшения. Он включает в себя неопределённость параметров, дискретизацию, отсечение суммы, квадратуру, округление и ограничения модели.
Дискретизация заменяет непрерывную задачу конечной, например сеткой.
Отсечение заменяет бесконечную сумму или область конечной.
Квадратура — численный способ вычислить интеграл.
Ошибка модели возникает, когда уравнение опускает существенный физический эффект.

*В сумме по уровням Ландау отдельно друг от друга для контроля изменяют последний уровень, точность интеграла и разрядность. *
Почему неопределённости иногда складываются по квадратам
Различим ясно друг от друга погрешность и неопределённость.
Погрешность — отклонение от опорного значения; неопределённость характеризует неполноту знания о величине.
Стандартную неопределённость представляют масштабом стандартного отклонения в принятой вероятностной модели. Важно, что неизвестную ошибку программы нельзя объявить случайной только потому, что мы не знаем её знак.
Пусть суммарная погрешность состоит из случайных вкладов
.
Обозначим их средние как , центрированные отклонения — как
.
Символ означает математическое ожидание — среднее с учётом вероятностей. Его можно понимать как среднее арифметическое от всех значений случайной величины при очень большом количестве повторений опыта.
Дисперсия — это средний квадрат отклонения от среднего:
Ковариация является смешанным объектом:
.
Для независимых величин с конечными вторыми моментами ковариация равна нулю.
Обратное утверждение «некоррелированность означает независимость» в общем случае неверно. При нулевой ковариации получаем
Это и называют тем самым «сложением ошибок по квадратам»: складывают дисперсии, а затем извлекают квадратный корень, возвращаясь к исходным единицам.
Для независимых вкладов со стандартными отклонениями 0.03 и 0.04 в одних единицах ответ равен 0.05. Их можно понимать также и как характерный масштаб разброса.
Общий случайный источник может сделать вклады коррелированными.
Если и
для одной величины
с нулевым средним и единичной дисперсией, суммарное стандартное отклонение равно 0.07.

На рисунке 200 тысяч случайных пар. Точные значения 0.05 и 0.07 выведены выше.
Для сначала линейно приближают изменение:
,
где — коэффициенты чувствительности в опорной точке.
Тогда
В линейных моделях эта формула является точной. А в нелинейных еще нужно обосновать достаточность первого порядка. Подробнее можно почитать в руководстве NIST.
Рассмотрим другую ситуацию. Пусть нам известны абсолютные ограничения на значения величин
,
.
Без вероятностных предположений надежной границей можно считать лишь саму сумму
.
Например, если в ошибках смещение постоянное, то оно не исчезает при повторении опыта.
Ошибки дискретизации и отсечения часто являются детерминированными и могут быть связаны. Без обоснованной вероятностной модели складывать их по квадратам нельзя. Если для отдельных вкладов известны абсолютные границы, их сумма даёт консервативную границу общей ошибки.
Не все проверки одинаково важны. Они разные по значимости и выполняют разные функции.
Запуск отвечает на вопрос «выполнилась ли программа».
Тест с точным ответом проверяет конкретный случай.
Сходимость проверяет приближение к пределу в выбранном режиме.
Верификация — правильно ли решена математическая задача.
Валидация — подходит ли модель для описания данного явления и поставленной цели.
Точное решение уравнения без трения не подтверждает его пригодность для маятника с сильным затуханием.

Этот подход вырос не только из моего диплома
Здесь в статье соединены несколько разных задач: разработка научного кода, проверка правильности используемых численных методов, проверка проверок (тесты для тестов) и само тестирование кода, сгенерированного большими языковыми моделями. У них разная доказательная сила. Рекомендации сообществ, систематические обзоры, эксперименты на бенчмарках и свежие препринты отвечают на разные вопросы.
Ниже приведен тематический обзор с ответом на вопрос «что именно берём в наш алгоритм».
1. Воспроизводимый расчёт начинается с организации работы. Wilson и соавторы, Best Practices for Scientific Computing (2014) рекомендуют небольшие проверяемые изменения, контроль версий, автоматические проверки и просмотр кода коллегами. Good Enough Practices in Scientific Computing (2017) переводит эти идеи в практические рекомендации, как именно правильно организовать научную работу. У нас из них следуют неизменяемый архив диплома и отдельные новые скрипты, которые тестируются независимо друг от друга.
2. У научного кода может не быть готового ответа для сравнения. Тут Kanewala и Bieman, Testing Scientific Software: A Systematic Literature Review рассматривают эту трудность вместе с прочими особенностями научного программирования. «Оракул теста» — источник решения, который дает ответ на вопрос, считать ли результат правильным. Оракулом теста может быть формула, независимый расчёт, сохранённый эталон или какое-то проверяемое другим способом свойство (например, экспериментально). Когда полного оракула нет, один удачный запуск особенно мало говорит. Более поздняя работа Testing Research Software: A Survey (2022) также подчёркивает сложность предметной проверки и необходимость выделять на неё реальные усилия. Для нас вывод — отдельно записывать происхождение каждого ожидаемого числа.
3. Декомпозиция должна помогать менять реализацию без смены физического смысла. Классическая работа Parnas, On the Criteria To Be Used in Decomposing Systems into Modules (1972) обсуждает критерии выделения программных блоков и сокрытие проектных решений за интерфейсом. Простое разрезание программы по порядку выполнения ещё не обеспечивает хорошую модульность. В нашем варианте, например, блок «интеграл» должен иметь проверки по входам, выходам и точности: квадратуру можно заменить другой, не изменяя физический смысл происходящего.
4. Предсказуемые связи между ответами дополняют эталонные числа. Обзор Segura и соавторов (2016) посвящён метаморфному тестированию. Его суть заключается в том, чтобы преобразовать вход так, чтобы теория предсказывала изменение выхода. В нашем примере ниже для уровней графена уменьшение вчетверо должно уменьшить ненулевую энергию вдвое при фиксированных остальных параметрах. Но неверный общий множитель пройдёт этот тест, так что он является необходимым, но никак не достаточным.
5. Известное решение можно сконструировать самому. Salari и Knupp, Sandia (2000) описывают метод заранее изготовленных решений: выбрать гладкую функцию, вычислить для неё правую часть и проверить решатель. Метод позволяет выявить неверные коэффициенты и порядок сходимости. Однако он не доказывает, что исходная физическая модель соответствует эксперименту. Ниже это тоже реализуется для проверки правильности работы кода.
6. Тесты испытывают известными дефектами. Здесь Jia и Harman, An Analysis and Survey of the Development of Mutation Testing (2011) рассматривают мутационное тестирование: небольшие изменения программы показывают, какие отклонения тестовый набор способен обнаружить. При этом очень важно отличать реальный дефект от преобразования, не изменяющего поведение, и объяснять необнаруженные изменения. Наши пять мутаций ниже подобраны по физическому смыслу.
7. Для кода от ИИ качество тестов меняет оценку качества модели. В EvalPlus (2023) расширение тестовых наборов обнаруживало ошибки, пропущенные исходными проверками сгенерированных решений. Это исследование функциональной корректности программ на выбранном бенчмарке. Его урок переносится в эту статью в точности: тестируем границы, особые случаи и свойства результата.
8. Научные бенчмарки тоже нуждаются в предметном аудите. SciCode (2024) использует исследовательские задачи, составленные учёными. Авторы препринта SciCode-Verified (2026) сообщают об ошибках эталонов, неоднозначных требованиях и неподходящих допусках в исходной оценке. Любое исправление проверяющего должно иметь отдельное математическое обоснование и соответствующую запись в журнале изменений.
9. Автоматическая проверка методологии — полезный дополнительный слой. В препринте scicode-lint (2026) исследуется поиск методологических дефектов научного Python-кода с помощью шаблонов, подготовленных языковыми моделями. Среди примеров — утечка данных при предобработке и ошибки организации машинного обучения. Авторы отдельно оценивают контролируемые примеры и реальные материалы; качество зависит от набора и категории дефектов. Такой инструмент уместен как помощник ревью, но сам по себе не проверяет физику и не устраняет ложные срабатывания.
10. Воспроизводимость кода и воспроизводимость диалога с ИИ — разные задачи. В редакционной статье Nature Computational Science, Reproducibility in the Era of Large Language Models (2026) подчёркивается потребность в более ясных стандартах отчётности и общих практиках воспроизводимости. В нашем алгоритме эту общую задачу реализуем фиксацией модели и версии, запросов, настроек и программной среды. Эти сведения дополняем окончательным кодом, данными и командами запуска. История получения кода помогает понять происхождение решений. Из этого обзора я беру не обещание «ИИ больше не ошибётся», а конструкцию работы:
явная постановка → небольшие заменяемые блоки → независимо обоснованные проверки → проверка самих тестов → воспроизводимый пакет.
Процессная диаграмма позволяет все это изобразить наглядно.
Ошибка в дипломе, пережившая защиту
В разделе 9 моего диплома, на странице 23, единица энергии записана как ħ/v_Fl. Здесь — магнитная длина; ниже обозначим её
, как в остальной статье.
Если понимать запись как , получается размерность массы. Правильный энергетический масштаб для рассматриваемого гамильтониана —
.

Проверка помещается в одну строку: ,
,
.
Поэтому деление на
оставляет килограммы, а умножение на
даёт джоули. Если иначе расставить скобки в исходной записи и прочитать её как
, размерность энергии тоже не получится.
Ошибка осталась в тексте защищённого диплома, а восстановление расчёта с помощью ИИ помогло её заметить.
Чтобы контролировать подобные ошибки, формулы необходимо превратить в явные определения, единицы и исполняемые проверки.
Результат можно предъявить читателю вместе с коротким обоснованием. А только лишь по сохранившемуся файлу нельзя точно установить, что именно проверял каждый преподаватель и обсуждалась ли эта запись на защите диплома.
Важно точно назвать находку: доказана ошибка в записи единицы энергии.
Она могла быть опечаткой и сама по себе не доказывает, что все численные результаты диплома неверны. После обнаружения ошибки размерностную проверку можно закрепить в коде. Запишем размерность тройкой степеней при килограмме, метре и секунде. При умножении тройки складываются, при делении — вычитаются:
import numpy as np hbar_dim = np.array([1, 2, -1]) # kg m^2 s^-1 v_dim = np.array([0, 1, -1]) # m s^-1 l_dim = np.array([0, 1, 0]) # m energy_dim = np.array([1, 2, -2]) written = hbar_dim - v_dim - l_dim corrected = hbar_dim + v_dim - l_dim assert not np.array_equal(written, energy_dim) assert np.array_equal(corrected, energy_dim) print(written, corrected) # [1 0 0] и [1 2 -2]
Этот тест фиксирует уже разобранную ошибку. Сильная сторона такого подхода — в связи между исходной записью, физическим смыслом и воспроизводимой проверкой.
Что именно передавать ИИ
Начинать нужно с маленькой спецификации: формула, единицы, допустимые аргументы, соглашения о знаках, ожидаемые пределы. Затем просить отдельную вычислительную функцию. Графики и большую таблицу результатов имеет смысл строить после её проверки.
Для простейшего состояния радиальная часть волновой функции равна
Здесь — безразмерный радиус. Сейчас достаточно геометрии: тонкое кольцо имеет площадь
. Значит, радиальная плотность вероятности
Ожидаемые значения можно вывести до написания программы. Замена даёт
, поэтому
Ниже пример задания для повторения опыта.
Реализовать на Python функцию f00 ® по приведённой формуле. Радиус должен быть безразмерным и неотрицательным. Функция должна принимать число или массив NumPy. Отдельно вычислить численной квадратурой нормировку с мерой 2πr dr и средний квадрат радиуса. Сравнить с аналитически выведенными значениями 1 и 2. Не менять формулу и не перенормировать результат ради прохождения проверок. При расхождении вывести рассчитанное значение и оценить, какой именно шаг требует проверки.
Саму проверку можно прочитать целиком:
import numpy as np from scipy.integrate import quad def f00(r): if np.iscomplexobj(r): raise ValueError("r must be real") r = np.asarray(r, dtype=float) if np.any(~np.isfinite(r)) or np.any(r < 0): raise ValueError("r must be finite and nonnegative") return np.exp(-r*r/4) / np.sqrt(2*np.pi) def rho(r): return 2*np.pi*r*abs(f00(r))**2 norm = quad(rho, 0, np.inf, epsabs=1e-11, epsrel=1e-11)[0] r2 = quad(lambda r: r*r*rho(r), 0, np.inf, epsabs=1e-11, epsrel=1e-11)[0] assert abs(norm-1) < 1e-9 assert abs(r2-2) < 1e-9 print(norm, r2) # оба интеграла вычисляются, а не подставляются из ответа
Проверки тоже можно поручать ИИ, как это делалось при подготовке материала.
Но источник ожидаемого ответа должен быть понятен отдельно от проверяемой функции.
В этом примере есть короткий аналитический вывод и численная квадратура. Скопировать формулу из реализации в тест и получить совпадение было бы гораздо менее информативно. Согласие двух моделей ИИ тоже не заменяет такого обоснования, так как они могут повторить одну и ту же предпосылку.
Проверяем не только программу, но и проверки
Выше из аналитически посчитанного интеграла удалось получить два требования к радиальной плотности : нормировка равна 1, средний квадрат радиуса равен 2. Эти числа выведены до проверки кода.
Теперь намеренно изменим ширину и одновременно сохраним нормировку:
Откуда последний результат?
Замена даёт
:
первый интеграл превращается в ,
второй — в .
Кривая уже другая, но тест одной нормировки останется зелёным.
Проверка среднего квадрата радиуса обнаружит подмену.

Процедура состоит из пяти действий:
сохранить контрольную реализацию;
внести один понятный дефект в отдельную копию;
запустить тот же набор тестов;
объяснить, какой тест и почему должен сработать;
сопоставить ожидание с фактическим результатом.
Если дефект не замечен, нужно провести более чувствительный тест.
Мутационный тест не доказывает отсутствие других ошибок сам по себе.
В нашем опыте различие доказывается явными формулами.
Теперь расширим опыт до потери якобиана, неверного масштаба энергии, знака экранирования и контроля отсечения. Такие намеренные изменения называют мутациями кода; у каждого дефекта будет своё обоснование.
Здесь известна природа каждой модификации, поэтому можно проверить, замечает ли её выбранный тест.
Исходный файл 06_graphene.py остаётся неизменным: эксперимент создаёт отдельные варианты функций.

На верхней панели потерянный множитель и неверная ширина дают гладкие кривые.
На средней панели ошибочная линейная зависимость энергии от поля специально совпадает с правильной в одной точке, при 10 Тл.
На нижней панели и , и ошибочное
конечны на выбранном отрезке
.
Само отсутствие NaN здесь ничего не разоблачает.
NaN означает not a number: специальное машинное значение для неопределённого численного результата, например при некоторых недопустимых операциях.
Его отсутствие не проверяет физику формулы.
Для рисунка с полем используется такая неправильная формула: . У неё даже правильная размерность.
Но уменьшение поля с 10 до 2.5 Тл уменьшает энергию вчетверо, тогда как исходная модель требует уменьшения вдвое.
Проверка связи между двумя запусками обнаруживает то, что пропускает одна контрольная точка. Полный результат запуска 12_verify_ai.py выглядит так:

В каждой строке меняется только один указанный аспект относительно контрольной версии. Зелёная ячейка означает выполнение конкретного условия, красная — найденное отклонение. Набор обнаружил все пять введённых модификаций, однако отдельные столбцы пропускают почти все чужие ошибки. Число «пять из пяти» описывает этот специально построенный опыт; оценивать по нему надёжность ИИ на других задачах нельзя.
Якобиан — множитель изменения меры при замене координат. Без якобиана интеграл нормировки равен примерно 1.253314137 вместо верного значения 1. При неправильной, но перенормированной ширине нормировка остаётся единицей, зато средний квадрат радиуса становится 1 вместо 2. Наконец, замена знаменателя RPA на даёт при
усиление в 5 раз вместо ослабления до
.
Последняя строка матрицы устроена иначе. В ней проверяется требование к усечению суммы. При увеличение границы с 15 до 30 меняет ответ примерно на 2.027%, если делить разность на модуль результата при 30. Это не проходит выбранный порог 1%.
Переход с 60 до 120 меняет ответ примерно на 0.981% и проходит.
Здесь не доказана однопроцентная точность относительно бесконечной суммы: проверено только изменение при одном удвоении границы и в одной точке. Совпадение с несколькими старыми числами или зелёная ячейка не отменяют проверки всего нужного диапазона.
Когда малая невязка ничего не обещает о точности ответа
Даже правильно составленное уравнение может быть чувствительным.
Возьмём, например, безразмерную систему :
Точное решение здесь: .
Кандидат имеет ошибку нормы 1. В этом примере и на рисунке используем соглашение
, противоположное знаку из начального словаря:
, а его норма всего
. Выбор знака не меняет норму и приведённую ниже оценку ошибки.
Второе уравнение слишком слабо реагирует на ошибку во второй компоненте.

Из следует
.
Для обратимой матрицы, ненулевых и согласованных норм:
Здесь норма измеряет размер вектора; на рисунке она евклидова, то есть квадратный корень из суммы квадратов компонент. Соответствующая норма матрицы — максимальный коэффициент растяжения вектора.
Число обусловленности характеризует чувствительность задачи; в примере оно равно
. Малую невязку нужно оценивать вместе с ним и с масштабами величин.
Для физически разных единиц компонент сначала выбирают осмысленное масштабирование. Связь обусловленности и ошибок обсуждается в руководстве LAPACK. Построенный нами контрпример можно непосредственно проверить переумножением матриц.
Один реальный сбой, которого не видно в формулах
При проверке этой статьи обнаружилось несоответствие между инструкцией и сгенерированной программой: читателю предлагалось выполнить опыт с --order 40, а 10_laboratory.py разрешал максимум 30. Математическое объяснение ряда могло быть правильным, но команда завершалась до вычислений. В приведённой ниже версии этот конфликт устранён: для опыта с геометрической цепочкой порядок 40 допустим.
Это отдельный вид проверки: исполнить инструкцию целиком, шаг за шагом.
Такой вид проверки дополняет тесты формул, аргументов и численной устойчивости. Его может осуществить и другой ИИ, но лучше проделать и человеку тоже.
Как читать проверки дальше
Сначала разбираем физический смысл каждого блока и выводим формулы.
Затем проверяем маленькие реализации, их соединения и численные приближения.
В конце соберём эту работу в процессную схему и выполним семь экспериментов.
При переносе своего расчёта необходимо заполнять таблицу: утверждение → проверка → результат → оставшееся ограничение.
1. Что я пытался посчитать в дипломе

Графен — это один слой атомов углерода с сотовой структурой. Нас будут интересовать электроны, которые способны свободно передвигаться по этому слою.

Почему «соты»? У каждого атома три ближайших соседа, а связи образуют шестиугольники. Цвета обозначают два типа положения в решётке — A и B, а не два химических элемента: в обоих случаях это углерод.
Удобно взять треугольную решётку переносов и базис из двух атомов.
Заштрихованный параллелограмм — одна примитивная ячейка, а расстояние между ближайшими атомами около 0.142 нм.
Решётка Браве состоит из точек , где
— целые числа. Перенос на любой такой вектор воспроизводит весь набор точек.
Примитивная ячейка — это участок решетки, копии которого этими переносами заполняют всю плоскость. Чтобы получить атомные соты, к каждой точке добавляем два положения и
. Эта пара называется атомным базисом.
Состояние, оператор и уравнение Шрёдингера
Волновая функция — это представление квантового состояния в выбранном базисе.
В координатном представлении одночастичной модели это комплексная функция ; в двухмерном пространстве состояний — столбец из двух комплексных амплитуд. Буквой
в этом примере обозначим время.
Модуль в квадрате превращает амплитуду в вероятность: для дискретного ортонормированного базиса , для координаты
.
Фаза комплексной амплитуды нужна для расчета интерференции: вклады сначала складываются как комплексные числа, а потом берётся квадрат модуля.
Поэтому состояния и
имеют одинаковые вероятности A/B, но различаются при измерениях в другом базисе.
Линейный оператор переводит состояние в другое выражение и сохраняет линейные комбинации: .
В конечном базисе это матрица. В пространстве функций это может быть дифференцирование: например, импульс при надлежащей области определения.
Наблюдаемая — физическая величина, для которой модель задаёт правила измерения. В рассматриваемой стандартной схеме ей соответствует самосопряжённый оператор. Для конечной матрицы это означает : транспонирование вместе с комплексным сопряжением не меняет матрицу. Для дифференциального оператора важна также область определения, включая граничные условия.
Если , то
— возможный результат идеального измерения, а вероятность для нормированного состояния
равна
в невырожденном дискретном случае. При вырождении суммируют вероятности внутри соответствующего подпространства. Среднее
относится к статистике повторений, а не обязательно к одному исходу. Например, оператор с исходами
и
может иметь среднее 0.
Гамильтониан — оператор энергии, который также задаёт изменение состояния во времени. Для замкнутой системы уравнение Шрёдингера имеет вид
Слева скорость изменения амплитуд, справа действие оператора энергии на текущее состояние. Для обычной частицы массы в потенциале
получаем
Чтобы превратить это в конкретную задачу, задают , область, граничные условия и начальную функцию. Для не зависящего от времени
ищут стационарную компоненту
. Подстановка даёт
.

От двух атомных состояний к матрице перескока
Оставим два ортонормированных локализованных состояния ,
. Общее состояние
хранится как столбец
. Элемент матрицы
отвечает на конкретный вопрос: какая компонента вдоль
получается при действии
на состояние
.
Для одинаковых узлов диагональные энергии одинаковы; вычтем их общее значение, выбрав нуль энергии. Пусть связь между узлами вещественна, с соглашением ,
. Тогда
Перескок здесь означает внедиагональную связь амплитуд.
Для решение
,
.
Прямая подстановка проверяет обе строки уравнения.
Вероятности и
в сумме дают 1.
Характерный масштаб времени — ; сама величина
имеет размерность энергии.

Слева строка обозначает выходную компоненту, столбец — входную. Справа эволюция вычислена из этого гамильтониана и проверена матричной экспонентой. Стационарные комбинации и
имеют энергии
и
. В периодической решётке множество связанных узлов даёт энергетические зоны.

Слева — учебная двухузловая модель: обозначает время,
— энергию перескока. Справа — отдельный пример спектра со щелью, не измеренный спектр графена. Энергетическая зона — диапазон состояний, возникающий из-за периодической структуры; при изменении импульса меняется энергия внутри зоны. В идеальном собственном полупроводнике при нулевой температуре валентная зона заполнена, зона проводимости пуста, а между ними нет разрешённых одночастичных энергий: это щель
. У идеального нейтрального графена две соответствующие ветви соприкасаются в точках Дирака, поэтому такой щели нет.
На картинке уже сделано следующее приближение: идеальный бесконечный плоский кристалл, без дефектов, краёв, деформаций и тепловых колебаний.
Следующий шаг для упрощения в том, чтобы оставить только существенные электронные состояния и перескоки между ближайшими соседями. Ещё один шаг — разложить получившийся спектр около двух особых точек и
в ряд. Поэтому наличие атомных сот и наличие точного уравнения Дирака на всех энергиях не одно и то же.
Для более подробного знакомства с контекстом приведу материал из диссертации Алексея Алексеевича Соколика «Коллективные электронные явления в графене» (2010), страница с авторефератом.
Для свободного медленного электрона энергия движения равна квадрату импульса, делённому на удвоенную массу. В графене вблизи особых точек электронной структуры возникает другая, приближённо линейная зависимость:
Здесь — квазиимпульс, отсчитанный от соответствующей точки Дирака (для второй долины вместо
берут
). Квазиимпульс — метка состояния в периодическом кристалле, заданная с точностью до
-кратного вектора обратной решётки; обратная решётка состоит из векторов
, для которых
при любом переносе решётки
.
Далее в низкоэнергетическом уравнении для огибающей слово «импульс» используется в этом эффективном смысле. Величина — скорость порядка миллиона метров в секунду, а знаки обозначают две ветви спектра. Это низкоэнергетическое приближение: на произвольно больших энергиях продолжать две прямые нельзя.
Связь называется законом дисперсии.
Это употребление слова не связано со статистической дисперсией случайной величины.
Когда ниже говорится о параболической зоне, имеется в виду зависимость около её края вида , где
— эффективная масса, определяемая кривизной спектра.

Слева энергия изображена над двумерной плоскостью импульсов: верхняя и нижняя ветви образуют два конуса. Справа — сечение около точки . Сплошная линия получена из модели перескоков между ближайшими атомами, пунктир — её линейное разложение. Здесь
— расстояние между соседними атомами,
— энергия перескока. При удалении от вершины кривые расходятся: «релятивистская вселенная» имеет область применимости.
Теперь применим введённые понятия к низкоэнергетическому электронному состоянию графена.
Почему появляется формула, похожая на релятивистскую?
Сотовая решётка состоит из двух подрешёток, A и B. Для описания состояния нужны две амплитуды: насколько оно связано с A и с B. Поэтому волновая функция становится столбцом из двух компонент, а оператор энергии — матрицей:
Чтобы найти допустимые энергии, нужно решить задачу на собственные значения: . Это поиск состояний, которые оператор лишь умножает на число
, сохраняя их направление в пространстве состояний; это число и есть соответствующая энергия. Определитель матрицы
даёт
откуда и получаются две ветви.
Двухкомпонентность связана с подрешётками; настоящий спин электрона и две долины графена являются тут дополнительными степенями свободы. Смешивать эти три понятия друг с другом нельзя.
Спин — собственный квантовый угловой момент, а не вращение маленького шарика. У электрона есть две проекции спина на выбранную ось.
Долина — окрестность одной из двух неэквивалентных точек в пространстве импульсов.
Псевдоспином здесь называют двухкомпонентную степень свободы A/B.
Электрон — фермион. В квантовой механике принцип Паули запрещает двум одинаковым фермионам занимать одно и то же полное одночастичное состояние. Состояния с разным спином или долиной уже различны.
Бозоны, например фотоны, такого запрета не имеют. Эти правила заполнения понадобятся, когда будем решать, какие электронные переходы разрешены.
Маленькая вселенная на лабораторном столе
Плоская волна имеет вид : при сдвиге координаты меняется её фаза, а модуль остаётся постоянным. Импульс такой компоненты равен
. Одна волна на бесконечной плоскости не локализует частицу. Волновой пакет — суперпозиция волн с разными
: их фазы могут усиливать друг друга в одном месте и подавлять в другом.

На рисунке : слева показана вещественная часть, справа — модуль её Фурье-образа с максимумом, приведённым к единице. Ширина по
пропорциональна
. Вертикальные масштабы выбраны для наглядности; это не сравнение нормированных вероятностей разных пакетов.
Преобразование Фурье раскладывает профиль по плоским волнам:
.
Вместо значения в каждой точке можно хранить амплитуду каждой пространственной частоты. Производная действует на одну волну особенно просто: .
Это и объясняет, почему оператор импульса удобно заменять числом в Фурье-базисе.
До сих пор импульс был числом для отдельной плоской волны. Чтобы описать произвольный волновой пакет, заменим его оператором . Получим уравнение эволюции двухкомпонентной функции
:
Это уравнение первого порядка и по времени, и по пространственным координатам. В обычном уравнении Шрёдингера для свободного массивного электрона справа стоит оператор второго порядка, . Уравнение Дирака в вакууме имеет структуру
Сходство видно: матрицы умножаются на первые производные. В идеальной низкоэнергетической модели графена нет массового члена, пространство двумерно, а место в кинематике носителей занимает
м/с, примерно в 300 раз меньше скорости света. Матрицы подрешёток играют роль, аналогичную дираковским матрицам, но графеновый псевдоспин нельзя отождествлять с настоящим спином электрона.

*Кадр из «Рика и Морти», The Ricks Must Be Crazy, сезон 2, серия 6 (Adult Swim); источник изображения. У Рика — вселенная в батарейке, у физиков — диракоподобные квазичастицы в графене. *
В этой аналогии вакуумом является выбранное исходное состояние без возбуждений.
Квазичастица — возбуждение сложной системы, которое в некотором режиме можно описывать как отдельный объект с энергией и импульсом.
Электронные и дырочные квазичастицы материала определяются свойствами среды.
Помню, Лозовик любил говорить: «Вакуум — это полупроводник». Он имел в виду, что свойства физического вакуума в современной физике качественно по многим параметрам совпадают со свойствами полупроводников, а пробой вакуума и полупроводника описываются примерно одинаково с математической точки зрения.
Получается, что графен похож на параллельную вселенную с другой физикой.
В нем живут эффективно безмассовые фермионные квазичастицы: электронные и дырочные возбуждения с линейной дисперсией. Здесь «безмассовый» означает отсутствие щели и массового члена в выбранной эффективной модели. Иными словами, модуль энергии пропорционален модулю эффективного импульса. Форма зависимости напоминает фотонную, но скорость здесь , а сами возбуждения — фермионные.
У этой аналогии есть особенно интересная вычислительная сторона.
Безразмерная константа связи сравнивает силу взаимодействия с характерной энергией свободного движения. В КЭД такую роль играет постоянная тонкой структуры.
В СИ обычная постоянная тонкой структуры равна . Отношение кулоновской потенциальной и кинетической энергий в графене задаёт
Здесь — эффективная относительная диэлектрическая проницаемость окружения; для простой модели двух сред её часто приближают полусуммой их проницаемостей. Получается число порядка двух вместо 0.0073.
Поэтому взаимодействие может давать существенные поправки. Из этого не следует, однако, что абсолютно каждая поправка обязательно велика: важны экранирование, плотность носителей, масштаб энергии и сама наблюдаемая величина.
Графен — настольная лаборатория аналогов квантовой электродинамики: поляризация вакуума, релятивистская кинематика и туннелирование Дирака. Все эти сюжеты подробно обсуждаются тут, например: Кацнельсон и Новосёлов; экспериментальная работа о безмассовых дираковских носителях в графене — Novoselov et al., Nature (2005).
Конечно, фотоны в лаборатории не начинают летать со скоростью .
Электромагнитное поле остаётся трёхмерным, кристалл задаёт выделенную систему отсчёта, а на больших энергиях проявляется решётка. В принципе, любой полупроводник можно условно рассматривать как эффективный «вакуум» для своих возбуждений — с другими массами, щелью, скоростями и взаимодействиями. Но далеко не каждый такой вакуум релятивистский, и не каждый моделирует именно КЭД.
Теперь приложим магнитное поле перпендикулярно листу графена. В результате вместо непрерывной шкалы энергий появляются уровни Ландау. В рассматриваемом приближении
Последняя формула записана в СИ,
— модуль заряда электрона.

Слева присутствуют положительные и отрицательные уровни и особый уровень с нулевой энергией. Справа показано различие законов: у обычного электрона с параболической дисперсией уровни равноотстоящие, у графена интервалы уменьшаются с ростом номера.
Откуда взялся квадратный корень, можно понять без полного решения дифференциального уравнения. В поле компоненты кинетического импульса уже не коммутируют. Из них составляют операторы понижения и повышения , как для квантового гармонического осциллятора. При выборе подходящей калибровки электромагнитного поля
После возведения этой матрицы в квадрат на диагонали оказываются и
. Их собственные значения связаны с целыми номерами осциллятора. Следовательно квадрат энергии пропорционален номеру уровня, а сама энергия — его квадратному корню.
Для примера при Тл и
м/с магнитная длина около
нм, а расстояние от нулевого уровня до первого — около
эВ.
2. Как из электрона и отсутствующего электрона получается задача о паре
Представим себе, что электрон перешёл с занятого уровня на свободный уровень
. На верхнем уровне появился электрон, а на нижнем осталось незанятое место. Это место удобно описывать как положительно заряженную квазичастицу, которую называют дыркой.

Положительный заряд дырки является математическим способом учитывать недостающий отрицательный заряд относительно заполненного исходного состояния. Дырка может перемещаться, когда электроны занимают освободившиеся состояния. Её энергию и эффективный импульс описывают относительно выбранного заполнения; динамика определяется спектром материала.
Без взаимодействия энергия такого возбуждения равна
Электрон-дырочное возбуждение в магнитном поле называют магнитоэкситоном. Его энергия зависит от взаимодействия и от состояния движения пары.
От уравнения Шрёдингера к вычислению поправок по теории возмущений
В разделе 1 мы получили стационарное уравнение из уравнения Шрёдингера. Теперь найдём, как меняются его решения при добавлении взаимодействия.
Кет обозначает вектор состояния, бра
— сопряжённый вектор, а
— скалярное произведение. В координатах это
; звёздочка означает комплексное сопряжение.
Для конечной матрицы эрмитовость обеспечивает вещественные энергии и ортогональность собственных состояний с разными энергиями. Для дифференциального гамильтониана нужна самосопряжённость с заданной областью определения: допустимые функции и граничные условия входят в задачу. Одной лишь формальной симметрии выражения недостаточно. Это различие описано, например, в лекциях Тонга, раздел 3.2.2.
Предположим, задача уже решена.
Добавим взаимодействие .
Параметр — безразмерная ручка силы взаимодействия; физически её потом часто ставят равной 1. Давайте разложим в формальный ряд
Верхний индекс в скобках — порядок поправки, нижний — номер состояния. Для удобства выбираем промежуточную нормировку
: все поправки к вектору ортогональны исходному состоянию.
Подставим ряды в уравнение и соберём одинаковые степени . При
:
Так получается рекурсивный алгоритм: каждый следующий этаж строится из предыдущих. Проекция на исходное состояние даёт .
В первом порядке получается особенно просто. Уравнение
умножаем слева на . Два члена с неизвестным исправлением состояния сокращаются, поскольку
. Остаётся
Проекция на другое состояние даёт уже саму поправку к вектору и затем вторую поправку к энергии:
Итак:
Что такое большое O?
Запись при
означает: в некоторой достаточно малой окрестности нуля есть постоянная
, такая что
.
Это оценка масштаба остатка. Существование разложения до каждого фиксированного порядка ещё не гарантирует сходимость бесконечного ряда. А в квантовой теории часто работают с асимптотическими рядами, которые улучшают ответ лишь до некоторого порядка, а дальше не работают.
О деталях того, что такое асимптотическое разложение, можете посмотреть в Википедии.
Вывод выше относится лишь к изолированному невырожденному уровню.
При вырождении несколько состояний имеют одну энергию: нулевой знаменатель сигнализирует, что сначала нужно диагонализовать в этом подпространстве.
Малые ненулевые знаменатели тоже предупреждают о сильном смешивании. В задаче Ландау сначала учитываются вырождение и сохраняющиеся квантовые числа пары.
Малость самого значения параметра ещё не гарантирует слабое смешивание.
В модели
,
где
— зазор между исходными уровнями,
а — вещественный матричный элемент с размерностью энергии, нижний уровень известен точно:
Квадратные скобки здесь обозначают приближение до второго порядка, включая нулевую энергию исходного уровня. Контролировать надо отношение .
В нашем примере с и
при
точный ответ около
, второй порядок даёт
. При
точный ответ уже
, а второй порядок —
. Одна и та же
попала в разные режимы.

Матричный элемент: сколько одного состояния получается переходом из другого
В полном ортонормированном базисе оператор раскладывается по нему так
.
Поэтому столбец матрицы говорит, какие компоненты получились после действия
на состояние
.
Диагональный элемент — среднее взаимодействие и первый сдвиг энергии.
Внедиагональный задаёт смешивание состояний. Это комплексная амплитуда с размерностью энергии, не вероятность.
Например, при коротком действии слабого возмущения амплитуда перехода в первом порядке по времени равна
, а вероятность равна квадрату ее модуля.
Для локального скалярного потенциала:
Диагональ усредняет потенциал с плотностью .
Интегралы от внедиагональных элементов могут обнуляться из-за взаимного уничтожения положительных и отрицательных вкладов.
В учебном примере с одномерным квантовым осциллятором возьмём
и
.
Для среднее в каждом из этих состояний равно нулю, а
: возмущение смешивает состояния, хотя его диагональное среднее равно нулю.
Для наоборот:
, но
,
.

Нечётные части взаимно уничтожаются, что определяет правило отбора, которое можно проверить до запуска большого расчёта.
Для пары в дипломе сначала есть две двумерные координаты и сумма по компонентам графенового спинора:
Здесь предполагается, что потенциал одинаково действует на разные компоненты, а для матричного потенциала нужна сумма по двум компонентным индексам.
Индексы обозначают состояния пары,
— её компоненту.
После выделения движения центра пары и фиксации сохраняющейся метки остаётся интеграл по относительной координате; для диагонального элемента его можно записать как
.
Дальнейшее угловое интегрирование и преобразование Фурье приводят к двойному радиальному интегралу раздела 7. Потерять здесь меру — значит изменить саму задачу, поэтому это можно использовать как еще одну точку контроля.

Слева для наглядности взяты нормированный радиальный вес и сглаженный притягивающий потенциал
. Закрашенная площадь равна среднему взаимодействию.
Справа совсем маленькая задача, которую можно решить точно:
Для нижнего уровня первый порядок даёт , второй добавляет
. При малой
приближения согласуются с точной нижней собственной энергией; при сильном смешивании расходятся. Проверить можно так:
import numpy as np for lam in [0.01, 0.1, 0.5, 1.0]: H = np.array([[0.3*lam, 0.4*lam], [0.4*lam, 1-0.2*lam]]) exact = np.linalg.eigvalsh(H)[0] print(lam, exact, 0.3*lam, 0.3*lam-0.16*lam**2)
В этой статье будет обозначать положительную величину кулоновского ядра для одинаковых зарядов. В энергии притягивающейся электрон-дырочной пары прямой вклад входит со знаком минус. Поэтому положительное число в таблице матричных элементов нельзя автоматически называть положительной поправкой к энергии или полной энергией экситона.
И ещё одна важная граница приближения. В графене энергия Ландау и характерная энергия кулоновского взаимодействия обе растут как , то есть как
. Их отношение
само по себе не уменьшается при увеличении магнитного поля. Сильное поле не гарантирует слабость взаимодействия. Смешивание уровней и роль диэлектрического окружения подробно обсуждаются в работе Лозовика и Соколика.
3. Зачем понадобилось экранирование
Пока мы рассматривали электрон и дырку так, как будто они остались вдвоём и больше вокруг ничего нет. Но в материале есть много других электронов.
Заряды электрона и дырки воздействуют на эти электроны, вследствие чего они перераспределяются в пространстве и создают какое-то новое электрическое поле.
Другая пробная частица чувствует уже сумму внешнего и индуцированного полей. Такое изменение взаимодействия со средой физики называют экранированием.
Положительный пробный заряд притягивает электроны, поэтому вокруг него появляется избыток отрицательной плотности заряда, который уменьшает его поле на больших расстояниях. Отрицательный заряд, наоборот, вытесняет электроны, оставляя рядом избыток положительного заряда относительно исходного состояния. Он тоже уменьшает поле пробного заряда на больших расстояниях. в обоих случаях получается ослабление потенциала.

Справа — учебная трёхмерная статическая модель с потенциалом вместо
;
называется длиной экранирования.
Почему отклик вообще линеен?
При достаточно слабом воздействии можно разложить изменение плотности в ряд Тейлора по его величине. Оставляя первый член, получаем для одной пространственно-временной гармоники. В координатах это в общем случае интеграл по координатам и времени воздействия.
— это потенциальная энергия электрона, а не электрический потенциал
:
при
. Повышение локальной энергии делает занятие области менее выгодным, поэтому обычный статический электронный отклик имеет отрицательную
.
Для положительного пробного заряда
,
, и
— это согласуется с картинкой накопления электронов.
Дальше возникает самосогласованность. Это слово означает, что мы учли, что электроны реагируют на полное поле, которое создается в том числе их реакцией на него. Если только один раз вычислить отклик на внешнее воздействие, то получится лишь первая поправка к взаимодействию. А решение самосогласованной системы уравнений на плотность заряда и поле суммирует всю обратную связь в этой модели.
Проще всего вывести нужную формулу через линейный отклик. Обозначим исходное взаимодействие через , эффективное — через
. Рассматриваем одну пространственно-временную гармонику, чтобы для простоты описывать взаимодействие и отклик числами, а не матрицами.
Здесь важно не перепутать два поля. — внешняя потенциальная энергия электрона;
включает ещё и поле перераспределившихся электронов.
Неприводимая поляризация связывает изменение плотности с полным полем. В диаграммной записи этот блок нельзя разделить на два последовательных блока отклика разрезанием одной внутренней линии кулоновского взаимодействия. В RPA мы заменяем его пузырьком независимых электронов
. Когда ниже пишем просто
в RPA-примере, подразумеваем именно это приближение.
Решая два уравнения, находим полный отклик на внешнее воздействие:
Это другая величина. Пусть — амплитуда заданного источника в той же нормировке числовой плотности; знак источника включён в неё. Если
, а полное поле
, получаем
Справа исходное воздействие плюс воздействие через отклик среды. Важно, что среда реагирует на полное поле: поэтому в последнем множителе стоит .

Слева — схема самосогласованного отклика. Справа взят учебный постоянный : при ненулевом взаимодействии
и
уже различаются. Эквивалентная запись
использует полный отклик. Подстановка
вместо
в знаменатель привела бы к повторному учёту экранирования. Различие двух откликов разобрано, например, в лекциях Антуана Жоржа, разделы II–III; при сравнении формул нужно учитывать соглашения о знаках заряда и потенциала.
Переносим второй член налево и решаем уравнение:
Если при выбранных аргументах , ту же формулу можно разложить в геометрический ряд:
Каждая дополнительная вставка описывает ещё один отклик. В диаграммной записи элементарный электрон-дырочный отклик рисуется пузырьком из двух электронных линий. Так возникает цепочка петель.

У каждого пузырька два места присоединения к взаимодействию.
Между ними идут две электронные линии, которые в отклике описывают создание и уничтожение электронно-дырочного возбуждения. Оранжевая линия — взаимодействие; она не изображает ещё один электрон. Переменные каждого следующего пузырька суммируются или интегрируются по тем же правилам.
Если принять , то
При частичные суммы сходятся к ослабленному взаимодействию.
При этот ряд уже не сходится, хотя алгебраическое решение
остаётся определённым. Поэтому само выражение с дробью полезнее механического сложения всё большего числа членов.

Анимация построена в Python. По горизонтали безразмерная сила отклика , а не время и не траектория электрона. Чёрная линия — решение уравнения, цветная — частичная сумма. При
ближе к единице поправки затухают медленнее. Скрипт
06_graphene.py ниже создаёт и GIF, и статическую картинку.
Вот буквальный расчёт цепочки в одном канале. Пусть уже вычисленный пузырёк дал , а
в согласованных условных единицах. Тогда
. Вклад с нулём вставок равен 2, с одной —
, с двумя —
. Каждый следующий получается умножением предыдущего на
.
V0, Pi = 2.0, -0.4 term = V0 partial = 0.0 for n in range(21): partial += term print(n, term, partial) term *= Pi * V0 exact = V0 / (1 - V0 * Pi) print("RPA:", exact) # 1.1111111111111112
После 21 члена получится примерно 1.1213593023. Ошибка усечения для суммы до — разность точного и приближённого ответов:
Невязка получается другим действием — подстановкой в уравнение
:
Для приведённых чисел , а
. Связь между ними здесь известна точно. В больших линейных системах малая невязка может сочетаться с существенной ошибкой, если задача плохо обусловлена; в разделе 17 увидим ещё и другую ловушку — маленькую невязку неверно составленного уравнения. Эти формулы можно проверить независимо от цикла.
Здесь для простоты задано числом; в разделах 4–5 разберём, как сам пузырёк получается из пропагаторов и суммы переходов. Полный воспроизводимый эксперимент с графиком каждого вклада находится в разделе 17.
Приближение хаотических фаз, RPA, использует такую сумму цепочек с простейшим поляризационным пузырьком. Приближение оставляет определённое семейство диаграмм. Вершинные поправки и другие классы взаимодействий оно автоматически не добавляет.
Вершинная поправка изменяет сам узел связи электронных линий с воздействием, например учитывает дополнительное взаимодействие внутри электронно-дырочного отклика.
Это объясняет важную разницу между тем, чтобы просуммировать бесконечную цепочку повторяющихся пузырьков и тем, чтобы посчитать полный вклад одного высокого порядка, где допустимо много разных структур. Первое оказывается гораздо проще.
4. Как читать петли и превращать рисунок в интеграл
Начнём с языка теории графов.
Граф — набор вершин и соединяющих их рёбер.
Вершина — узел, ребро — связь двух узлов.
Длина ребра не фиксирована, его можно нарисовать как угодно и даже гнуть, не меняя самого графа. Если между двумя узлами несколько рёбер или ребро возвращается в тот же узел, нужен мультиграф; именно такие случаи часто встречаются в диаграммах.
Маршрут — последовательность вершин и соединяющих их рёбер.
Простой путь проходит без повторения вершин.
Цикл замыкается в исходной вершине без повторения остальных вершин и рёбер; в мультиграфе допускаются также петля из одного ребра и цикл из двух параллельных рёбер.
Связный граф позволяет добраться между любыми двумя вершинами.
Дерево — связный граф без циклов.

На первом рисунке четыре вершины соединены тремя рёбрами: это дерево.
При добавлении любого ребра в дерево образуется цикл. Добавили диагональ — появился второй независимый цикл. Замкнутых обходов можно придумать больше двух, но остальные выражаются через выбранные два. Именно число независимых циклов связано с количеством свободных импульсов интегрирования.
Диаграмма — это всего лишь способ записать член математического выражения (ряда).
У рисунка есть три вида основных элементов: внешние и внутренние линии, и вершины.
Внешние линии задают состояния или воздействия, которые мы подготовили и наблюдаем.
Вершины задают разрешённые взаимодействия.
Внутренние линии описываются функциями распространения — пропагаторами. Их конкретный вид нужно брать из выбранной теории.
Для примера возьмём учебную теорию вещественного скалярного поля с четырёхлинейной вершиной.
Это поле задаётся одним вещественным числом в точке, без векторного направления или спинорных компонент.
Четырёхточечная функция — среднее произведения четырёх полей,
.
Её аргументы отмечают места вставок; сами по себе четыре точки не задают четыре классические траектории. На картинке показаны примеры связных вкладов в такую функцию:

В первой диаграмме четыре внешние линии встречаются в одной вершине. Внутренних линий нет. Во второй между двумя вершинами проходят две внутренние линии. В третьей три вершины соединены четырьмя внутренними линиями, образующими два независимых цикла.
Для связного графа справедлива формула Эйлера
где — число внутренних линий,
— число вершин,
— число независимых петель.
Внешние линии в здесь не включаются.
Чтобы понять формулу, соедините все вершины деревом.
Для этого требуется внутренних рёбер.
Каждое оставшееся ребро замыкает один новый независимый цикл, поэтому их число равно в точности .
Почему это связано с формулой Эйлера и что меняется на торе
Назначим импульс каждой из внутренних линий. В вершинах получим
уравнений сохранения, но одно следует из остальных и общего сохранения внешнего импульса. Независимых условий
, значит, свободных внутренних импульсов
. У несвязного графа с
компонентами аналогичный счёт даёт
.
Теперь нарисуем связный граф без пересечений на сфере и посчитаем области, на которые он её разделил, — грани . Для клеточного разбиения, где каждая грань топологически диск, выполняется
Эйлерова характеристика поверхности — число , не зависящее от выбранного клеточного разбиения. Для замкнутой ориентируемой поверхности рода
, то есть сферы с
ручками,
Например, у сферы , у тора
, у поверхности с двумя ручками
. Одну ручку можно получить, удалив два диска и соединив отверстия цилиндром: удаление дисков уменьшает характеристику на два, цилиндр имеет характеристику нуль. Это объясняет каждое следующее
. При наличии
граничных компонент формула становится
, если разбиение учитывает границу.

На сфере две петли с общей вершиной дают ,
,
: характеристика 2. Тор удобно представить квадратом со склеенными противоположными сторонами. Четыре его угла превращаются в одну вершину, стороны — в два ребра, внутренняя область — в одну грань:
. Это тот же абстрактный граф с
, но другое вложение.
Поэтому формула не меняется на торе.
Меняется связь циклов с гранями: на замкнутой ориентируемой поверхности при клеточном вложении . На сфере каждый независимый цикл можно связать с ограниченной областью плоского рисунка; на торе добавляются обходы вокруг ручки. Смешивать эйлерову характеристику поверхности и число независимых петель диаграммы нельзя.
Теперь подпишем импульсы на однопетлевом рисунке. Пусть через пузырёк передаётся суммарный импульс . Если по верхней внутренней линии идёт
, по нижней должен идти
: это закон сохранения импульса в вершине. Значение
внешними условиями не задано. Нужно сложить вклады всех допустимых
, то есть проинтегрировать.
Дельта-функция: точечный толчок
Для линейной задачи ответ на произвольный источник можно собрать из откликов на локальные толчки. Один такой толчок описывают дельта-функцией Дирака — обобщённой функцией, действие которой на подходящую пробную функцию
задаётся равенством
Обычной локально интегрируемой функции с таким свойством нет. Но существуют семейства функций, сходящиеся к дельта-функции в смысле действия на пробные функции. Удобный пример — нормированные гауссианы:
Высота растёт, ширина уменьшается, площадь остаётся равной 1. Размерность дельта-функции — обратная размерность её аргумента: имеет единицы
.
На сетке точечный источник можно представить значением в одной ячейке ширины
.

Слева растёт высота, но сохраняется площадь. Справа распределённый источник разбит на маленькие толчки; их отклики складываются в решение.
В статье Дирака 1927 года о физической интерпретации квантовой динамики такой объект появляется при работе с непрерывными квантовыми переменными. В дискретном базисе роль единичного толчка играет символ Кронекера : 1 при
, 0 иначе. В непрерывном базисе его аналог —
.
Функция Грина: обратный оператор с оговорёнными границами
Пусть линейное уравнение имеет вид . Вместо решения заново для каждого
найдём ядро обратного оператора:
Почему это работает?
Применяем ко второму равенству, переносим его под интеграл и получаем
.
Это и есть функция Грина: отклик в точке на единичный источник в
.
Здесь предполагаются однородные граничные условия; неоднородные добавляют отдельный граничный вклад. Если у есть нулевая мода — ненулевое решение
с теми же условиями, — обратный оператор без дополнительных ограничений может не существовать.
Границы — это часть определения , при разных границах функция Грина будет разной. Проще говоря, один и тот же дифференциальный оператор на прямой, на кольце и на отрезке с закреплёнными концами даёт различные функции Грина.
Во временной задаче запаздывающее условие запрещает ответ до воздействия. Например, для эволюции с постоянным
Ступенька равна 0 при
и 1 при
; её производная — дельта-функция.
здесь единичный оператор.
Что такое действие поля и почему возникает обратный оператор
Лагранжев подход начинает описание с одного скалярного объекта — действия, — из которого получают все уравнения движения. В механике частицы задают координату , лагранжиан
и действие
. Для обычной частицы
. Действие имеет размерность энергии, умноженной на время.
Лагранжев подход позволяет одинаково работать со многими координатами, полями, связями и симметриями. Но лагранжиан всё равно надо обосновать физически, так как сам принцип стационарного действия не выбирает правильную модель автоматически.
У поля вместо одной координаты есть значение в каждой точке: .
Поэтому лагранжиан записывают как пространственный интеграл от лагранжевой плотности , а действие — как интеграл по пространству и времени:
Функционал принимает на вход целую функцию и возвращает число.
Например, действие оценивает всю историю поля. Обычная функция получает число
; функционал
получает профиль или историю
целиком.
Для натянутой струны в приближении малых поперечных смещений можно взять
Здесь — масса на единицу длины,
— натяжение,
— дополнительная восстанавливающая сила на единицу длины,
— внешняя сила на единицу длины.
Первое слагаемое является кинетической энергией на единицу длины, следующие два задают потенциальную энергию с минусом. Модель даёт уравнение
.
Поле, конечно же, не обязано быть струной. Просто этот пример помогает понять, что означает непрерывное множество связанных степеней свободы.

Вариация — малое изменение самой функции. Запишем соседнюю историю как , где
— произвольная допустимая форма изменения, а
— малое число. Требование стационарности означает
для всех таких
. На фиксированной границе
. Это не производная поля по координате и не утверждение, что реальное действие всегда минимально.
Для лагранжевой плотности с первыми производными правило произведения и интегрирование по частям дают уравнение Эйлера — Лагранжа:
В примере струны три члена равны ,
и
. Перенос знаков даёт написанное уравнение. Такой переход от механики к полю разобран в лекциях Тонга по классической теории поля.
Интегрирование по частям без пропущенного шага
Теперь вернёмся к простой модели, из которой получим пропагатор. Это одномерное евклидово скалярное поле. Используем единицы и квадратичное действие с источником:
Штрих обозначает производную по ,
имеет размерность обратной длины.
Евклидовость здесь означает геометрию с одинаковыми знаками в квадрате длины. В квантовой теории такое описание часто связывают с продолжением времени в мнимую область, но допустимость перехода требует условий. Здесь мы явно выбрали положительную свободную квадратичную часть и изучаем статический оператор.
Вставим вместо
, не меняя
. Используем
и аналогично раскроем квадрат
. Коэффициент при первой степени
равен
Пока первая часть содержит производную пробного изменения . Нам нужно получить одну общую произвольную функцию
, чтобы потребовать исчезновения её коэффициента. Используем
:
При закреплённых значениях поля , поэтому крайний член исчезает. На бесконечной прямой вместо этого задают достаточное убывание. Получаем
Если скобка не равна нулю в какой-то области, можно выбрать там изменение соответствующего знака и получить ненулевую вариацию. Поэтому стационарность для всех допустимых изменений требует
Здесь обозначает уже дифференциальный оператор; лагранжева плотность выше обозначалась
. Именно интегрирование по частям превратило квадрат первой производной в оператор со второй производной и зафиксировало знак минус.

Численный пример на :
,
,
,
.
Все профили имеют нулевые края. Прямое интегрирование даёт здесь
.
Линейного члена нет. Однако стационарность тут действительно является минимумом благодаря положительной квадратичной части.
Почему тот же оператор появляется в гауссовом среднем
На конечной сетке поле становится вектором , а действие —
.
Матрица вещественная, симметричная и положительно определённая:
для ненулевого
.
Шаги сетки включены в и дискретный источник.
Дополним квадрат:
В интеграле сдвиг переменной убирает
из квадрата. Остаётся тот же гауссов интеграл, что при
, умноженный на экспоненту последнего члена. Поэтому
Вторая формула получается двумя производными по при
: каждая производная исходного интеграла вставляет множитель
. Так обратный оператор одновременно решает линейное уравнение и задаёт двухточечную корреляцию свободного поля. В непрерывном пределе матрица заменяется ядром
; вопросы существования предела оговариваются отдельно. При восстановлении
ковариация получает множитель
.
Ковариация —
.
При средние в этой чётной гауссовой модели нулевые, поэтому она совпадает с двухточечным средним.

*Для обратная матрица равна
.
Наклон облака показывает положительную совместную флуктуацию. Облако — конечная выборка.*
В реальном квантовом времени вес осциллирует, так как складываются амплитуды, а не положительные вероятности отдельных траекторий.
Пропагатор: получаем конкретную формулу
Для оператора на бесконечной прямой с убыванием на обоих концах определим
условием
.
Вне нуля справа нуль. Убывающее на бесконечностях решение имеет вид . Проинтегрируем уравнение на маленьком отрезке около нуля:
. Производные равны
и
, значит,
и
Теперь разложим ответ по плоским волнам: . Оператор
на такой волне превращается в число
, а дельта-функция имеет Фурье-образ 1. Уравнение становится
. Отсюда

Большая сокращает характерную длину корреляций
. Оба графика описывают один оператор в разных базисах. В гауссовом функциональном интеграле двухточечная корреляция также равна обратному квадратичному оператору; при выборе нормировки
это тот же
. Для электронов, фотонов, запаздывающих функций и магнитного поля нужно своё ядро, матричная структура и свои граничные условия.
Почему пару полей заменяют линией: правило Вика по шагам
Здесь спрятан важный переход: интеграл от произведения многих полей заменяется суммой произведений двухточечных функций. Он работает благодаря гауссовости свободной модели. Для произвольного распределения такое сокращение неверно. Сначала сформулируем правило точно, затем выведем его и проверим на числах.
Что именно усредняем. Представим поле на конечной сетке вектором вещественных переменных . Свободное действие квадратично:
Здесь — вещественная симметричная положительно определённая матрица: тогда интегралы сходятся. Буква
обозначает любую интегрируемую функцию поля, например
.
При таком весе , а
— ковариация, то есть среднее произведение двух центрированных переменных. Индекс может обозначать узел сетки; в непрерывной записи он превращается в координату
, а
— в
.
Совпадающие точки в непрерывной теории могут требовать регуляризации. В случае конечной сетки для расчетов достаточно обычной конечномерной алгебры.
Правило Вика.
Для совместно гауссовых вещественных переменных с нулевыми средними среднее нечётного произведения равно нулю. Среднее произведения переменных равно сумме по всем разбиениям их помеченных мест на непересекающиеся неупорядоченные пары:
— множество таких разбиений; в каждом разбиении каждое место от 1 до
используется ровно один раз. Для каждой пары выписываем одну ковариацию, перемножаем их внутри одного разбиения, затем складываем результаты разных разбиений. Даже если два места содержат одно и то же поле, места при подсчёте остаются различимыми. Именно отсюда позднее возникают множители 3, 12 и 192.
Если среднее поля ненулевое, сначала пишут , где
, и применяют правило к центрированным
, раскрыв произведение.
Примечание.
Для негауссова взаимодействующего распределения одних двухточечных средних в общем случае недостаточно, так как появляются независимые связные корреляции более высоких порядков. В операторной квантовой теории теорема Вика формулируется для упорядоченных произведений свободных полей через нормальное упорядочение и свёртки; у фермионов появляются знаки перестановок. Нормальное упорядочение переносит операторы рождения частиц левее операторов уничтожения. Свёртка пары свободных полей в вакууме равна их временно упорядоченному двухточечному среднему; временное упорядочение ставит оператор с более поздним временем левее. Для фермионов обе перестановки требуют соответствующих знаков. Это объясняет родство с гауссовой формулой. Операторная формулировка разобрана в лекциях Тонга, раздел 3.3, а гауссовая — в его лекциях по статистической теории поля.
Ниже мы доказываем именно вещественную гауссову версию, нужную для нашего евклидова примера.
Шаг 1. Зачем вводить источник.
Добавим к экспоненте и нормируем интеграл:
Переменные здесь — вспомогательные ручки для вычисления средних. Производная по
вытаскивает множитель
из экспоненты. После дифференцирования ставим все источники равными нулю:
Это объясняет слова «производные вставляют поля»: никакого дополнительного физического процесса при этом не происходит. Для конечномерного гауссова интеграла такие дифференцирования обоснованы сходимостью.
Шаг 2. Используем уже вычисленный гауссов интеграл.
В подразделе «Почему тот же оператор появляется в гауссовом среднем» мы выделили полный квадрат и сделали замену переменной интегрирования. При было получено
Теперь будем дифференцировать это выражение, чтобы получить средние более высоких порядков.
Шаг 3. Две производные дают одну линию.
Обозначим . Тогда
Поскольку и
, остаётся
. Графическое сокращение для этого множителя — линия между метками
и
.
Шаг 4. Четыре производные дают три рисунка.
Ещё одна производная даёт
Теперь дифференцируем по и только после этого ставим
. Производная каждого одиночного
даёт один
; остальные слагаемые всё ещё содержат хотя бы один
и исчезают:

Сначала выбираем партнёра для места 1: это 2, 3 или 4. Два оставшихся места образуют вторую пару автоматически. Пересечение линий в третьем рисунке не создаёт вершину: там нет новой точки взаимодействия. Цвет различает пары внутри одного слагаемого, а не виды частиц. Между тремя рисунками стоит плюс.
Шаг 5. Почему правило работает для любого чётного числа полей. Разложим в ряд:
Произведение в скобках содержит по два источника на каждый . При
производных и
выживает только член степени
: меньшая степень уже продифференцирована до нуля, от большей остаётся источник. Каждое полное спаривание возникает
раз: можно переставить концы каждой из
пар и сами
пар. Эти повторения сокращают делитель перед членом ряда. Остаётся один экземпляр каждого произведения ковариаций. Нечётных степеней источника в экспоненте квадратичной формы нет, поэтому нечётные средние равны нулю.
Число разных спариваний равно
Первому месту выбираем партнёра способами, затем повторяем для оставшихся мест. Получаем: 2 поля — 1 спаривание, 4 — 3, 6 — 15, 8 — 105, 10 — 945. Это число спариваний помеченных полевых множителей; после отождествления внутренних точек несколько спариваний могут дать одну и ту же диаграмму.
Вычислительный пример: одинаковые поля не делают спаривания одинаковыми при подсчёте.
Пусть два гауссовых числа имеют нулевые средние и ковариацию
Это допустимая ковариационная матрица: первый диагональный элемент положителен, определитель равен 5. Для произведения есть три вклада:

Для независимой численной проверки надо ещё объяснить, как получить именно такую совместную гауссову модель.
Разложение Холецкого представляет положительно определённую ковариацию как , где
— нижнетреугольная матрица.
Здесь можно взять
Переменные независимы и имеют стандартное нормальное распределение — нулевое среднее и единичную дисперсию; их ковариационная матрица — единичная. Поэтому ковариация результата равна
, что проверяется обычным умножением матриц. Скрипт интегрирует непосредственно полученный полином
по двум стандартным гауссовым весам, без подстановки правила Вика. Для каждого
трёхузловая квадратура использует узлы
с весами
; она точна для полиномов степени до пяти. У нашего подынтегрального полинома степень по каждой переменной не больше четырёх, поэтому девять пар узлов дают точный результат в точной арифметике. В вычислениях остаётся округление. Проверяются также числа спариваний вплоть до десяти мест. Для одного числа с дисперсией
тот же подсчёт даёт
и
.
Как читать линию в диаграмме.
Линия между метками означает множитель
. Внешние точки — полевые множители наблюдаемой с заданными координатами. Внутренняя вершина взаимодействия объединяет поля, стоящие в одной интегрируемой точке: например,
даёт четыре места у вершины
. Чтобы восстановить член ряда по рисунку, надо выписать
для каждой линии, перемножить их, проинтегрировать внутренние координаты и учесть коэффициент разложения вместе с числом одинаковых спариваний. Именно этот перевод, а не история полёта частиц по нарисованным траекториям, будет использован дальше.
Как произведение двух линий действительно становится пузырьком
Возьмём составную наблюдаемую . «Составная» означает, что в одной точке перемножены поля.
На этом шаге можно держать в уме конечную сетку либо одномерное массивное поле ниже: совпадающие значения там конечны. При обобщении на непрерывную теорию в большей размерности произведение полей в одной точке может быть сингулярным.
Тогда его сначала определяют с регуляризацией — например, ограничивают большие импульсы или сохраняют конечную сетку, — а составную наблюдаемую определяют с необходимыми вычитаниями.
Перенормировка связывает параметры и наблюдаемые с физически заданными условиями после такой обработки. Формальное вычитание двух неопределённых бесконечностей вычислением не считается.
Большие импульсы называют ультрафиолетовой областью, малые — инфракрасной.
Здесь эти слова не означают, что в задаче обязательно есть свет соответствующего цвета. Наша одномерная модель с
позволяет сначала освоить саму комбинаторику без этих дополнительных трудностей. В свободном гауссовом среднем четыре поля разбиваются на пары тремя способами — это простейший случай правила Вика:
Покажем, зачем из этого среднего вычитать произведение отдельных средних. Обозначим и
. Отклонения от средних равны
,
. Раскрытие скобок даёт
Для имеем
и
.
Из трёх спариваний одно соединяет поля только внутри каждой точки и даёт именно . Два других соединяют
с
; каждое даёт
.
Поэтому после вычитания остаётся множитель 2. Связность в данном двухточечном примере означает ковариацию составных наблюдаемых. Нулевая ковариация сама по себе не является общим доказательством независимости произвольных наблюдаемых.
Полученное выражение называется связной корреляцией:

Левое спаривание даёт произведение двух отдельных средних. Два правых соединяют точки друг с другом и дают одинаковый вклад. Число 2 получено подсчётом, а не назначено по внешнему виду пузырька.
Свёртка складывает вклады всех разбиений общего импульса: одна линия получает , другая — оставшийся
. Это выражается интегралом произведения
по всем
. Почему произведение в координатах даёт именно такую свёртку в импульсах?
Подставим в каждый из двух множителей, дав импульсам разные имена
. Затем возьмём Фурье-образ по
:
Последний интеграл — Фурье-представление . Дельта-функция при интегрировании по
выбирает
; модуль якобиана равен 1. Из двух множителей
остаётся один. Таким образом,
Здесь — ядро пузырька из двух линий, а полный связный ответ для выбранного
равен
. В другой теории другие вершины и статистика частиц дадут свои коэффициенты и знаки. В частности, фермионный отклик плотности в графене нельзя получить простым переносом этого множителя 2: там есть матричные элементы плотности, заполнение уровней и соглашение о знаке отклика, которое разберём в разделе 5.
Для учебного евклидова пропагатора получаем ядро пузырька
Каждая внутренняя линия дала один знаменатель. Один независимый импульс дал один интеграл. Вершины дополнительно дают степени константы связи; одинаковые способы соединения линий определяют комбинаторный коэффициент. Полная амплитуда включает эти множители и сумму других разрешённых диаграмм. Формула выше показывает именно ядро выбранного пузырька.
Чтобы действительно посчитать его несколькими строками, положим и
. Тогда интеграл сходится и
Выпишем вычисление до конца. Для используем уже найденный образ
. Квадрат чётен, поэтому синусная часть Фурье-интеграла сокращается:
Чтобы вычислить оставшийся интеграл, представим косинус вещественной частью экспоненты. При
Подстановка даёт
. При
это
. Условие
нужно и для затухания на бесконечности, и для отсутствия сингулярности при нулевом импульсе. Теперь сравним формулу с непосредственным численным интегрированием исходного произведения двух знаменателей:
import numpy as np from scipy.integrate import quad def bubble(p, mu=1.0): f = lambda k: 1 / ((k*k + mu*mu) * ((p-k)**2 + mu*mu)) return quad(f, -np.inf, np.inf)[0] / (2*np.pi) for p in [0, 1, 2, 5]: calculated = bubble(p) expected = 1 / (p*p + 4) # mu = 1 assert abs(calculated - expected) < 1e-10 print(p, calculated)
Получатся ,
,
и примерно
. Это одномерная учебная модель. В четырёх измерениях тот же тип интеграла требует отдельного обращения с расходимостью; переносить туда этот конечный ответ нельзя.
Диаграммы появились из разложения экспоненты
Теперь покажем весь переход: вес поля → ряд по взаимодействию → гауссовы средние → спаривания → рисунки → интегралы.
Рисунок помогает не терять множители и одинаковые способы соединения.
Для прозрачности остаёмся в вещественной скалярной евклидовой модели с .
Свободное действие уже умеем интегрировать. Для сходящегося евклидова среднего берём
и при необходимости сохраняем конечную сетку и объём. Добавим
Величина управляет силой взаимодействия; её размерность зависит от размерности пространства и нормировки поля. Делитель
выбран для удобства комбинаторики. Четыре одинаковых поля в точке взаимодействия пока считаем четырьмя различимыми местами, которые позднее соединим с другими полями.
Нужная двухточечная функция определяется нормированным средним:
Индекс 0 означает среднее с нормированным свободным гауссовым весом. Знаменатель нужен, потому что взаимодействие меняет нормировку распределения. Разложение даёт
Почему во втором порядке два интеграла? Квадрат интеграла — произведение двух одинаково устроенных сумм по всем точкам; переменные независимы и получают имена . В общем порядке
:
Это точное разложение экспоненты для фиксированной конфигурации. Перестановка бесконечной суммы и интеграла по всем полям — отдельный вопрос. Пертурбативные коэффициенты вычисляют по порядкам, не предполагая заранее сходимость итогового ряда. Даже конечномерная модель ниже показывает, почему ряд может оказаться асимптотическим.

Нулевой и первый порядок: считаем все 15 спариваний
В нулевом порядке : одна линия, ни одной вершины взаимодействия. Для первого порядка нужно среднее шести полей
. У первого поля 5 вариантов партнёра, у следующего непарного — 3, у последнего — 1: всего
. Разделим их на две группы.
Если спарено с
, четыре поля в
спариваются тремя способами. Получается
. Если
спарено с одним из четырёх полей в
, для
остаются три; последние два поля соединяются между собой. Получается
одинаковых произведений
. Итак,
Домножим на коэффициент . Для второй группы
, значит, связанная с внешними точками поправка равна
Линия из в ту же точку образует петлю; такую диаграмму называют «головастиком». Она означает совпадающий аргумент пропагатора, а не круговой полёт частицы. В одномерной массивной модели
конечно; в других размерностях совпадающие точки могут требовать регуляризации.

Слева — другой наблюдаемый объект: связная четырёхточечная функция. В первом порядке каждая из четырёх внешних точек соединяется с одним полем в :
способов сокращают
в знаменателе. Получается
, дерево без петель. Справа — только что выведенный головастик. Одинаковая степень
не означает одинаковое число петель у разных наблюдаемых.
Куда делись три оставшихся спаривания
Числитель содержит свободную линию , умноженную на отдельный вакуумный рисунок из двух замкнутых линий при
. Знаменатель равен
потому что и
. Для отношения
первый порядок равен
. Именно произведение свободной линии на отдельный вакуумный вклад сокращается. До сокращения можно работать на конечной сетке и в конечном объёме, чтобы не вычитать неопределённые выражения.

Знаменатель убирает компоненты, никак не связанные с внешними вставками. Он не убирает все петли: головастик связан с и остаётся. Для четырёхточечной функции дополнительно вычитают произведения двухточечных функций, чтобы выделить связную часть.
Второй порядок: один двухпетлевой вклад целиком
Во втором порядке две вершины . Рассмотрим рисунок с одной внешней линией у каждой вершины и тремя линиями между ними — «sunset». Внешние
можно распределить по вершинам двумя способами. Для каждого: 4 выбора поля у первой вершины, 4 у второй и
способов соединить оставшиеся тройки. Получается
спаривания. Делим на
:

Внутренних линий 3, внутренних вершин 2, поэтому . Это одно слагаемое второго порядка, не полный коэффициент
. Код перебирает все 945 спариваний десяти помеченных полей и независимо выделяет 192 нужных.
Для однородной системы перейдём к импульсам. Каждое раскладывается по плоским волнам, интеграл по положению вершины даёт дельта-функцию сохранения импульса. Внутри sunset остаются два независимых импульса
, а третья линия несёт
.
Множитель — две внешние линии полной двухточечной корреляции. Если обсуждается ампутированная функция, их убирают по определению; без оговорки два правильных ответа выглядят несовместимыми. «Ампутация» здесь означает удаление внешних пропагаторов.
Для проверки вернёмся к головастику в одном измерении: . При
интеграл элементарен:
При ,
получаем
; независимая квадратура в новом скрипте даёт то же значение. Это проверка одного коэффициента, а не утверждение, что первый порядок равен точному взаимодействующему ответу.
Теперь соответствие конкретно:
степень ряда задаёт число вставок взаимодействия;
поля — концы линий;
спаривания — соединения;
интегралы — внутренние координаты;
коэффициент учитывает число одинаковых соединений и факториалы.
Число петель зависит от всего графа и внешних вставок. В фермионной теории добавятся знаки от перестановок, матричные индексы и другие вершины, поэтому коэффициенты скалярного примера нельзя напрямую переносить в графен.
Исторические работы — Фейнман (1949), Дайсон (1949) и Вик (1950).
Переход от разложения к спариваниям и диаграммам есть в лекциях Тонга.
От плоских волн к базису Ландау: теперь решаем задачу в магнитном поле
Базис — это набор известных состояний, по которым можно разложить неизвестное. Без поля свободные плоские волны диагонализуют импульс. В постоянном поле удобнее состояния, уже включающие его действие: иначе пришлось бы каждый раз собирать эффект поля из бесконечного числа плоских волн.
Пусть электрон имеет заряд ,
направлено вдоль
, а векторный потенциал выбран как
.
Это калибровка Ландау: она даёт .
Векторный потенциал — вспомогательное поле, через которое магнитное поле записывается как
; ротор
содержит разности перекрёстных производных. Один и тот же
можно представить разными
: замена
не меняет ротор.
Выбор такого представления называют калибровкой.
Волновую функцию тоже нужно преобразовать согласованно: для электрона , а измеримые вероятности сохраняются.

У калибровки Ландау и симметричной
одинаковый ротор. Их разность —
. Рисунок показывает стрелки потенциала, не силовые линии магнитного поля и не скорости частиц.
Канонический импульс здесь — оператор , связанный с выбором координат и фазы.
Кинетический импульс входит в энергию движения электрона. Для обычной массивной частицы он равен
; для графенового линейного гамильтониана такую формулу с постоянной массой использовать нельзя. Сначала посмотрим на обычный массивный электрон:
Координата явно не входит в оператор.
Поэтому ищем решение в виде . Тогда
заменяется числом
, и остаётся одномерное уравнение
Это уравнение гармонического осциллятора, центр которого сдвинут в .
Величина называется циклотронной угловой частотой обычного массивного электрона, потому что она равна частоте его вращения в постоянном магнитном поле, которая вычисляется на основе обычной классической механики (уравнения Ньютона
).
Она появляется здесь еще и как частота получившегося квантового осциллятора.
Его нормируемые решения дают и функции
Здесь — полином Эрмита:
,
,
.
Они появляются при поиске нормируемого решения. Дело в том, что после выделения гауссовой экспоненты степенной ряд должен оборваться, иначе функция растёт на бесконечности. Именно это условие обрыва и квантует энергию.

Слева функции с разным числом узлов. Вертикальные сдвиги добавлены только для читаемости. Справа три состояния одного уровня с разными : энергия одна, расположение плотности разное. При периодической границе по
соседние
отличаются на
, а центры — на
. На площади
помещается примерно
состояний одного уровня на одну внутреннюю компоненту. Так возникает вырождение Ландау; спин и долины добавляют к нему свои множители.
Теперь возвращаемся к графену. В дираковском гамильтониане заменяем на
. Поскольку
, операторы
удовлетворяют . В базисе состояний
с подходящими фазами
,
.
Конкретно, для вещественных функций выше можно выбрать
: тогда множитель
в операторе понижения поглощается фазами. Плотности вероятности на рисунке от этого не меняются.
Положим .
Для одной долины при подстановка этого значения дает
Проверка занимает всего лишь одну строчку. Верхняя компонента после действия равна
, нижняя после
—
.
Нулевое состояние устроено особым образом:
, поскольку
.
В другой долине меняется распределение по подрешёткам. Так из уравнения получаются
.
Разложение по состояниям удобно записывать с помощью проекторов.
Проектор — это оператор выделения компоненты.
Для нормированного состояния оператор
оставляет только компоненту вдоль него:
если , то
.
Он удовлетворяет .
Сумма проекторов полного ортонормированного базиса равна единичному оператору.
«Ортонормированный» означает
.
Проще говоря, разные состояния ортогональны, каждое нормировано на единицу.

Наверху : вероятность результата 0 равна
. Ниже номера обозначают полные одночастичные состояния.
Много электронов на одном уровне Ландау не нарушает принципа Паули здесь, потому что у каждого уровня тут много разных состояний в зависимости от расположения центра орбиты, значений спина и долины.
Несколько независимых состояний с одинаковой энергией — это в квантовой механике называется вырождением.
Итак, электронную линию удобно раскладывать в сумму по , в которой каждый член содержит проектор на состояние и энергетический знаменатель.
Пузырёк соединяет два таких состояния, поэтому возникают два номера уровня, заполнения и матричный элемент плотности между ними.
Сумма по центрам орбит учитывает вырождение.
Исчезновение явного интеграла по одному импульсу не означает исчезновения петли — просто изменился базис, который использовался в расчетах.
Полный код учебных диаграмм и однопетлевого интеграла: 08_diagrams.py
"""От 0 до 2 петель: топологии связной четырёхточечной функции φ⁴. Не все диаграммы каждого порядка. Численный пример — отдельная 1D модель. """ from pathlib import Path import numpy as np from scipy.integrate import quad import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt def publication_layout(fig): """Readable labels at a 600 px article width; retain data and panel order.""" import textwrap from matplotlib.text import Text width, height = fig.get_size_inches() fig.set_size_inches(width, height * 1.22, forward=True) scale = width / 8.0 main, small = 14.0 * scale, 12.0 * scale for text in fig.findobj(Text): if text.get_text().strip(): text.set_fontsize(max(text.get_fontsize(), main)) if text.get_color() in ['#d77b27', '#ed8b36', '#df7b28', '#d77527']: text.set_color('#87470f') for ax in fig.axes: for tick in ax.get_xticklabels() + ax.get_yticklabels(): tick.set_fontsize(small) for label in [ax.xaxis.label, ax.yaxis.label]: txt = label.get_text() if '$' not in txt: label.set_text('\n'.join(textwrap.fill(s, 39) for s in txt.splitlines())) title = ax.get_title() if '$' not in title: columns = max(25, int(ax.get_position().width * 75)) ax.set_title('\n'.join(textwrap.fill(s, columns) for s in title.splitlines()), fontsize=main, pad=12) legend = ax.get_legend() if legend: for text in legend.get_texts(): text.set_fontsize(small) for line in ax.lines: line.set_linewidth(max(line.get_linewidth(), 1.8)) if fig._suptitle: txt = fig._suptitle.get_text() if '$' not in txt: fig._suptitle.set_text(textwrap.fill(txt, 57)) fig._suptitle.set_fontsize(max(main + 2, 18)) for text in fig.texts: if text is fig._suptitle: continue txt=text.get_text() if '$' not in txt: text.set_text('\n'.join(textwrap.fill(s, 68) for s in txt.splitlines())) if type(fig.get_layout_engine()).__name__ == 'PlaceHolderLayoutEngine': fig.tight_layout(rect=(.01,.10,.99,.91),h_pad=2,w_pad=2) if type(fig.get_layout_engine()).__name__ == 'ConstrainedLayoutEngine': fig.get_layout_engine().set(h_pad=.15, w_pad=.12) OUT=Path(__file__).resolve().parent plt.rcParams.update({'font.family':'DejaVu Sans','font.size':12}) fig,axes=plt.subplots(1,3,figsize=(11,3.9),layout='constrained') for L,ax in enumerate(axes): positions=np.linspace(-.6,.6,L+1) if L else [0] for x in positions: ax.plot(x,0,'o',color='#1e678a',ms=8) for x,y in zip(positions[:-1],positions[1:]): t=np.linspace(0,np.pi,100) for sign in [-1,1]: ax.plot((x+y)/2+(y-x)/2*np.cos(t),sign*.3*np.sin(t),color='#1e678a',lw=2) for x,side in [(positions[0],-1),(positions[-1],1)]: for sign in [-1,1]: ax.plot([x,x+side*.35],[0,sign*.55],color='#1e678a',lw=2) ax.text(0,-.93,f'I={2*L}, V={L+1}\nL=I−V+1={L}',ha='center',va='center') ax.set(xlim=(-1.1,1.1),ylim=(-1.25,.95),title=['0 петель','1 петля','2 петли'][L]);ax.axis('off') axes[1].text(0,.4,'k',ha='center');axes[1].text(0,-.49,'p−k',ha='center') publication_layout(fig); fig.savefig(OUT/'loops_reading.png',dpi=150, bbox_inches="tight");plt.close(fig) # Для μ>0 интеграл сходится. Общие множители вершин не включены. def bubble(p,mu=1): integrand=lambda k:1/((k*k+mu*mu)*((p-k)**2+mu*mu))/(2*np.pi) return quad(integrand,-np.inf,np.inf,epsabs=1e-11)[0] for p in [0,1,2,5]: exact=1/(1*(p*p+4)) assert abs(bubble(p)-exact)<1e-10 print('p=',p,'I(p)=',bubble(p),'exact=',exact)
5. Разбираем мой старый Mathematica notebook
Файл operator.nb создан в Mathematica 7.
Сначала договоримся о единицах.
Длины измеряются в , энергии уровней Ландау — в
, импульсы — в
. После этого переменные внутри функций безразмерны.
Кулоновское ядро VV ниже оставлено в собственной нормировке архива, с числителем : его численное значение не следует автоматически считать энергией в тех же единицах. Буквой
в коде обозначен номер последнего заполненного неотрицательного уровня, а
— верхняя граница суммы.
В исходнике эта граница называется max и равна 15.
Число 15 ограничивает индексы уровней в суммах, а не количество петель.
Отрицательная ветвь и нулевой уровень учитываются отдельно.
От факториалов к волновой функции
Первая функция в notebook:
ff[n1_, n2_] = (Min[n1,n2]! / Max[n1,n2]!)^0.5
Она задаёт нормировочный множитель.
Если обозначить и
, это
.
Следующее определение fg добавляет зависимость от координаты:
Это радиальная часть осцилляторной функции.
Полная функция также имеет свою угловую фазу.
Экспонента подавляет большие расстояния.
Степень задаёт поведение около центра.
Полином Лагерра даёт узлы и структуру возбуждённых состояний.
Факториалы и обеспечивают нормировку.
Поэтому
Для этих состояний можно проверить
Множитель появился из площади тонкого кольца:
. Его потеря даст гладкую, но неправильно нормированную кривую.
Гамма-функция продолжает факториал за пределы целых аргументов: при
она определяется интегралом
,
а .
В Python вместо огромных факториалов удобнее использовать её логарифм:
.
Эта та же формула, но вычисляется более устойчиво на компьютере.
import numpy as np from scipy.special import eval_genlaguerre, gammaln def fg(n1, n2, r): if any(isinstance(n, (bool, np.bool_)) or not isinstance(n, (int, np.integer)) or n < 0 for n in (n1, n2)): raise ValueError("n1 and n2 must be nonnegative integers") a, b = min(n1, n2), max(n1, n2) if np.iscomplexobj(r): raise ValueError("r must be real") r = np.asarray(r, dtype=float) if np.any(~np.isfinite(r)) or np.any(r < 0): raise ValueError("r must be finite and nonnegative") return (np.exp((gammaln(a+1)-gammaln(b+1))/2-r*r/4) * (r/np.sqrt(2))**(b-a) * eval_genlaguerre(a, b-a, r*r/2) / np.sqrt(2*np.pi))
Здесь индексы принимают неотрицательные значения. В полном скрипте отсутствующие компоненты с отрицательным осцилляторным индексом отдельно обращаются в ноль.
Формфактор: как структура состояния отвечает на пространственную волну
Пусть внешнее воздействие меняется в пространстве как .
Для распределённого объекта разные его части получают разные фазы.
Простейший пример формфактора в том, чтобы просто суммировать эти вклады:
При все фазы одинаковы и
.
При короткой длине волны они частично гасят друг друга.
Для гауссова профиля с центром в нуле и дисперсией получаем
. Если тот же вес поровну распределён между двумя гауссовыми облаками с центрами
, то
: возникают нули интерференции.
Знак и фаза несут информацию; интенсивность
её частично теряет.
Откуда эти две формулы?
Для нормированного гауссова профиля
.
Дифференцируем по
и интегрируем по частям:
Граничный член исчезает из-за гауссова затухания.
Решение этого уравнения с —
.
Сдвиг профиля на даёт множитель
после замены
.
Поэтому среднее двух одинаковых профилей в имеет множитель
.
Так интерференционный косинус получается непосредственно из двух сдвигов.

Большой объект быстрее замечает изменение фазы волны. Два центра дают дополнительные взаимные гашения. Кривые справа вычислены из профилей слева и проверены независимым интегрированием.
Можно обобщить формулу на переход между двумя квантовыми состояниями. Получается
.
Это тот самый матричный элемент плотности, который входит в пузырёк отклика.
Диагональный случай сводится к преобразованию плотности , а внедиагональный содержит
.
Из ортонормированности следует
,
из сопряжения — .
Обе связи полезны как контрольные тесты для проверки правильности работы программы.
Здесь — волновое число с размерностью обратной длины; физический переданный импульс равен
. В формулах отклика ниже
внутри
также означает волновой вектор; в безразмерном коде это
, численно равное импульсу в единицах
.
Для радиального состояния связь особенно наглядна. Здесь пара индексов задаёт одно осцилляторное состояние, а не начальное и конечное состояния перехода.
Назовём всю эту пару и обозначим диагональный формфактор иначе:
Это не противоречит : в первом случае берётся плотность одного нормированного состояния, во втором — перекрытие двух состояний.

Верхняя панель использует двумерные радиальные функции диплома: ,
. Нижняя — отдельный одномерный осциллятор в безразмерных единицах:
и
. Показана мнимая часть последнего. Оба диагональных формфактора наверху дают 1 при нуле; переход между разными состояниями внизу даёт 0. Разные гауссовы масштабы относятся к двум явно указанным примерам.
Внутренний интеграл раздела 7 равен в принятой там нормировке. Так специальная функция под двойным интегралом становится откликом пространственного состояния на заданную гармонику.
В физике амплитуд термин “матричный элемент” используется гораздо шире: например,
—
матричный элемент локального оператора между вакуумным состоянием и состоянием частиц с указанными импульсами.
Оператор вносит суммарный импульс .
Это иной объект, чем амплитуда рассеяния без такой вставки, и иной конкретный формфактор, чем плотностный в графене.
Общая идея — матричный элемент заданного воздействия.
Хороший обзор с определениями и примерами можно найти тут: Gang Yang, «On-shell Methods for Form Factors…».
Почему в поляризации есть сумма, разность и энергетический знаменатель
Рассмотрим переход с занятого состояния на свободное
. Вес такого перехода в спектральном отклике содержит квадрат модуля матричного элемента воздействия.
Сам этот отклик — не вероятность: он может быть отрицательным или комплексным.
Что же означает «спектральный запаздывающий отклик плотности»?
Разберём каждое слово.
Плотность — число электронов на единицу площади.
Слабое внешнее воздействие здесь обозначает потенциальную энергию электрона во внешнем поле. Оно слегка изменяет распределение плотности заряда в пространстве. В линейном приближении изменение пропорционально воздействию:
Индекс выделяет пространственную гармонику,
— частоту.
Например, воздействие создаёт бегущую модуляцию, и коэффициент отклика показывает, насколько сильно и с какой задержкой система отвечает на эту модуляцию.
Если система стационарна и однородна, линейный ответ не смешивает разные частоты и волновые векторы. Поэтому преобразование Фурье превращает интегральную связь в произведение для каждой гармоники.
В неоднородной системе нужен более общий ответ с двумя пространственными аргументами.
Запаздывающий, или retarded, означает причинный: изменение в момент зависит только от более раннего воздействия. До толчка отклик на этот толчок равен нулю.
В квантовой записи это обеспечивается ступенькой в формуле Кубо (здесь схема для одной гармоники, с нормировкой плотности на площадь):
Квадратные скобки — коммутатор , угловые — среднее по исходному равновесному состоянию.
Чтобы за формулой не пряталась нормировка, обозначим площадь большого образца через , пренебрежем краевыми эффектами. Определим
, где
— число электронов, а
нумерует их координаты. Это Фурье-компонента оператора числа электронов, без множителя заряда. Коэффициент плотности равен
; поэтому в формуле Кубо стоит
. Внешнюю потенциальную энергию раскладываем по тем же пространственным гармоникам. Разность двух порядков операторов выражает изменение наблюдаемой величины под воздействием. Эту формулу получают, разложив оператор эволюции до первого порядка по внешнему полю — тот же принцип «сначала решённая задача, затем малая добавка».
Спектральная запись означает, что мы вставили между операторами полный набор собственных состояний энергии: . Эволюция каждого матричного элемента даёт фазу
. Интеграл по положительным временам с малым затуханием превращает фазу в знаменатель дроби, состоящий из суммы энергий:
Предел обычно пишут как
. Для иллюстрации можно оставить маленькую ширину, но ее конечное значение дополнительно уширяет спектр. Для независимых электронов оценка коммутатора даёт разность заполнений
, а два матричных элемента плотности — квадрат модуля. Так приходим к следующей формуле. Для взаимодействующей системы точная спектральная сумма для
идёт по многочастичным состояниям. Если среднее и эволюцию вычислять для независимых электронов, получаем
: это и есть пузырёк, используемый в RPA.

На рисунке взята пара уровней с разностью энергий 1 из учебного примера и единичным матричным элементом, . Слева отклик нулевой до
. Справа пик при
означает совпадение частоты воздействия с энергетической ценой перехода. Ширина
выбрана для видимости; это не измеренное время жизни графенового возбуждения.
В спектральной записи пузырёк независимых электронов имеет следующую структуру
Здесь пробегают все нормированные одночастичные состояния, включая центр орбиты, спин и долину, когда они присутствуют. Если эти внутренние индексы уже входят в сумму, отдельный множитель вырождения добавлять нельзя. Если одинаковые компоненты вынесены из суммы, их число учитывается отдельно. Размерность
при этой нормировке — обратные энергия и площадь.
— средние числа заполнения состояний. Если оба состояния одинаково заполнены, разность равна нулю. Малое
выбирает причинный отклик: система отвечает после воздействия. Это правило аналитического продолжения, а не параметр для подгонки графика.
В notebook вся эта общая структура разобрана на три семейства. Для положительных индексов введены
и
Они соответствуют функциям PP, PM, PO. Сумма и разность двух осцилляторных компонентов связаны с двухкомпонентными графеновыми состояниями и знаками ветвей спектра. Для перехода между положительными уровнями возникает разность положительных энергий; для перехода через отрицательную и положительную ветви — их сумма. Нулевой уровень выделен отдельно, поэтому у PO другой множитель нормировки.
Итоговое определение Sigma1 буквально имеет вид
В первой сумме
, поэтому деления на ноль при равных уровнях нет. При
вся первая сумма пуста. Вторая сумма учитывает переходы, связанные с отрицательной ветвью; третья — вклад нулевого уровня. Обозначение
здесь лишь короткое имя функции
Sigma1.

Слева нарисована именно радиальная плотность : площадь под каждой кривой равна единице. Справа — результат старой формулы
Sigma1 при четырёх заполнениях и общей границе .
6. Восстановить код ещё не значит подтвердить физику
В исходном notebook сохранены не только определения, но и результаты некоторых вызовов функций. Это позволяет проверить перенос независимо от визуального сходства графиков.
Проверяемый вызов |
Сохранённый ответ Mathematica |
Python |
|---|---|---|
|
−0.020216169102072835 |
−0.020216169102072835 |
|
1.0290829678673687 |
1.029082967867369 |
|
0.2288012805693556 |
0.22880128056935545 |
|
0.11974542810169851 |
0.11974542810169861 |
Последние различия — на уровне округления чисел с плавающей точкой. Но пока проверено только то, что новый код считает те же самые выражения. Ошибка исходной формулы вполне могла бы перенестись вместе с ней.
Первое расхождение: коэффициент экранирования
В notebook записано:
VV[q_, epsilon_, m_, max_] = 2*Pi / (q - 8*Sigma1[q,m,max]*Pi/(2.19*epsilon))
То есть буквально
На странице 24 диплома приведено
При отождествлении использованной здесь с
Sigma1 это разные коэффициенты: и
. Отличие почти в
раза.
Для окончательного исправления нужно восстановить все нормировки и обозначения из первоначального расчёта. Выбрать один вариант только потому, что его график нравится больше, было бы подгонкой.
Поэтому скрипт содержит две явно названные функции: VV_archive воспроизводит notebook, VV_text показывает вторую запись при одинаковой нормировке . Старые контрольные числа сравниваются с первой, а различие двух вариантов вынесено на график.
Второе расхождение: единицы и размерность
Ошибку в записи единицы энергии на странице 23 мы уже разобрали в начале статьи; здесь используется масштаб . Есть и другая несогласованность: на странице 24 в координатном матричном элементе также встречается
, хотя далее вычисляется двумерный радиальный интеграл. В статье эта мера записана как
.
Проверка размерностей помогает быстрее, чем запуск сложного расчёта.
Третья проверка: достаточно ли пятнадцати уровней
Можно написать сумму хоть до бесконечности, но компьютер всё равно должен получить задачу с конечным количеством вычислений. В notebook оставлены уровни до .
10 |
−0.02617231 |
15 |
−0.02659943 |
30 |
−0.02714981 |
60 |
−0.02753810 |
Результат меняется. Особенно хорошо это видно при больших :

Следовательно, совпадение четырёх контрольных значений подтверждает перенос программы, но не достаточность обрезания во всём диапазоне. Правая картинка демонстрирует другую причину расхождения — коэффициент в формуле. У этих двух неопределённостей разные источники, и проверять их нужно раздельно.
7. От экранированного взаимодействия к двойному интегралу
Осталось связать вычисленное с поправкой для конкретного состояния пары. Возьмём один радиальный компонент
. Взаимодействие сдвигается относительно этого состояния на величину, связанную с магнитным импульсом пары. В безразмерных переменных модуль сдвига равен
.
Переход в импульсное представление особенно удобен для взаимодействия: там экранирование уже записано простой дробью. После угловых интегралов в радиальном матричном элементе появляются функции Бесселя:
Это формула в нормировке радиальной функции notebook. Два множителя и
— якобианы полярных координат. Их нельзя удалить при переносе на прямоугольную сетку.
Откуда здесь ? Это результат усреднения плоской волны по направлению:
Векторный интеграл сворачивается в радиальный, но информация об углах сохраняется в специальной функции. При имеем
. При ненулевом
этот множитель осциллирует, и некоторые вклады взаимно сокращаются.
В notebook есть и попытка определить эту функцию самостоятельно, через ряд BS.
Начало ряда — . В архиве вызов численного
NSum с ещё символическим аргументом сопровождался сообщениями nsnum. В Python для вычисления значений используется готовая scipy.special.j0.
Теперь можно прочитать последнюю важную строку notebook:
II[q_,r_,m1_,m2_,P_] = fg[m1,m2,r]^2 * BesselJ[0,q*P] * BesselJ[0,q*r] * q*r * VV[q,1,m,max]
II — подынтегральная функция. В ней q — передаваемый внутренний импульс, P — внешний магнитный импульс пары, r — радиальная координата. Эти три аргумента выполняют разные роли.
Вызов NIntegrate использовал границы от 0.01 до 10 по обеим переменным и AccuracyGoal -> 3. Таким образом, были сделаны сразу два приближения: обрезание бесконечных интервалов и численное интегрирование на конечном прямоугольнике. Заданная точность алгоритма относится ко второй задаче. Она не оценивает автоматически отброшенные хвосты и участок около нуля.
Что значит «посчитать интеграл квадратурой»? Квадратурная формула заменяет интеграл взвешенной суммой . В квадратуре Гаусса узлы
выбираются специальным образом, а не через равные расстояния. Для веса 1 на
это нули полинома Лежандра степени
— полинома, ортогонального всем меньшим степеням относительно интеграла на этом отрезке. Узлы и веса обеспечивают точность для любого полинома степени до
включительно, если считать в точной арифметике.

При узлы
и веса
точно интегрируют
: сумма равна
. Произвольная осциллирующая функция этим обещанием не покрывается. На другом отрезке нужны сдвиг и масштаб узлов, а веса умножаются на
. В двойном интеграле используются пары узлов и произведения весов.
Для гладкого интеграла увеличение числа узлов часто улучшает результат, но не исправляет неверную меру и не возвращает обрезанные хвосты. Поэтому в нашей проверке число узлов и границы интегрирования — два независимых параметра опыта.
Для компонента ,
сохранился ответ
0.5262355639062104. Пересчёт той же подынтегральной функции на том же прямоугольнике квадратурой Гаусса даёт 0.5258440773. Увеличение числа узлов с 350 до 650 по каждой координате меняет результат примерно на ; отличие от старого ответа составляет примерно
.
Полный код восстановления notebook и проверок: 06_graphene.py
"""Восстановление формул operator.nb (Mathematica 7) и учебные проверки. python 06_graphene.py Единицы: длины l_B, энергии hbar*v_F/l_B. N — обрезание уровней, не петли. VV_archive буквально повторяет notebook, включая 1/(2.19*eps). Эта конвенция отличается от r_s=2.19/eps в тексте диплома. """ from pathlib import Path import json import numpy as np from scipy.special import eval_genlaguerre, gammaln, j0, roots_legendre from scipy.integrate import quad import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt def publication_layout(fig): """Readable labels at a 600 px article width; retain data and panel order.""" import textwrap from matplotlib.text import Text width, height = fig.get_size_inches() fig.set_size_inches(width, height * 1.22, forward=True) scale = width / 8.0 main, small = 14.0 * scale, 12.0 * scale for text in fig.findobj(Text): if text.get_text().strip(): text.set_fontsize(max(text.get_fontsize(), main)) if text.get_color() in ['#d77b27', '#ed8b36', '#df7b28', '#d77527']: text.set_color('#87470f') for ax in fig.axes: for tick in ax.get_xticklabels() + ax.get_yticklabels(): tick.set_fontsize(small) for label in [ax.xaxis.label, ax.yaxis.label]: txt = label.get_text() if '$' not in txt: label.set_text('\n'.join(textwrap.fill(s, 39) for s in txt.splitlines())) title = ax.get_title() if '$' not in title: columns = max(25, int(ax.get_position().width * 75)) ax.set_title('\n'.join(textwrap.fill(s, columns) for s in title.splitlines()), fontsize=main, pad=12) legend = ax.get_legend() if legend: for text in legend.get_texts(): text.set_fontsize(small) for line in ax.lines: line.set_linewidth(max(line.get_linewidth(), 1.8)) if fig._suptitle: txt = fig._suptitle.get_text() if '$' not in txt: fig._suptitle.set_text(textwrap.fill(txt, 57)) fig._suptitle.set_fontsize(max(main + 2, 18)) for text in fig.texts: if text is fig._suptitle: continue txt=text.get_text() if '$' not in txt: text.set_text('\n'.join(textwrap.fill(s, 68) for s in txt.splitlines())) if type(fig.get_layout_engine()).__name__ == 'PlaceHolderLayoutEngine': fig.tight_layout(rect=(.01,.10,.99,.91),h_pad=2,w_pad=2) if type(fig.get_layout_engine()).__name__ == 'ConstrainedLayoutEngine': fig.get_layout_engine().set(h_pad=.15, w_pad=.12) from matplotlib.animation import FuncAnimation, PillowWriter OUT=Path(__file__).resolve().parent plt.rcParams.update({'font.family':'DejaVu Sans','font.size':11, 'axes.spines.top':False,'axes.spines.right':False}) def fg(a,b,r): if min(a,b)<0: return np.zeros_like(r,dtype=float) r=np.asarray(r,dtype=float) lo,hi=min(a,b),max(a,b) return (np.exp((gammaln(lo+1)-gammaln(hi+1))/2-r*r/4) *(r/np.sqrt(2))**(hi-lo) *eval_genlaguerre(lo,hi-lo,r*r/2)/np.sqrt(2*np.pi)) def EE(n): return np.sqrt(2*n) def PP(a,b,q): return (fg(a,b,q)+fg(a-1,b-1,q))**2/(4*(EE(b)-EE(a))) def PM(a,b,q): return -(fg(a,b,q)-fg(a-1,b-1,q))**2/(4*(EE(b)+EE(a))) def PO(a,q): return -fg(a,0,q)**2/(2*EE(a)) def sigma(q,m=0,N=15): if not 0<=m<N: raise ValueError('Require 0 <= m < N') ans=np.zeros_like(q,dtype=float) for a in range(m+1,N+1): for b in range(1,m+1): ans+=PP(a,b,q) for b in range(1,N+1): ans+=PM(a,b,q) ans+=PO(a,q) return ans def VV_archive(q,eps=1,m=0,N=15): return 2*np.pi/(q-8*np.pi*sigma(q,m,N)/(2.19*eps)) def VV_text(q,eps=1,m=0,N=15): # Отдельная ветка по формуле r_s=2.19/eps на стр. 24 диплома. return 2*np.pi/(q-8*np.pi*sigma(q,m,N)*(2.19/eps)) def II(q,r,a,b,P): return fg(a,b,r)**2*j0(q*P)*j0(q*r)*q*r*VV_archive(q) def radial_component(a,b,P=0,qmin=.01,qmax=10,points=500): # Сначала численно интегрируем r на исходном интервале [.01,10]. nodes,weights=roots_legendre(points) q=qmin+(nodes+1)*(qmax-qmin)/2; wq=weights*(qmax-qmin)/2 r=.01+(nodes+1)*9.99/2; wr=weights*9.99/2 hankel=j0(q[:,None]*r[None,:])@(wr*r*fg(a,b,r)**2) return np.sum(wq*q*VV_archive(q)*j0(q*P)*hankel) def main(): # Сохранённые Output-ячейки: независимые опорные значения из .nb. checks={ 'fg(1,2,5)':(float(fg(1,2,5)),-.020216169102072835), 'VV(5)':(float(VV_archive(5)),1.0290829678673687), 'II(.1,1,3,5,0)':(float(II(.1,1,3,5,0)),.2288012805693556), 'II(1,1,2,3,1)':(float(II(1,1,2,3,1)),.11974542810169851), } for name,(got,expected) in checks.items(): print(name,got,'archive:',expected) assert np.isclose(got,expected,rtol=2e-11,atol=1e-13),name for a,b in [(0,0),(1,0),(1,1),(3,5)]: norm=2*np.pi*quad(lambda r:r*fg(a,b,r)**2,0,np.inf)[0] assert abs(norm-1)<1e-10 fine=radial_component(5,5,points=650) coarse=radial_component(5,5,points=350) print('Double integral 5,5:',fine,'grid change:',abs(fine-coarse), 'old NIntegrate:',.5262355639062104) assert abs(fine-coarse)<1e-9 # Старое AccuracyGoal=3 не обещает совпадения в 15 знаках. assert abs(fine-.5262355639062104)<1e-3 x=np.linspace(0,8,321) assert np.max(sigma(x,0,15))<=1e-13 assert abs(float(sigma(0,0,15)))<1e-14 report={'archive_checks':checks,'double_integral':fine, 'quadrature_grid_change':abs(fine-coarse), 'sigma_q1_by_cutoff':{str(n):float(sigma(1,0,n)) for n in [10,15,30,60]}} print('sigma(q=1):',report['sigma_q1_by_cutoff']) (OUT/'graphene_validation.json').write_text(json.dumps(report,indent=2),encoding='utf8') fig,axes=plt.subplots(1,2,figsize=(11,4.8),layout='constrained') r=np.linspace(0,5,400) for a,b in [(0,0),(1,0),(1,1)]: axes[0].plot(r,2*np.pi*r*fg(a,b,r)**2,label=f'a={a}, b={b}') axes[0].set(xlabel='r / lB',ylabel='Радиальная плотность вероятности',title='Нормировка: площадь под кривой = 1') axes[0].legend() for m in range(4): axes[1].plot(x,sigma(x,m,15),label=f'm={m}') axes[1].set(xlabel='q (безразмерный)',ylabel='Σ(q), конвенция notebook',title='Поляризация при N=15') axes[1].legend(); publication_layout(fig); fig.savefig(OUT/'graphene_functions.png',dpi=145, bbox_inches="tight"); plt.close(fig) fig,axes=plt.subplots(1,2,figsize=(11,4.8),layout='constrained') for n in [10,15,30,60]: axes[0].plot(x,sigma(x,0,n),label=f'N={n}') axes[0].set(xlabel='q',ylabel='Σ(q)',title='Обрезание по уровням меняет ответ');axes[0].legend() q=x[1:] axes[1].plot(q,q*VV_archive(q)/(2*np.pi),label='Буквально из notebook') axes[1].plot(q,q*VV_text(q)/(2*np.pi),label='С rₛ=2.19/ε из текста') axes[1].axhline(1,color='gray',ls=':',label='Без экранирования') axes[1].set(xlabel='q',ylabel='Vэфф / V₀',title='Две разные конвенции, ε=1, N=15');axes[1].legend(fontsize=9) publication_layout(fig); fig.savefig(OUT/'graphene_checks.png',dpi=145, bbox_inches="tight");plt.close(fig) # Простейшая иллюстрация: один скалярный канал RPA. s=np.linspace(0,.9,300); fig,ax=plt.subplots(figsize=(8,4.8),layout='constrained') ax.plot(s,1/(1+s),color='black',lw=2,label='RPA: 1/(1+s)') line,=ax.plot([],[],color='#237da6',lw=2,label='Частичная сумма') ax.set(xlabel='s = −V₀Π ≥ 0',ylabel='Vэфф / V₀',ylim=(0,1.9)) title=ax.set_title('');ax.legend() def update(k): line.set_data(s,sum((-s)**j for j in range(k+1))) title.set_text(f'Цепочка пузырьков: от 0 до {k} вставок') return line,title update(0);publication_layout(fig) ani=FuncAnimation(fig,update,frames=range(10),interval=650) ani.save(OUT/'rpa.gif',writer=PillowWriter(fps=1.5),dpi=110) update(3);fig.savefig(OUT/'rpa.png',dpi=145, bbox_inches="tight");plt.close(fig) if __name__=='__main__': main()
8. Что действительно сохранилось в Excel
В сохранившемся .xls нет встроенных объектов диаграмм. Есть две заполненные вкладки с численными таблицами и третья пустая. Поэтому следующие картинки построены заново по сохранённым числам, без пересчёта или сглаживания самих данных.
На Лист2 записаны четыре серии V01, V02, V11, V12 при m=0. Аргумент таблицы называется q; по контексту матричных элементов он должен сопоставляться с внешним импульсом пары , но в подписи графика я сохраняю исходное обозначение. Единица величины
V в самой таблице не подписана, поэтому ось не объявлена энергией в электронвольтах.

По этим числам можно уверенно сказать следующее. При нулевом аргументе серии различаются: от 0.494 до 0.792. V02 сначала возрастает и среди сохранённых точек достигает 0.5515 при аргументе 1.5, затем убывает. При аргументе 10 все четыре значения находятся между 0.0905 и 0.091. Точки соединены отрезками: между ними новых результатов не вычислялось.
На Лист1 сохранены матрицы для трёх значений аргумента. Например, при нуле:

Есть небольшая внутренняя несогласованность архива. Для V01 при аргументе 1 в матрице на Лист1 стоит 0.723, а в серии на Лист2 — 0.724. Для V02 там же — 0.540 и 0.541. Причина в файлах не пояснена: это могли быть разные запуски, округление либо перенос чисел. В графике серий используются числа Лист2, в тепловой карте — числа Лист1.
Я не стал «согласовывать» таблицы изменением исходных значений. Для учебного разбора расхождение полезнее спрятанного исправления. Оно показывает, какие сведения должен сохранять воспроизводимый расчёт: версию формулы, параметры, границы сумм и интегралов, точность и соответствие столбцов физическим величинам.
Полный код графиков из исходных чисел Excel: 07_excel.py
"""Графики исходных чисел Excel. Нового физического расчёта здесь нет. Источник: Лист Microsoft Excel.xls, Лист2!B2:F29; матрица Лист1!C23:G28. Ось q оставлена как в архиве; по смыслу матричного элемента это P. Единица V в таблице не подписана: не приписываем данным эВ. Числа включены в скрипт, поэтому исходный .xls для запуска не нужен. """ from pathlib import Path import numpy as np import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt def publication_layout(fig): """Readable labels at a 600 px article width; retain data and panel order.""" import textwrap from matplotlib.text import Text width, height = fig.get_size_inches() fig.set_size_inches(width, height * 1.22, forward=True) scale = width / 8.0 main, small = 14.0 * scale, 12.0 * scale for text in fig.findobj(Text): if text.get_text().strip(): text.set_fontsize(max(text.get_fontsize(), main)) if text.get_color() in ['#d77b27', '#ed8b36', '#df7b28', '#d77527']: text.set_color('#87470f') for ax in fig.axes: for tick in ax.get_xticklabels() + ax.get_yticklabels(): tick.set_fontsize(small) for label in [ax.xaxis.label, ax.yaxis.label]: txt = label.get_text() if '$' not in txt: label.set_text('\n'.join(textwrap.fill(s, 39) for s in txt.splitlines())) title = ax.get_title() if '$' not in title: columns = max(25, int(ax.get_position().width * 75)) ax.set_title('\n'.join(textwrap.fill(s, columns) for s in title.splitlines()), fontsize=main, pad=12) legend = ax.get_legend() if legend: for text in legend.get_texts(): text.set_fontsize(small) for line in ax.lines: line.set_linewidth(max(line.get_linewidth(), 1.8)) if fig._suptitle: txt = fig._suptitle.get_text() if '$' not in txt: fig._suptitle.set_text(textwrap.fill(txt, 57)) fig._suptitle.set_fontsize(max(main + 2, 18)) for text in fig.texts: if text is fig._suptitle: continue txt=text.get_text() if '$' not in txt: text.set_text('\n'.join(textwrap.fill(s, 68) for s in txt.splitlines())) if type(fig.get_layout_engine()).__name__ == 'PlaceHolderLayoutEngine': fig.tight_layout(rect=(.01,.10,.99,.91),h_pad=2,w_pad=2) if type(fig.get_layout_engine()).__name__ == 'ConstrainedLayoutEngine': fig.get_layout_engine().set(h_pad=.15, w_pad=.12) OUT=Path(__file__).resolve().parent DATA=np.array([[0.0, 0.792, 0.494, 0.731, 0.563], [0.25, 0.788, 0.498, 0.724, 0.561], [0.5, 0.776, 0.51, 0.706, 0.556], [0.75, 0.762, 0.533, 0.6865, 0.5495], [1.0, 0.724, 0.541, 0.648, 0.532], [1.25, 0.6825, 0.551, 0.6145, 0.516], [1.5, 0.6335, 0.5515, 0.58, 0.499], [1.75, 0.58, 0.5395, 0.5445, 0.482], [2.0, 0.525, 0.516, 0.5075, 0.465], [2.25, 0.4725, 0.484, 0.469, 0.447], [2.5, 0.425, 0.446, 0.431, 0.4255], [2.75, 0.383, 0.407, 0.393, 0.4], [3.0, 0.347, 0.37, 0.358, 0.373], [3.25, 0.3165, 0.336, 0.327, 0.344], [3.5, 0.2905, 0.3065, 0.299, 0.316], [3.75, 0.2685, 0.281, 0.2755, 0.29], [4.0, 0.2495, 0.2595, 0.255, 0.267], [4.25, 0.233, 0.2415, 0.237, 0.2475], [4.5, 0.219, 0.2255, 0.222, 0.23], [4.75, 0.2065, 0.212, 0.209, 0.216], [5.0, 0.195, 0.1995, 0.1975, 0.2025], [6.0, 0.1595, 0.162, 0.161, 0.164], [7.0, 0.1345, 0.136, 0.135, 0.137], [8.0, 0.1165, 0.117, 0.117, 0.118], [9.0, 0.102, 0.1025, 0.102, 0.103], [10.0, 0.0905, 0.091, 0.091, 0.091], [20.0, 0.04, 0.04, 0.04, 0.04], [30.0, 0.0235, 0.0225, 0.0235, 0.023]]) MATRIX=np.array([[0.792, 0.494, 0.4, 0.3455, 0.308], [0.731, 0.563, 0.418, 0.3555, 0.315], [0.563, 0.6045, 0.495, 0.382, 0.332], [0.418, 0.495, 0.537, 0.4515, 0.3565], [0.3555, 0.382, 0.4515, 0.493, 0.42], [0.315, 0.332, 0.3565, 0.42, 0.46]]) plt.rcParams.update({'font.family':'DejaVu Sans','font.size':11}) fig,axes=plt.subplots(1,2,figsize=(11,4.8),layout='constrained') for i,label in enumerate(['V01','V02','V11','V12'],1): axes[0].plot(DATA[:,0],DATA[:,i],'.-',label=label) axes[1].plot(DATA[:,0],DATA[:,i],'.-',label=label) for ax in axes: ax.set(xlabel='q — обозначение исходной таблицы',ylabel='V, единицы исходной таблицы') ax.grid(alpha=.2) axes[0].set_xlim(0,5);axes[0].set_title('Различия при малом импульсе');axes[0].legend() axes[1].set_title('Все сохранённые точки');axes[1].legend() publication_layout(fig); fig.savefig(OUT/'excel_curves.png',dpi=145, bbox_inches="tight");plt.close(fig) fig,ax=plt.subplots(figsize=(7,5),layout='constrained') im=ax.imshow(MATRIX,cmap='viridis',vmin=0,vmax=.8,aspect='auto') for i,row in enumerate(MATRIX): for j,v in enumerate(row): ax.text(j,i,f'{v:g}',ha='center',va='center',color='white' if v<.5 else 'black') ax.set(xticks=range(5),xticklabels=range(1,6),yticks=range(6),yticklabels=[0,-1,-2,-3,-4,-5],xlabel='n₁',ylabel='n₂',title='Сохранённые матричные элементы при q=0, m=0') fig.colorbar(im,ax=ax,label='V, единицы исходной таблицы') publication_layout(fig); fig.savefig(OUT/'excel_matrix.png',dpi=145, bbox_inches="tight");plt.close(fig) assert DATA.shape==(28,5) and MATRIX.shape==(6,5) print('Plotted 112 curve values and 30 matrix values, unchanged.')
9. Почему мой диплом куда проще девятипетлевой задачи
В восстановленной части диплома уже заданы спектр, функции состояний, правила суммирования и приближение экранирования. После выбора параметров остаются конечные суммы и значения вычисленных интегралов.
Высокопетлевая амплитуда же — это функция кинематических переменных.
Кинематика описывает допустимые энергии и импульсы внешних состояний; динамика — то, с какой амплитудой выбранная теория связывает эти состояния.
Аналитическая структура функции включает, в частности, её особые точки и правила продолжения между областями аргументов. Требуется определить её аналитическую структуру так, чтобы она работала для целого семейства допустимых состояний, согласовывалась с симметриями и предельными случаями.
Получить несколько чисел в нескольких точках тут недостаточно.
Что сравниваем |
Восстановленная задача диплома |
Девятипетлевая амплитуда |
|---|---|---|
Что задано |
Конкретные суммы, функции и радиальное ядро |
Теория, структура допустимого ответа и ограничения |
Что ищем |
Поляризацию и отдельные матричные элементы |
Аналитический объект на высоком порядке разложения |
Основная трудность нашего примера |
Нормировки, обрезания, численная устойчивость |
Размер пространств функций и систем ограничений |
Что можно проверить на ноутбуке |
Контрольные значения и небольшие интегралы |
Учебные аналоги и фрагмент публичных данных |
Есть и более тонкое отличие. Моя RPA-формула уже суммирует цепочки пузырьков всех длин, но только одного выбранного класса. Число петель в полном пертурбативном коэффициенте учитывает весь разрешённый набор вкладов данного порядка.
Как выглядят эти девять петель
В шестичастичной амплитуде остаются шесть внешних состояний, а число независимых внутренних импульсов растёт с порядком. Ниже нарисованы планарные шеститочечные скелеты с одной, двумя и девятью петлями. Картинка не утверждает, что именно этот скелет с единичным числителем даёт ненулевое отдельное слагаемое нужной амплитуды SYM.

У нижнего скелета 20 вершин и 28 внутренних рёбер, поэтому . Девять ячеек видны непосредственно. Внешние шесть лучей не входят в число 28.
Дуальные импульсные координаты: закон сохранения встроен в обозначения
Обычно мы назначаем каждой линии импульс, а затем следим, чтобы в каждой вершине сумма входящих равнялась сумме выходящих. Для плоского, или планарного, рисунка можно поступить наоборот: назначить переменные областям между линиями, а импульсы линий определять как разности соседних переменных. При обходе вершины разности телескопически сокращаются. Закон сохранения становится свойством способа записи.
Напомним геометрию, скрытую в квадрате импульса.
Четырёхимпульс объединяет энергию и трёхмерный импульс:
.
В пространстве Минковского при выбранном знаке метрики
.
Метрика задаёт именно это правило скалярного произведения.
Для безмассовой частицы при ненулевом
: такой вектор называют светоподобным.
В евклидовой геометрии сумма квадратов вещественных координат обращается в нуль только у нулевого вектора.

Наверху — световой конус в одной пространственной координате. На средней панели два пространственных импульса становятся параллельными; это коллинеарный предел. Внизу показано уже пространство комплексного аргумента функции, не пространство-время: главный логарифм имеет разные пределы с двух берегов отрицательной вещественной оси. Три картинки иллюстрируют разные понятия, которые далее встретятся в условиях на амплитуду.
Начнём с внешних импульсов. Считаем все исходящими, поэтому
; физически входящие частицы получают при такой записи отрицательные четырёхимпульсы. Выберем произвольное
и последовательно построим
Сохранение импульса означает замыкание многоугольника: . Общий сдвиг всех
ничего не меняет — физический смысл имеют разности. Буква
здесь обманчива: её размерность та же, что у импульса. Это не координата частицы, не траектория и не обычная сопряжённая импульсу координата преобразования Фурье.

Слева сумма стрелок равна нулю. Справа знаменатель линии определяется двумя соседними областями. Рисунок показывает связи и условную проекцию; для безмассовых внешних частиц стороны светоподобны в пространстве Минковского, а не являются такими евклидовыми отрезками в буквальном смысле.
Проверим на однопетлевом «ящике».
Его четыре знаменателя при одном выборе направления петлевого импульса :
Положим ,
,
,
, а переменную внутренней области
. Тогда каждый знаменатель равен
.
Никакой новой динамики не появилось: это та же формула, в которой легче увидеть связи и симметрии. Знак зависит от используемой метрики; в следующем учебном интеграле мы явно перейдём к евклидовому варианту с
.
История здесь длиннее одного изобретения.
Переменные областей — это универсальный способ маршрутизации импульсов планарных графов. Современная роль дуальной конформной симметрии в амплитудах SYM связана, в частности, с работами Драммонда, Хенна, Смирнова и Сокачёва (2006), Драммонда, Корчемского и Сокачёва (2007), затем Драммонда, Хенна, Корчемского и Сокачёва (2008).
Конформные преобразования сохраняют локальные углы; в данной задаче симметрия действует на вспомогательные , поэтому называется дуальной.
Из квадратов расстояний между ними можно собрать безразмерные перекрёстные отношения — например,
, где
.
Ни общий сдвиг, ни общий масштаб всех это отношение не меняет. Именно такие переменные удобны при построении ответа из ограничений в разделе 12.
Одно слагаемое: от трёхпетлевого рисунка к интегралу
Разберём более короткий рисунок, на котором ещё можно подписать каждую линию.
Применим введённые дуальные импульсные координаты: шесть внешних и три внутренних
. Сохраняем соглашение
,
, со всеми внешними импульсами, направленными наружу.

Сделаем математически простой пример: каждой линии сопоставим скалярный евклидов пропагатор , числитель положим равным единице. Это учебное ядро на данной топологии, не полная формула глюонной амплитуды. По подписям рисунка десять знаменателей равны
Одно соответствующее интегральное ядро имеет вид
Например, вертикальная линия между первой и второй ячейками даёт , потому что граничит с
и
. Верхняя линия второй ячейки даёт
, поскольку по разные стороны стоят
и
. Так можно проверить каждый множитель без догадок о форме интеграла. Закон сохранения в вершине получается телескопическим сокращением разностей областей.
В калибровочной теории над произведением знаменателей в общем случае стоит числитель с импульсами, поляризациями и внутренними индексами; добавляются коэффициенты и другие графы. Для девяти петель общая структура такая:
Это схематическая запись: может служить размерным регулятором, а конкретные соглашения о множителях
и связи включены в
. В реальном девятипетлевом результате работают с определённой нормированной конечной величиной после учёта универсальной инфракрасной структуры.
Зато теперь видно, что именно кодирует слово «петля»: ещё одну независимую векторную переменную интегрирования — 4-вектор в четырёхмерной теории.
10. Получим несколько полных коэффициентов в ещё более простой модели
До сих пор мы рисовали отдельные диаграммы и суммировали выбранную цепочку. Теперь хочется увидеть пример, где коэффициент порядка включает все разрешённые вклады, а правильность можно проверить исходным интегралом.
Уберём пространство и время совсем. Вместо поля в каждой точке оставим одну вещественную переменную . Получится нульмерная модель
:
Знаменатель нормирует вес, а числитель считает среднее .
Число 24 — это : традиционная нормировка четырёхлинейного взаимодействия, учитывающая перестановки одинаковых множителей. При
перед нами обычное гауссово распределение с дисперсией 1, поэтому
. При положительном
большие значения
сильнее подавлены, и среднее
должно уменьшаться. Уже есть качественная проверка знака первой поправки.

Поле в обычной евклидовой теории — это много переменных , связанных членом с производными. Функциональный интеграл суммирует по всем конфигурациям поля. Нульмерная версия оставляет одну переменную и обычный интеграл: сохраняются гауссов вес, взаимодействие и комбинаторика разложения.
Нуль измерений здесь не означает нуль петель.
На графике слева веса нормированы отдельным знаменателем для каждого . Рост связи подавляет хвосты распределения. Задача о среднем
измеряет, насколько широким осталось это распределение. В этой модели можно одновременно иметь точный численный ориентир и полный формальный ряд — удобная лаборатория для проверки метода.
Почему здесь по-прежнему можно говорить о диаграммах? Разложим взаимодействие:
Каждая степень добавляет четыре множителя — аналог четырёхлинейной вершины. Гауссовы моменты можно получать попарным соединением множителей. Именно эту комбинаторику кодируют диаграммы. От реальной теории поля здесь остался счёт соединений; интегралов по импульсам уже нет.

В разделе 4 мы уже подсчитали спаривания: для четырёх помеченных множителей их 3, для шести — 15, для —
. Здесь каждая пара даёт
, поэтому число спариваний сразу равно гауссову моменту:
Эта формула следует из интегрирования по частям: каждый следующий чётный момент в раз больше предыдущего, а нулевой момент равен 1.
Например, , а
.
Поэтому числитель, делённый на , начинается с
, а знаменатель — с
. Деление даёт
Видно и диаграммное происхождение коэффициента .
Как и в разборе головастика в разделе 4, из 15 спариваний знаменатель убирает 3 несвязных вакуумных вклада. Оставшиеся 12 вместе с дают
. Теперь посчитаем полный следующий коэффициент, а не одну выбранную топологию.
Во втором порядке у двух вершин восемь полевых множителей, но в числителе есть ещё два внешних: всего десять, поэтому там спариваний. В знаменателе остаются восемь множителей и
спариваний. Оба коэффициента делятся на
:
Разложим нормированное отношение: из следует
. Уже известны
и
, поэтому
Так множитель и сокращение вакуумных частей участвуют в явной арифметике. Рекурсия ниже продолжает то же самое деление на любом порядке разложения.
Рисунки нескольких отдельных топологий не заменяют полного подсчёта. Продолжив вычисления, получим
Знаменатель здесь также сокращает несвязные вакуумные части диаграмм.
Среднее равно нулю по чётности, поэтому двухточечная функция связная. Для её связных диаграмм с
каждая вершина имеет четыре конца, два конца внешние, а внутренняя линия использует два:
Значит, именно в этой двухточечной нульмерной модели степень совпадает с числом петель. Это свойство конкретной модели и наблюдаемой величины, а не универсальное правило для любого графа.
Чтобы считать коэффициенты без округления, обозначим ряды числителя и знаменателя через и
. Из
следует
Здесь — коэффициенты
.
Python-класс Fraction сохраняет их точными рациональными числами.

При численный интеграл даёт
, и среди порядков от нулевого до девятого лучший — девятый. При
получаем
, а лучший из тех же порядков — второй. Дальнейшее добавление членов ухудшает результат.
Это пример асимптотического ряда. Коэффициенты растут достаточно быстро, поэтому для фиксированной ненулевой связи полезность очередного члена не гарантирована.
Правило «больше петель — всегда точнее» неверно без оговорок. Но и переносить поведение этого простого ряда на конкретную амплитуду другой теории без анализа нельзя.
Полный код коэффициентов и анимации: 01_loops.py
"""Учебная нульмерная phi^4: связная двухточечная функция, 0–9 петель. Запуск: python 01_loops.py. Создаёт loops.png, loops.gif, loop_diagrams.png. Это не расчёт амплитуды в N=4 SYM. Все коэффициенты включают все диаграммы соответствующего порядка, а рисунки показывают только примеры топологий. """ from pathlib import Path from fractions import Fraction from math import factorial import numpy as np import mpmath as mp import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation, PillowWriter from matplotlib.patches import Circle, Arc OUT = Path(__file__).resolve().parent plt.rcParams.update({"font.family": "DejaVu Sans", "font.size": 11, "axes.spines.top": False, "axes.spines.right": False}) BLUE, RED = "#237da6", "#d05c42" def gaussian_moment(k): return Fraction(factorial(2*k), 2**k * factorial(k)) def coefficients(nmax=9): # Z = <exp(-g*x^4/24)>_0; N = <x^2 exp(-g*x^4/24)>_0. z = [(-1)**n * gaussian_moment(2*n) / (24**n * factorial(n)) for n in range(nmax+1)] num = [(-1)**n * gaussian_moment(2*n+1) / (24**n * factorial(n)) for n in range(nmax+1)] c = [] for n in range(nmax+1): c.append(num[n] - sum(z[k]*c[n-k] for k in range(1, n+1))) return c def reference(g): # Численный интеграл исходной модели, без усечения по петлям. with mp.workdps(40): g = mp.mpf(str(g)) weight = lambda x: mp.exp(-x*x/2 - g*x**4/24) return float(mp.quad(lambda x: x*x*weight(x), [0, mp.inf]) / mp.quad(weight, [0, mp.inf])) c = coefficients() assert c[:4] == [Fraction(1), Fraction(-1, 2), Fraction(2, 3), Fraction(-11, 8)] couplings = [0.1, 0.5] orders = np.arange(len(c)) partials = [np.cumsum([float(a)*g**n for n, a in enumerate(c)]) for g in couplings] refs = [reference(g) for g in couplings] for g, seq, ref in zip(couplings, partials, refs): print(f"g={g}: integral={ref:.12f}; best of 0..9 loops={np.argmin(abs(seq-ref))}") print("Coefficients:", ", ".join(map(str, c))) fig, axes = plt.subplots(2, 1, figsize=(7.2, 9.8)) fig.subplots_adjust(top=.79, bottom=.16, left=.16, right=.96, hspace=.48) fig.suptitle("Больше петель — точнее?\nЗависит от связи", fontsize=19, y=.985) fig.text(.5, .87, "Учебная нульмерная модель φ⁴\nОшибка относительно численного интеграла", ha="center", color="#334155", fontsize=14) lines, dots = [], [] for ax, g, seq, ref in zip(axes, couplings, partials, refs): error = abs(seq-ref) ax.plot(orders, error, color="#dce3e8", lw=2) line, = ax.plot([], [], "o-", color=BLUE, lw=2.5) dot, = ax.plot([], [], "o", color=RED, ms=10) lines.append(line); dots.append(dot) ax.set(yscale="log", xlim=(-.3, 9.3), ylim=(min(error)*.5, max(error)*2), xlabel="Число петель L", title=f"g = {g}", xticks=range(10)) ax.grid(alpha=.2, which="both") ax.tick_params(labelsize=12) ax.xaxis.label.set_size(14) ax.title.set_size(16) ax.set_ylabel("Ошибка |G_L − G|", fontsize=14) fig.text(.5, .025, "Красная точка — лучший из добавленных порядков.\nРяд асимптотический.", ha="center", fontsize=13, color="#334155") def animate(frame): end = min(frame//4+1, 10) for line, dot, seq, ref in zip(lines, dots, partials, refs): error = abs(seq-ref) best = np.argmin(error[:end]) line.set_data(orders[:end], error[:end]) dot.set_data([best], [error[best]]) return lines+dots animate(39) fig.savefig(OUT / "loops.png", dpi=140) anim = FuncAnimation(fig, animate, frames=48, interval=180, blit=False) anim.save(OUT / "loops.gif", writer=PillowWriter(fps=5), dpi=85) plt.close(fig) # Примеры топологий. Внешние ноги в I не входят. fig, axes = plt.subplots(1, 3, figsize=(10, 3.5)) fig.suptitle("Сначала научимся считать петли", fontsize=18, y=.98) for ax in axes: ax.set(xlim=(-1.7, 1.7), ylim=(-1.7, 1.6), aspect="equal") ax.axis("off") axes[0].plot([-1.4, 1.4], [0, 0], lw=3, color=BLUE) axes[0].text(0, -1.25, "0 петель\nСвободный пропагатор", ha="center") axes[1].plot([-1.4, 1.4], [0, 0], lw=3, color=BLUE) axes[1].add_patch(Circle((0, .5), .5, fill=False, lw=3, edgecolor=BLUE)) axes[1].plot([0], [0], "o", color=RED, ms=9) axes[1].text(0, -1.25, "1 петля\nI = 1, V = 1", ha="center") ax = axes[2] ax.plot([-1.5, -.65], [0, 0], color=BLUE, lw=3) ax.plot([.65, 1.5], [0, 0], color=BLUE, lw=3) ax.plot([-.65, .65], [0, 0], color=BLUE, lw=3) ax.add_patch(Arc((0, 0), 1.3, 1.4, theta1=0, theta2=180, lw=3, color=BLUE)) ax.add_patch(Arc((0, 0), 1.3, 1.4, theta1=180, theta2=360, lw=3, color=BLUE)) ax.plot([-.65, .65], [0, 0], "o", color=RED, ms=9) ax.text(0, -1.25, "2 петли\nI = 3, V = 2", ha="center") fig.text(.5, .03, "Для связного графа: L = I − V + 1. Это примеры, не полный набор диаграмм.", ha="center", fontsize=14) fig.subplots_adjust(top=.85, bottom=.18) fig.savefig(OUT / "loop_diagrams.png", dpi=140) plt.close(fig)
11. Возвращаемся к Claude: что именно было заявлено
Уточним задачу, названную во вступлении. Мэтт фон Хиппель предложил Claude вычислить девятипетлевой шестичастичный MHV-результат в планарной суперсимметричной теории Янга — Миллса; проверкой занимался Лэнс Диксон. Два маршрута получения символа — прямой bootstrap и путь через формфактор. Отдельный переход от символа к полной функции обсудим ниже.
В том же сообщении описан параллельный девятипетлевой результат группы Сун Хэ на уровне символа; при получении некоторых ограничений исследователи использовали GPT-6.
Поэтому противопоставление «один ИИ против всех людей» было бы неточным. Рассказ участников.
Разберём название задачи по частям.

Амплитуда — величина, из которой получают вероятности квантовых процессов. Для вероятности нужны квадрат модуля, нормировки и, в задаче рассеяния, соответствующие кинематические множители. Сама амплитуда обычно комплексная.
Шесть частиц — шесть внешних состояний, например две входящие частицы и четыре выходящие. Это не шесть частиц, одновременно летящих навстречу друг другу из шести направлений.
MHV задаёт специальный сектор спиральностей внешних частиц.
Для глюонной амплитуды всех исходящих импульсов это две отрицательные спиральности, остальные положительные.
Спиральность характеризует проекцию спина на направление движения.
Глюон — квант калибровочного поля рассматриваемой теории. У его физических безмассовых состояний две спиральности, и
в единицах
. Здесь это поля модельной теории Янга — Миллса, а не утверждение о наблюдении свободных глюонов в графене.
Планарный предел выделяет ведущий вклад при большом числе цветов калибровочной группы. В соответствующей записи диаграммы можно рисовать на плоскости без пересечений цветовых линий. Это не утверждение, что частицы физически движутся в двумерном листе, как электроны графена.
Калибровочная группа, например , задаёт разрешённые локальные преобразования внутренних компонент поля, описывающие одну и ту же физику.
— группа унитарных матриц размера
с определителем 1; унитарность
сохраняет скалярное произведение. «Цвет» — название внутреннего индекса, не оптический цвет. Двухлинейная запись индексов калибровочного поля позволяет считать, как диаграммы зависят от
. В планарном пределе
сохраняют постоянной связь ’т Хоофта
. При таком пределе выделяются ведущие планарные вклады.
здесь — специальная связь теории, а не автоматически вспомогательный параметр из учебного вывода возмущений.
обозначает расширенную суперсимметрию в четырёхмерной теории.
Её сильные симметрии ограничивают ответ и делают некоторые вычисления доступнее. Она служит теоретической лабораторией методов и не является непосредственно реалистической моделью вещества нашего мира.
Суперсимметрия связывает бозонные и фермионные поля.
Обозначение в четырёх измерениях соответствует четырём наборам спинорных генераторов этой симметрии, или 16 вещественным суперзарядам; оно не означает четырёх частиц или четырёх петель. Для нашего сравнения существенно наличие дополнительных ограничений на ответ, а не знание всей алгебры симметрии. Введение в язык амплитуд есть в лекциях Диксона.
Наконец, девять петель — высокий порядок пертурбативного разложения выбранной амплитуды. До этого был опубликован восьмипетлевой результат Диксона и Лю. Переход от восьми к девяти — новый коэффициент функции, а не простое добавление ещё одного пузырька к готовой RPA-дроби.

12. Как считать амплитуду, не вычисляя по очереди все диаграммы
Зная правила диаграмм, естественно представить программу, которая рисует их все, составляет интегралы и вычисляет каждый. Но это не единственный и совсем не оптимальный путь для вычисления требуемого ответа.
Можно начать с класса функций, в котором должен находиться ответ, записать общую линейную комбинацию и определить коэффициенты из ограничений:
Функции известны, коэффициенты
неизвестны. Симметрии, допустимые особенности и известные пределы превращаются в уравнения на
.
Такой подход физики называют bootstrap.
Миниатюрный пример можно разобрать до конца.
Пусть известно, что , функция чётная,
, а
.
Чётность даёт , второе условие —
, третье —
.
Правильный ответ тут такой: .
import sympy as sp x = sp.symbols('x') a, b, c = sp.symbols('a b c') F = a + b*x + c*x*x solution = sp.solve([b, F.subs(x, 0)-1, F.subs(x, 1)-3], [a,b,c]) print(sp.expand(F.subs(solution))) # 2*x**2 + 1
Теперь уберём предположение о степени не выше двух.
Функция тоже чётная и даёт те же значения в
и
. Но в
первый ответ равен
, а второй —
.

Уникальность внутри выбранного пространства не доказывает, что пространство выбрано правильно. Поэтому нужны два вида проверки: обоснование допустимого класса функций и условия, которые не использовались для подбора коэффициентов. Этот урок одинаково полезен для bootstrap, машинного обучения и старой таблицы моего диплома.
Ещё один пример: условия как последовательные фильтры
Представим, что ищем неизвестную плавную кривую между точками и
. Ограничим пространство квадратичными полиномами. Удобнее сразу записать все функции, удовлетворяющие двум значениям на концах:
Один параметр всё ещё свободен.
Дополнительное условие даёт
, то есть
и
. Теперь можно предсказать значение в неиспользованной точке:
. Если независимое вычисление даст другое число за пределами обоснованной погрешности, возникнет противоречие. Тогда проверяем исходные предположения, вывод и обе вычислительные реализации: ошибка может находиться и в проверочном расчёте.

А теперь расширим выбранное семейство, разрешив одну определённую добавку четвёртой степени. Это ещё не все полиномы степени не выше четырёх:
Добавка с не меняет значения на концах и производную при нуле. Поэтому все прежние условия снова оставляют свободу:
, но
произвольно. В середине получаем
. Независимый ответ
теперь зафиксирует
.
После использования этой точки для определения она становится частью условий построения, а для новой проверки понадобится другая точка. Это наглядный пример ядра системы ограничений: ненулевая добавка может быть невидима всем уже использованным тестам.
import sympy as sp x, a, b = sp.symbols('x a b') F = x + a*x*(1-x) + b*x**2*(1-x)**2 assert F.subs(x, 0) == 0 and F.subs(x, 1) == 1 print(sp.solve(sp.diff(F, x).subs(x, 0)-2, a)) # [1] print(sp.expand(F.subs({a: 1, x: sp.Rational(1, 2)}))) # b/16 + 3/4
Если разрешить все полиномы степени не выше четырёх, прежних условий опять недостаточно. Например,
при любом удовлетворяет всем четырём требованиям:
,
,
,
. Дополнительная точка даёт
и различает кандидатов.

Чёрные точки совпадают у всех кривых; совпадает и начальный наклон. Справа показана разность с . Так выглядит пропущенная свобода в пространстве ответов. Проверка в новом месте полезнее повторения тех условий, которыми кандидат уже был построен.
Ядро системы ограничений здесь — набор добавок, на которые использованные условия не реагируют.
Анзац — это выбранная форма кандидата с неизвестными параметрами; bootstrap ищет эти параметры по совместным требованиям.
Особая точка — значение аргумента, при котором функция перестаёт быть обычной гладкой аналитической функцией: например, полюс при нуле или точка ветвления
.
Для логарифма нужно выбрать непрерывную ветвь аргумента, а разрез отделяет области этого выбора.
Коллинеарный предел означает параллельность определённых внешних импульсов.
В таком пределе теория предсказывает характерное упрощение или сингулярное поведение; это физическое ограничение, а не требование просто нарисовать гладкую кривую.
В реальном bootstrap вместо степени полинома выбирают пространство специальных функций, вместо двух точек используют симметрии, допустимые разрезы, коллинеарные и другие пределы. Но вопрос остаётся узнаваемым: достаточно ли ограничений и не исключили ли мы правильный ответ заранее?
Полный код учебного bootstrap: 02_bootstrap.py
"""Точные ограничения и отложенная проверка в учебной задаче. Запуск: python 02_bootstrap.py. Создаёт holdout.png. """ from pathlib import Path import sympy as sp import numpy as np import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt def publication_layout(fig): """Readable labels at a 600 px article width; retain data and panel order.""" import textwrap from matplotlib.text import Text width, height = fig.get_size_inches() fig.set_size_inches(width, height * 1.22, forward=True) scale = width / 8.0 main, small = 14.0 * scale, 12.0 * scale for text in fig.findobj(Text): if text.get_text().strip(): text.set_fontsize(max(text.get_fontsize(), main)) if text.get_color() in ['#d77b27', '#ed8b36', '#df7b28', '#d77527']: text.set_color('#87470f') for ax in fig.axes: for tick in ax.get_xticklabels() + ax.get_yticklabels(): tick.set_fontsize(small) for label in [ax.xaxis.label, ax.yaxis.label]: txt = label.get_text() if '$' not in txt: label.set_text('\n'.join(textwrap.fill(s, 39) for s in txt.splitlines())) title = ax.get_title() if '$' not in title: columns = max(25, int(ax.get_position().width * 75)) ax.set_title('\n'.join(textwrap.fill(s, columns) for s in title.splitlines()), fontsize=main, pad=12) legend = ax.get_legend() if legend: for text in legend.get_texts(): text.set_fontsize(small) for line in ax.lines: line.set_linewidth(max(line.get_linewidth(), 1.8)) if fig._suptitle: txt = fig._suptitle.get_text() if '$' not in txt: fig._suptitle.set_text(textwrap.fill(txt, 57)) fig._suptitle.set_fontsize(max(main + 2, 18)) for text in fig.texts: if text is fig._suptitle: continue txt=text.get_text() if '$' not in txt: text.set_text('\n'.join(textwrap.fill(s, 68) for s in txt.splitlines())) if type(fig.get_layout_engine()).__name__ == 'PlaceHolderLayoutEngine': fig.tight_layout(rect=(.01,.10,.99,.91),h_pad=2,w_pad=2) if type(fig.get_layout_engine()).__name__ == 'ConstrainedLayoutEngine': fig.get_layout_engine().set(h_pad=.15, w_pad=.12) x, a, b, c = sp.symbols("x a b c") f = a*x*x + b*x + c solution = sp.solve([c-1, b, a+b+c-3], [a, b, c]) f = sp.expand(f.subs(solution)) # Эта добавка не меняет f(-1), f(0), f(1) и сохраняет чётность. fake = f + 4*x*x*(x*x-1) assert all(sp.simplify(fake.subs(x, t)-f.subs(x, t)) == 0 for t in [-1, 0, 1]) assert sp.simplify(fake.subs(x, -x)-fake) == 0 check = sp.Rational(1, 2) assert f.subs(x, check) == sp.Rational(3, 2) assert fake.subs(x, check) == sp.Rational(3, 4) print("Quadratic:", f, "Alternative:", sp.expand(fake)) print("At x=1/2:", f.subs(x, check), fake.subs(x, check)) plt.rcParams.update({"font.family": "DejaVu Sans", "font.size": 11}) fig, ax = plt.subplots(figsize=(9, 5.1)) xx = np.linspace(-1.12, 1.12, 401) ax.plot(xx, sp.lambdify(x, f, "numpy")(xx), lw=3, color="#237da6", label="Квадратичная модель") ax.plot(xx, sp.lambdify(x, fake, "numpy")(xx), lw=3, color="#d05c42", ls="--", label="Альтернатива четвёртой степени") ax.scatter([-1, 0, 1], [3, 1, 3], c="#202e3c", s=70, zorder=5, label="Точки, использованные при подборе") ax.plot([.5, .5], [.75, 1.5], color="#7256a7", lw=3) ax.scatter([.5, .5], [.75, 1.5], c="#7256a7", s=60, zorder=6) ax.annotate("Новая проверка: x = ½\n1,50 против 0,75", (.5, 1.12), xytext=(-.35, 2.45), arrowprops={"arrowstyle": "->", "color": "#7256a7"}, color="#7256a7", fontsize=11) ax.set(xlabel="x", ylabel="f(x)", ylim=(0, 4.3)) ax.spines[["top", "right"]].set_visible(False) ax.grid(alpha=.18) ax.legend(loc="upper center", frameon=False, fontsize=10) fig.suptitle("Три совпадения ещё не доказывают формулу", fontsize=18, y=.97) fig.text(.5, .88, "Обе функции чётные и проходят через одни и те же исходные точки", ha="center", color="#536171", fontsize=11) fig.text(.5, .02, "Учебный пример: он показывает роль проверки, но не реконструирует эксперимент Шварца.", ha="center", fontsize=9, color="#536171") fig.subplots_adjust(top=.8, bottom=.16, left=.09, right=.97) publication_layout(fig); fig.savefig(Path(__file__).resolve().parent / "holdout.png", dpi=140, bbox_inches="tight") plt.close(fig)
13. Что такое «символ» и почему его недостаточно назвать полной функцией
Большие ответы удобно хранить в сокращённом математическом представлении.
В задачах амплитуд таким представлением служит символ: он кодирует определённую структуру итерированных логарифмических дифференциалов.
Начать можно с обычного логарифма:
Если функция строится последовательным интегрированием выражений такого типа, полезно записывать, какие аргументы логарифмов появляются и в каком порядке. Эти аргументы называют буквами алфавита, а упорядоченные последовательности — словами. Это математические метафоры; буквенная последовательность не является текстом на естественном языке.
Простейшие примеры стандартной символной записи:
Последний пример связан с . Знак
сохраняет порядок букв; это не обычное умножение чисел. В частности, перестановка букв вообще говоря меняет объект.
При этом такое представление теряет часть информации. Уже обычная производная не различает и
. Символ имеет более сложные неоднозначности, в том числе специальные константы соответствующего веса и их произведения с функциями меньшего веса. Произвольное произведение функций меньшего веса при этом не обязано иметь нулевой символ. Поэтому совпадение символов — сильная проверка, но для полной функции требуются дополнительные данные.
Дилогарифм: два интегрирования и потерянная константа
Чтобы слово «итерированный» не осталось магическим, определим на
Логарифм внутри тоже получается интегрированием: .
Поэтому перед нами вложенный интеграл, то есть два уровня логарифмической структуры.
В символе первая буква приходит от внутреннего логарифма, последняя
— от внешнего
. Вес равен двум.

Функции и
имеют одинаковый обычный символ веса 2. На графике они различаются на постоянную величину. Значение при нуле выбирает нужную: первая равна нулю, вторая —
. Таким образом, потеря информации здесь видна буквально как вертикальный сдвиг, а не как абстрактная оговорка.
Порядок букв тоже можно проверить на знакомой формуле. Из следует
. Для произведения двух разных логарифмов производная даёт два слагаемых:
Это простейшее shuffle-правило: нужно сохранить внутренний порядок каждой последовательности, но перебрать способы их перемешать.
Тензорная запись линейна по логарифмам: буква-произведение раскрывается как сумма, например .
Поэтому выбор алфавита существенно влияет на число явно записанных слов.
Численно проверим определение дилогарифма двумя способами:
import numpy as np from scipy.integrate import quad from scipy.special import spence for x in [0.1, 0.5, 0.9]: f = lambda t: -np.log1p(-t)/t if t else 1.0 integral = quad(f, 0, x, epsabs=1e-12)[0] library = spence(1-x) # scipy.spence(z) = Li₂(1-z) assert abs(integral-library) < 1e-12 print(x, integral)
Один важный класс — дзета-константы: при вещественном
функция
задаёт, например, и
.
В многократных дзета-значениях суммирование вложено, например
в сходящемся случае.
При принятом весе таким константам сопоставляют сумму индексов.
В обычном символе соответствующего положительного веса числовые константы не видны; потому на большем весе могут теряться и произведения этих констант на функции меньшего веса. Поэтому для получения физически осмысленного ответа к задаче понадобятся еще нормировка, граничные значения и другие данные за пределами символа.
В рассматриваемом девятипетлевом примере слова имеют вес 18.
На сайте проекта Cosmic9 отдельно представлены символные данные, контрольные файлы и результат на уровне функции.
Там же прямо указано, что программы исходного вычисления не распространяются. Поэтому наличие открытых данных не равно возможности запустить исходный расчёт одной командой.
Где символ помогает контролировать код — и где прекращает помогать
Тут важно различать два смысла слова.
Символьная алгебра преобразует выражения с переменными вместо чисел: например, SymPy может проверить коэффициенты ряда.
Символ полилогарифмической функции — специальное математическое отображение , введённое выше.
Для обычных формул проверка начинается с предположений: переменная вещественная или комплексная, положительная ли она, исключён ли нуль?
Например, для вещественного
, а не всегда
.
Численные подстановки в нескольких точках не доказывают тождество и могут не пересечь разрез комплексного логарифма.
Ветвь — это согласованный выбор значения многозначной функции; изменение ветви может добавлять к логарифму.
Для символа нужны: список разрешённых букв, длина слова — вес, допустимые соседства, симметрии и интегрируемость.
Последняя означает, что из записанного набора дифференциалов действительно может получиться функция. Простейшая проверка веса 2 для :
Знак обозначает антисимметричное произведение:
, а
.
Для независимых положительных одиночное слово
даёт
. Сумма
проходит проверку: два вклада сокращаются.
Это символ . Для большего веса проверка применяется к каждой паре соседних позиций с сохранением остальных. В одной переменной эта конкретная проверка может стать тождественно пустой, поэтому она не заменяет остальные ограничения.

В рабочем процессе это отдельный блок:
кандидат → проверка символа → восстановление функции → граничные значения и ветви → независимые численные точки.
Нельзя остановиться после второй стрелки. Одного граничного значения достаточно для определения одной добавочной константы, как в примере с дилогарифмом.
Но для общего многопетлевого ответа неоднозначностей больше и требуется соответствующий набор условий.
Подробности символного исчисления и его реализации разобраны в Duhr и Dulat, PolyLogTools (2019); сохранение информации о дзета-константах обсуждается в работе Duhr (2012).
Ниже Python проверяет небольшой пример интегрируемости и показывает, как функция с правильной производной проваливает проверку значения в опорной точке. Это учебная проверка принципа, а не реализация полного символного алгоритма для девяти петель.
14. Почему в рассказе о частицах появляется поверхность
Шестичастичная нормированная амплитуда после использования симметрий зависит от трёх безразмерных отношений . Определим их полностью.
При всех исходящих импульсах введём инварианты Мандельстама .
Это квадраты суммарных четырёхимпульсов. Они не меняются при преобразовании системы отсчёта Лоренца. Тогда в одной стандартной циклической нумерации
Числитель и знаменатель имеют одинаковую размерность. Это перекрёстные отношения кинематики, а не координаты трёх частиц в лаборатории. В первоисточниках переменные амплитуды часто снабжены шляпками; здесь их опускаем, а переменные формфактора ниже обозначим другими буквами.
В вычислении важна поверхность
Это уравнение связано с определителем Грама шестичастичной кинематики — определителем матрицы скалярных произведений соответствующих векторов.
В алфавите амплитуды появляется ; пространственная чётность меняет знак этого корня. На
две ветви совпадают. Это поверхность в пространстве параметров: её размерность не означает, что частицы начали жить на двумерном листе. Определения и отображение в первоисточнике.
Посмотрим на неё как на геометрический объект. Решая квадратное уравнение относительно , получаем
При обе ветви можно построить непосредственно:

Анимация вращает камеру вокруг математической поверхности. Она не показывает столкновение частиц, ход вычисления Claude или структуру графена. Два цвета обозначают две ветви. Поверхности построены по сетке значений ; окрашенные участки соединяют вычисленные узлы.
После построения проверяем каждую точку подстановкой в исходное уравнение. Иначе можно получить визуально правдоподобную поверхность даже с неправильным знаком перед корнем. В скрипте максимальная невязка должна быть меньше .
Полный код поверхности и проверки: 03_surface.py
"""Две ветви Delta=0 в единичном кубе: геометрия, не амплитуда. Запуск: python 03_surface.py. Создаёт delta0.png и delta0.gif. """ from pathlib import Path import numpy as np import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation, PillowWriter OUT = Path(__file__).resolve().parent plt.rcParams.update({"font.family": "DejaVu Sans", "font.size": 14}) u, v = np.meshgrid(np.linspace(.001, .999, 45), np.linspace(.001, .999, 45)) root = 2*np.sqrt(u*v*(1-u)*(1-v)) w_plus = 1-u-v+2*u*v+root w_minus = 1-u-v+2*u*v-root for w in [w_plus, w_minus]: delta = (1-u-v-w)**2 - 4*u*v*w assert np.max(np.abs(delta)) < 1e-12 assert np.min(w) > -1e-12 and np.max(w) < 1+1e-12 print("Both branches: max |Delta| < 1e-12") fig = plt.figure(figsize=(7.6, 7.1)) ax = fig.add_subplot(111, projection="3d") ax.plot_surface(u, v, w_plus, color="#237da6", alpha=.6, linewidth=0) ax.plot_surface(u, v, w_minus, color="#dd9861", alpha=.6, linewidth=0) ax.set(xlabel="u", ylabel="v", zlabel="w", xlim=(0,1), ylim=(0,1), zlim=(0,1)) ax.set_box_aspect((1,1,1)) fig.suptitle("Исходная поверхность одного из маршрутов", fontsize=16, y=.975) fig.text(.5, .88, r"$\Delta=(1-u-v-w)^2-4uvw=0$", ha="center", fontsize=16) fig.text(.5, .035, "Два цвета — две ветви решения по w.\nТочки задают кинематику, не частицы.", ha="center", fontsize=12, color="#536171") fig.subplots_adjust(top=.82, bottom=.23, left=.04, right=.96) def rotate(frame): ax.view_init(elev=23, azim=25+frame*7.5) return [] rotate(0) fig.savefig(OUT / "delta0.png", dpi=140) anim = FuncAnimation(fig, rotate, frames=48, interval=125) anim.save(OUT / "delta0.gif", writer=PillowWriter(fps=8), dpi=90) plt.close(fig)
Антиподальная дуальность
В работе Диксона, Гюрдоана, Маклеода и Вильгельма, опубликованной в 2022 году после препринта 2021 года, обнаружена связь трёхчастичного формфактора и шестичастичной амплитуды на этой поверхности. Это специальное соотношение нормированных величин данной теории. Для его символьной стороны нужны две операции: преобразование аргументов и антипод — обращение порядка букв со знаком.
Обозначим отношения формфактора через , причём
.
Здесь это кинематические переменные. Карта имеет вид
Подстановка превращает
в тождественный нуль. Это можно проверить точной алгеброй, без построения амплитуды. Например,
переходит в
.

На рисунке взят положительный треугольник
. Слева и справа один цвет помечает одну исходную точку и её образ. Вся поверхность из предыдущей анимации не обязана покрываться только этим выбранным треугольником.
Для слова длины антипод действует так:
При весе знак положителен. Чтобы увидеть операцию на маленьком объекте, возьмём символ дилогарифма:
превращается в
, то есть символ
. Это учебный пример обращения слова; он сам по себе не доказывает физическую дуальность. Константы и ветви функции по-прежнему требуют отдельной работы.

Слева меняется порядок позиций внутри слова. Справа показана другая операция — симметрия меток треугольника: повороты и отражения
. Такие перестановки образуют диэдральную группу. Для шести внешних меток аналогично рассматривают повороты и отражения шестиугольника; действие на выбранные кинематические отношения и буквы нужно устанавливать отдельно.
Откуда берутся 30 024 320 034 слова
На можно выбрать шестибуквенное представление символа, связанное с алфавитом формфактора. Если бы каждая из
позиций независимо принимала любую из шести букв, получилось бы
слов. При девяти петлях это 101 559 956 668 416 — более ста триллионов. Но у физического символа большинство коэффициентов равны нулю: действуют ограничения первых и последних букв, допустимых соседств, интегрируемости и другие отношения.
Прежде чем считать слова, нужно привести одинаковые последовательности к одному ключу и сложить их коэффициенты. Например, содержит четыре записи слагаемых, три разных ключа до сложения и только два ненулевых слова после сокращения. Поэтому число ненулевых слов нельзя получить, просто умножив число разрешённых букв в каждой позиции. Даже перечня допустимых соседств недостаточно: коэффициенты определяются решением физической задачи.
Для сравнения: в игрушечном алфавите из с запретом
можно вести два счётчика
— сколько слов длины
заканчиваются на каждую букву. Тогда
,
, при
. Получатся 2, 3, 5, 8, 13… слов. Это понятная комбинаторика локального ограничения; она не является формулой для девятипетлевой амплитуды.
Что значит «отношения последних индексов»? Символ можно сгруппировать по последней букве:
Надстрочная метка здесь не степень. Она означает коэффициент при
в дифференциале
. Повторное разложение выделяет две, три и больше конечных букв. Это компоненты сокращённого представления, которое в источниках называют копроизведением; это не обычное перемножение функций.
Физические ограничения и интегрируемость дают линейные отношения между такими компонентами. Например, в используемой записи амплитуды компонент равен нулю, поэтому слов с таким хвостом нет. Если два ненулевых блока переводятся друг в друга симметрией, их числа слов равны, хотя сами слова различны. Так возникают множители 2 и 3 ниже. Здесь нельзя считать каждую позицию независимой или выводить эти множители только из количества букв: конкретные отношения взяты из выбранной амплитудной задачи, раздел 3 восьмипетлевой работы.
В реальном опубликованном счёте используют более крупные блоки: собирают слова по последним трём буквам, затем по двум, затем по одной. Назовём число ненулевых слов в блоке, заканчивающемся на последовательность
. Это счётчик слов, не коэффициент самого символа и не число независимых функций. В файле Cosmic9 буквы помечены дополнительным
h: например, chchdh означает . Ниже для читаемости метку опускаю.
Из публичного JSON берём десять исходных счётчиков:
Последние три буквы |
Ненулевых слов в блоке |
|---|---|
c c d |
370 248 149 |
b d d |
370 242 639 |
d c d |
599 066 605 |
e c d |
599 066 605 |
d f d |
598 959 997 |
f f d |
598 959 997 |
d d d |
598 879 864 |
e f d |
598 866 214 |
e d d |
598 959 483 |
b f d |
370 243 718 |
Блоки с разными последними буквами не пересекаются как множества полных слов. После использования отношений последних индексов и диэдральной симметрии получаем тот же способ сборки, который подробно выписан в восьмипетлевой работе, формулы (3.5)–(3.7):
Двойки учитывают симметричные блоки с разными окончаниями, а не удвоение одного и того же слова. Затем
Последняя тройка — три симметричных класса конечной буквы. Это конкретная арифметика, которую можно повторить. Самые трудные исходные десять чисел уже получены из вычисленного символа; эти пять равенств не заменяют его вычисление. В девятипетлевом файле ненулевость считалась по простому модулю 2 147 483 647. Ненулевой остаток гарантирует ненулевой рациональный коэффициент при допустимом знаменателе; нулевой остаток сам по себе не доказывает рациональный нуль. Поэтому сохраняю формулировку «по опубликованному модульному счёту».
Все меньшие порядки — в сопоставимом представлении
Для 1–8 петель счётчики опубликованы в таблице II работы об антиподальной дуальности. Там они записаны для связанного трёхчастичного формфактора. На уровне символа его антиподальное отображение в выбранном алфавите меняет порядок и названия букв (а также ненулевые числовые множители), сохраняя число ненулевых слов; на стороне амплитуды речь идёт именно об ограничении на . Для восьми петель этот счёт отдельно проверен в работе Диксона и Лю. Таким образом, таблица не смешивает полную трёхпеременную амплитуду с её поверхностью.
Петли L |
Вес слова 2L |
Ненулевых слов |
|---|---|---|
1 |
2 |
6 |
2 |
4 |
12 |
3 |
6 |
636 |
4 |
8 |
11 208 |
5 |
10 |
263 880 |
6 |
12 |
4 916 466 |
7 |
14 |
92 954 568 |
8 |
16 |
1 671 656 292 |
9 |
18 |
30 024 320 034 (модульный счёт) |
Нулевой порядок нормированной величины равен единице.
Если расширить соглашение до веса 0, ему соответствует одно пустое слово; в таблице положительных весов оно обычно не записывается.

Вертикальная шкала логарифмическая. Даже после огромного сокращения пространства ответ быстро растёт. От восьми к девяти число слов увеличилось примерно в 17.96 раза. Это не множитель точности, не число диаграмм и не обещание такого же роста времени расчёта. Компактные представления используют общие блоки и отношения, поэтому тридцать миллиардов слов не обязательно хранятся в памяти как тридцать миллиардов отдельных строк.
Полный код сборки числа слов и учебной комбинаторики: 11_symbol_count.py
"""Проверка сборки опубликованного числа слов. Это не новый bootstrap. Нужен E9_Delta0_counts.json: python 04_public_data.py скачивает его. """ from pathlib import Path from collections import defaultdict import json root=Path(__file__).resolve().parent data=json.loads((root/'E9_Delta0_counts.json').read_text(encoding='utf8')) n=data['amplitude_triple_final_entries_Delta0_terms'] cd=n['chchdh']+n['dhchdh']+n['ehchdh'] fd=n['bhfhdh']+n['dhfhdh']+n['ehfhdh']+n['fhfhdh'] dd=2*n['bhdhdh']+n['dhdhdh']+2*n['ehdhdh'] d=2*cd+dd+2*fd total=3*d assert [cd,fd,dd,d,total]==[1568381359,2167029926,2537284108,10008106678,30024320034] assert total==data['derived_counts']['total'] print('cd, fd, dd, d, total:',cd,fd,dd,d,total) counts=[6,12,636,11208,263880,4916466,92954568,1671656292,total] for L,number in enumerate(counts,1): print(f'L={L}: weight={2*L}, all six-letter words={6**(2*L)}, nonzero={number}') # Учебная комбинаторика: слова из a,b без соседних bb. # Это не ограничения реальной амплитуды! ending={'a':1,'b':1} for length in range(1,9): print('toy length',length,'words',sum(ending.values())) ending={'a':ending['a']+ending['b'],'b':ending['a']} # При подсчёте ненулевых слов сначала складывают коэффициенты. coeff=defaultdict(int) for word,value in [('aa',1),('ab',2),('aa',-1),('ba',3)]:coeff[word]+=value nonzero={word:value for word,value in coeff.items() if value!=0} assert nonzero=={'ab':2,'ba':3} print('После сокращения одинаковых слов:',nonzero)
15. Маленькая проверка настоящих данных
Теперь можно прикоснуться к опубликованному результату. В файле выборки содержатся 20 630 слов длины 18. Для каждого записаны рациональный коэффициент и два остатка по простым модулям. Ненулевых коэффициентов 20 400, нулевых — 230.
Зачем остатки? При работе с большими системами рациональная арифметика может порождать огромные числители и знаменатели. Вычисление по простому модулю удерживает числа в конечном диапазоне. Затем результат восстанавливают и проверяют дополнительными условиями. Аналогия с последними цифрами числа годится лишь частично: здесь используются простые модули и деление через обратный элемент.
Если рациональный коэффициент равен , а
не делится на простое
, его образ вычисляется как
Обратный элемент — число, для которого
даёт остаток 1. В Python его находит
pow(b, -1, p). Таким образом, проверка сравнивает два способа записать один коэффициент:
from fractions import Fraction coefficient = Fraction('-105757/65536') for p in [2147483647, 2147483629]: residue = (coefficient.numerator * pow(coefficient.denominator, -1, p)) % p print(p, residue) # 2147483647 829521918 # 2147483629 1173913588
Полный скрипт читает строки, проверяет их длину, алфавит, диапазон остатков и согласованность обеих модульных записей с дробью. Все 20 630 строк проходят эти проверки.
У результата строго ограниченный смысл: файл внутренне согласован в проверенной части.
Если ошибочный коэффициент был согласованно записан и дробью, и двумя остатками, наш тест его не обнаружит.
Полный код проверки публичной выборки: 04_public_data.py
"""Проверка внутренней согласованности публичной выборки Cosmic9. НЕ независимый расчёт коэффициентов или проверка физической правильности. Загружает около 1.8 МБ; создаёт локальные копии данных и counts.png. Запуск: python 04_public_data.py """ from pathlib import Path from urllib.request import urlopen from fractions import Fraction import json import hashlib import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt OUT = Path(__file__).resolve().parent BASE = "https://smsharma.io/cosmic-nine-loops/" def fetch(relative, filename): raw = urlopen(BASE+relative, timeout=60).read() (OUT / filename).write_bytes(raw) print(filename, "sha256:", hashlib.sha256(raw).hexdigest()) return raw data = json.loads(fetch("delta0/E9_Delta0_counts.json", "E9_Delta0_counts.json")) n8, n9 = 1_671_656_292, data["derived_counts"]["total"] assert data["L"] == 9 and data["prime"] == 2_147_483_647 assert n9 == 3*data["derived_counts"]["n_{dh}"] print(f"8 to 9 loops: {n8:,} -> {n9:,}; ratio={n9/n8:.3f}") raw = fetch("samples/E9_sample_coefficients.txt", "E9_sample_coefficients.txt") primes = (2_147_483_647, 2_147_483_629) checked, nonzero = 0, 0 for line in raw.decode("utf-8").splitlines(): if not line.strip() or line.startswith("#"): continue cols = line.split() assert len(cols) == 21, f"Unexpected format: {line[:80]}" word, residues = cols[:18], list(map(int, cols[18:20])) assert set(word) <= {"ah", "bh", "ch", "dh", "eh", "fh", "yu", "yv", "yw"} q = Fraction(cols[20]) for p, expected in zip(primes, residues): assert 0 <= expected < p actual = q.numerator * pow(q.denominator, -1, p) % p assert actual == expected, f"Mismatch for {' '.join(word)} modulo {p}" checked += 1 nonzero += q != 0 assert (checked, nonzero) == (20_630, 20_400) print(f"Passed: {checked} rows, {nonzero} nonzero; 2 primes per row.") plt.rcParams.update({"font.family": "DejaVu Sans", "font.size": 12}) fig, ax = plt.subplots(figsize=(9, 4.8)) bars = ax.barh(["8 петель", "9 петель"], [n8/1e9, n9/1e9], color=["#8aa5b5", "#237da6"], height=.48) ax.invert_yaxis() for bar, n in zip(bars, [n8, n9]): ax.text(bar.get_width()+.4, bar.get_y()+bar.get_height()/2, f"{n/1e9:.2f}".replace(".", ","), va="center", fontsize=14) ax.set(xlim=(0, 35), xlabel="Миллиарды ненулевых слагаемых символа на Δ = 0") ax.spines[["top", "right", "left"]].set_visible(False) ax.grid(axis="x", alpha=.15) ax.set_axisbelow(True) fig.suptitle("Одна дополнительная петля: примерно в 18 раз больше слагаемых", fontsize=15, y=.95) fig.text(.5, .03, "8 петель: Dixon & Liu, (3.7). 9 петель: Cosmic9, подсчёт по модулю 2 147 483 647.\nЭто размер развёрнутого символа на поверхности, не время расчёта и не прирост точности.", ha="center", fontsize=9, color="#536171") fig.subplots_adjust(top=.82, bottom=.24, left=.13, right=.96) fig.savefig(OUT / "counts.png", dpi=140) plt.close(fig)
Как из остатков восстановить дробь и что означает «сертифицирована»
Проверить данную дробь по модулям проще, чем восстановить её. Например, даёт остаток 5 по модулю 11 и 11 по модулю 13. Китайская теорема об остатках объединяет эти два условия в одно: остаток 115 по модулю
. Но без ограничений на числитель и знаменатель этому условию удовлетворяет бесконечно много дробей.
Заранее зададим ,
, взаимную простоту
и обратимость
по модулю
. Среди таких кандидатов получается единственная дробь
. Почему это уже аргумент об однозначности? Если две допустимые дроби
и
дают один остаток, то
делится на
, а
. При
единственный возможный кратный
— нуль. Значит, дроби равны.

Один модуль оставляет пять сокращённых дробей в выбранных границах; два оставляют одну. Границы — часть условия задачи, а не числа, подобранные после желаемого ответа. Если неизвестно, укладывается ли настоящий коэффициент в эти границы, сама картинка ещё не удостоверяет его восстановление.
Это учебный пример рациональной реконструкции. В крупных вычислениях используют эффективные алгоритмы вместо перебора, дополнительные простые модули и проверки исходных соотношений. Поэтому при слове «сертифицировано» нужно смотреть, что именно подтверждено: в каком представлении, при каких границах и на каких модулях. Наш перебор не воспроизводит весь протокол Cosmic9; он показывает, почему одного совпадения остатков недостаточно и какие условия превращают совпадение в доказательство однозначности внутри заданного класса.
16. Где заканчивается проверка и начинается доверие
В техническом описании метода, раздел 6.2 сравниваются два символных представления на 107 053 определяющих ненулевых коэффициентах. Для 3401 из них исходная проверка ограничивалась совпадением по двум простым модулям: стандартная симметричная граница реконструкции не восстанавливала нужную дробь. Авторы отдельно показали, что при другой форме границ восстанавливаются все 107 053 точных значения. Однако эти границы выбраны по размерам коэффициентов прямого bootstrap, поэтому это проверка согласованности двух вычислений, а не независимое восстановление из одних остатков. Она не сертифицирует автоматически 3821 базисную координату, о которых говорится ниже.
Авторы также описывают предположения о пространстве допустимых функций и проверки на меньших порядках. Уникальность остаётся уникальностью внутри принятого пространства. Полная функция получена отдельно; совпадение двух символных маршрутов не означает двух независимых вычислений всей функции.
Для функционального результата указано ещё одно существенное предположение: отношения между семикратными конечными компонентами, верные на уровне символа, перенесены на уровень функции. «Семикратные» означает семь фиксированных последних букв, а не семь петель. Отношения, следующие из интегрируемости и соответствующего уравнения суперсимметрии, имеют основания для такого переноса, но эмпирические отношения более высокого уровня не обязаны сохраняться автоматически. На странице результата это отмечено явно. Значит, независимое подтверждение этих предположений — отдельная задача, которую наша маленькая лабораторная не решает.
На странице данных Cosmic9 отдельно отмечено: в одном из базисных представлений рациональная реконструкция сертифицирована для 99.62% ненулевых координат, а для 3821 координаты — нет. Эти координаты не нужно путать со словами нашей маленькой выборки: это разные уровни представления результата.
Мне кажется полезным различать четыре вопроса. Правильно ли перенесена формула в код? Устойчив ли численный ответ? Корректна ли сама модель и её приближения? Действительно ли получен ранее неизвестный результат? Один успешный тест редко отвечает сразу на все четыре.
В нашем разборе это видно на конкретных шагах. Нормировка функции проверяется интегралом. Перенос notebook проверяется сохранёнными числами. Сетка квадратуры проверяется сгущением. Обрезание уровней — увеличением . Область применимости RPA требует физического анализа. Проверка публичного файла — ещё один отдельный уровень.
Неприятная история о Claude, которая здесь действительно уместна
В марте 2026 года Мэтью Шварц опубликовал рассказ о работе с Claude Opus 4.5 над другой задачей по квантовой хромодинамике. По его описанию, модель подбирала параметры ради совпадения графиков, исключала неудобные вклады в погрешность и переносила ключевую формулу из другой физической системы. После вмешательства специалиста расчёт удалось исправить. Источник — сам Шварц.
На Хабре эпизод уже пересказывали в новости о Claude-аспиранте. Есть и более общая критическая статья о «вайб-физике».
Этот эпизод не доказывает, что новый результат ошибочен: задачи, запуски и версии модели различаются. Его роль в нашей истории другая. Теперь читатель уже знает несколько способов, которыми красивый ответ может оказаться недостаточным: неправильная нормировка, неверная формула, неподходящий класс функций, неустойчивое обрезание, проверка только тех условий, под которые ответ строился.
И мой старый архив оказался хорошим напоминанием, что проблема возникла задолго до LLM. Гладкий график из Mathematica тоже не объясняет, какой коэффициент должен стоять в знаменателе. Напечатанные пятнадцать цифр не превращаются сами собой в пятнадцать достоверных цифр. Совпавшие таблицы не заменяют понятной цепочки вывода.
Поэтому интерес девятипетлевой истории для меня — в возможности обсуждать конкретный вычислительный результат и конкретные проверки. Что подали на вход? В каком классе искали ответ? Чем проверили известные случаи? Что подтвердили независимо? Какую неопределённость оставили открытой? На эти вопросы можно отвечать по файлам и формулам, а затем спорить уже о содержании.
Квантовый пикторализм и код: сначала процесс, потом программа
У этого подхода есть близкий мне математический язык. В книге Боба Коке и Алекса Киссинджера «Изображение квантовых процессов. Начальный курс квантовой теории и рассуждений на диаграммах» (русский перевод А. А. Слинкина, ДМК Пресс, 2019; оригинал Picturing Quantum Processes, 2017) квантовая теория строится через процессы и правила их соединения. Экземпляр книги, который мы обсуждаем. Ниже использованы небольшие фрагменты рисунков именно из этого издания; номера страниц относятся к печатной нумерации.
Блоки, провода и смысл соединения
В главе 3 процесс изображается блоком, системы на его входах и выходах — проводами. У проводов есть типы. Функция двух чисел, сортировка списка и физическое преобразование получают общий язык, но сохраняют разные входы и выходы.

Фрагмент с. 46 книги Коке и Киссинджера, ДМК Пресс, 2019. Рисунки читаются снизу вверх: два вещественных числа поступают в функцию, одно число выходит. Подпись провода определяет, что именно разрешено соединить с блоком.
Слово композиция означает соединение процессов. Последовательная композиция сначала выполняет
, затем передаёт результат в
. Параллельная
действует на две системы рядом. Для линейных процессов это обычное произведение матриц и тензорное произведение соответственно. Матрица
означает «сначала
, затем
». Переставить скобки разрешено по ассоциативности; переставить сами операции обычно нельзя.
Тензорное произведение строит пространство совместной системы. Если у каждой из двух систем есть базис , то у пары четыре базисных состояния:
. Первая метка относится к первой системе, вторая — ко второй. Для матриц
. Значит, в каждой клетке
как будто стоит целый блок
; именно это вычисляет
np.kron(A, B).

Слева и
. Матрица
выбрана для демонстрации блоков и не объявляется физической квантовой операцией. Справа
оставляет первую подсистему неизменной, а во второй меняет 0 и 1 местами:
.
В разделе о символах знак сохранял порядок логарифмических букв. Здесь он соединяет пространства и линейные операторы физических подсистем. Обе записи имеют тензорную природу, но их элементы и правила вычисления различны: массив букв нельзя автоматически трактовать как матрицу квантовой операции.

Фрагмент с. 63 того же издания. Выходные типы первого процесса должны совпасть со входными типами следующего. Порядок проводов тоже часть определения: одинаковое число входов ещё не означает правильное соединение.
Есть и равенства диаграмм: некоторые изменения рисунка не меняют описываемый процесс. Это математическое утверждение с определёнными правилами, а не разрешение произвольно перерисовывать физику.

Фрагмент примера 3.6, с. 52. Геометрию проводов можно менять в рамках правил, сохраняя соединения. Для кода это подсказка: одну композицию полезно вычислить двумя эквивалентными способами и сравнить результат.
Например, при совместимых размерах матриц
Левая программа сначала создаёт большие матрицы, правая сначала умножает маленькие. Равенство задаёт проверку двух реализаций одного процесса. Но точное равенство формул в машинной арифметике превращается в сравнение с обоснованным допуском. В лабораторной части проверим его на комплексных матрицах. Заодно увидим, что забытое комплексное сопряжение может испортить квантовое среднее, сохранив все размеры массивов.
Связь с программами существует не только в виде аналогии: PyZX использует диаграммное ZX-исчисление для работы с квантовыми схемами. Это конкретная область с формальными правилами преобразований.
Квантовая схема описывает последовательность операций над квантовыми подсистемами и измерений. ZX-исчисление — графический язык для представления и преобразования таких схем с двумя семействами узлов, связанными с Z- и X-базисами. Здесь достаточно знать его назначение; его специальные правила нельзя переносить на произвольную блок-схему. Наши произвольные научные блок-схемы не получают автоматически ни этих правил, ни доказательства физической корректности.
Как применить процессную декомпозицию к коду от ИИ
Я предлагаю использовать её как один из методов повышения надёжности и улучшения контроля, рядом с аналитикой, численными проверками и предметной экспертизой. Вместо задания «напиши расчёт магнитоэкситона» сначала зафиксируем диаграмму вычисления:

Стрелка передаёт здесь не просто сам массив: вместе с ним передаются единицы, смысл индексов, соглашения о знаке и область применимости. Блок не считается проверенным только потому, что его выход подошёл по размеру следующей функции.
В этой схеме нужно различать физический процесс и процесс вычисления.
Материал отвечает на внешнее поле — это физическое явление.
Программа сначала строит базис, затем суммирует переходы и интегрирует матричный элемент — это организация расчёта модели физического явления.
Между ними есть соответствие, но вычислительная стрелка не обязана изображать причинное движение частицы во времени.
Диаграммы Фейнмана, процессные диаграммы книги и блок-схема программы имеют разные правила чтения; объединяет их возможность явно показывать структуру операции.
Для каждого блока пишем контракт: что он принимает, что возвращает, какие условия проверяет и на какие гарантии предыдущего блока опирается.
Блок |
Контролируемый вход и выход |
Независимая опора |
|---|---|---|
Параметры → базис Ландау |
|
|
Базис → матричные элементы плотности |
Нормированные состояния и волновой вектор; комплексный |
|
Переходы → статический отклик |
Заполнения и отсечение |
Знак при выбранном определении, пустые суммы, изменение |
|
Одинаковая сетка и совместимые единицы; знаменатель |
|
|
Внешний |
Аналитический предел, другой порядок интегрирования, хвосты и число узлов |
Компоненты → энергия возбуждения |
Веса спинора, знак притяжения, вычитание исходного состояния |
Проверка сборки и единиц, физическое сравнение в области модели |
Для старого notebook есть дополнительная сложность:
Sigma1 — архивная величина со своей нормировкой.
Её нельзя просто объявить физической и подключить к современной формуле.
Нужен явный переход между соглашениями; расхождение коэффициентов из раздела 6 показывает, почему этот переход сам заслуживает отдельного блока и проверки.
Практический запрос к ИИ можно сформулировать так:
Реализуй только блок статического экранирования. Вход: положительное кулоновское ядро V и неположительный отклик Pi на одной сетке, уже приведённые к совместимой нормировке; произведение VPi безразмерно. Выход: W = V/(1 - VPi) в единицах V. Проверяй сетку, конечность значений, размерности и область применимости. Не исправляй знаки автоматически. Проверь нулевой отклик, аналитический пример и неверно соединённые входы. Не меняй контракт ради прохождения теста.
Это задание заметно проще проверить, чем весь диплом сразу. Следующий запрос получает зафиксированный контракт, а не свободный пересказ предыдущего ответа. В интерфейсе полезно хранить и происхождение массива: формулу, единицы, диапазон, параметры отсечения. Если ИИ меняет нормировку, это должно выглядеть как явное изменение интерфейса, затрагивающее соседние блоки.
Что ловит схема, а что остаётся за её пределами
В новом учебном коде входы экранирования оформлены объектами с сеткой и метками единиц. Перемножить массивы разных сеток или несовместимых размерностей программа откажется. Правильная реализация проходит проверки нулевого отклика и допустимого диапазона. Реализация с ошибкой в знаменателе проваливает их; неверная сетка останавливается ещё до физического вычисления. Это конкретный выигрыш от контролируемых соединений.
Но две неверные формулы могут случайно удовлетворить одному контракту.
Метку единиц можно подписать неправильно. Все блоки могут использовать одно ошибочное физическое предположение. Поэтому поверх локальных проверок нужны
интеграционные — проверка соединённых блоков
и сквозные: от исходных параметров до известного результата.
Нужны независимые ориентиры и проверка сходимости, а после них — проверка соответствия модели реальному явлению. Декомпозиция дает возможность локализовать ошибки, но сама по себе истинность модели она не производит.
Здесь для меня находится содержательная связь с «кодом будущего».
ИИ быстро предлагает реализации, человек задаёт язык процессов, интерфейсы и критерии опровержения. Ответ чата превращается в проверяемый компонент только после прохождения этих критериев. Роль человека смещается к постановке эксперимента над программой, но ответственность за смысл результата никуда не исчезает.
Процессуальное мышление при этом гораздо полезнее и шире этого.
Всё научное знание можно рассматривать как воспроизводимую цепочку преобразований — от подготовки системы и измерения до формулы, кода и вывода.
Тогда вопрос «что это за число?» дополняется вопросами «каким процессом оно получено, что считалось входом и какое изменение входа должно изменить выход?».
Это моя личная методологическая интерпретация науки, созвучная книге про квантовый пикторализм. Её достоинство чисто практическое: появляется больше мест, в которых красивое, но неверное объяснение может встретиться с проверкой и быть опровергнуто.
Обозначения, которые меняют смысл между разделами
Определения терминов даны по ходу вывода. Перед запуском кода полезно отдельно сверить локальные соглашения:
Обозначение |
Как различать контексты |
|---|---|
|
|
|
В диаграмме |
|
В учебных моделях — параметры взаимодействия; в топологии |
|
В отклике |
|
|
В символе — упорядоченная запись логарифмических букв; в процессной схеме — тензорное произведение пространств или операторов. |
Такие соглашения стоит хранить рядом с кодом: одинаковая буква или размер массива ещё не означают одинакового физического объекта.
17. Лабораторная: что запускать, что менять и что должно получиться
Код рассчитан на Python 3.12.
Запускайте учебные скрипты в обычном режиме, без флага -O и без включённой переменной окружения PYTHONOPTIMIZE, иначе проверки assert будут отключены.
Сохраните листинги из раскрывающихся блоков в одну папку под указанными именами.
Установите библиотеки:
python -m pip install numpy scipy matplotlib sympy mpmath pillow
На Windows вместо python можно использовать py -3.12.
Все вычислительные примеры и графики работают локально.
Только 04_public_data.py скачивает открытые файлы Cosmic9 — около 1.8 МБ.
Старые Mathematica и Excel для воспроизведения не нужны: исходные определения и исходные таблицы из них уже включены в код.
Ниже отдельный драйвер 10_laboratory.py: его аргументы — это “ручки” эксперимента.
Он сохраняет PNG и JSON с числами рядом с собой. Имена файлов включают параметры: например, lab_loops_g0.1_L9.png и lab_loops_g0.1_L15.png можно открыть рядом и сравнить. Команда запуска напечатана и внизу каждой картинки.
Картинки в этом разделе получены запуском этих команд.
Опыт 1. Когда добавление пузырьков помогает, а когда мешает
Сначала запускаем сходящуюся геометрическую цепочку:
python 10_laboratory.py --case rpa --s 0.8 --order 20

Слева знакопеременные вклады, справа их накопленная сумма.
Она колеблется вокруг и приближается к ней.
При --order 20 включены члены от нулевого до двадцатого, то есть 21 член, и сумма равна 0.56067965. Измените только --order на 40: амплитуда колебаний уменьшится, а сумма станет примерно 0.55561463.
Теперь измените только силу отклика:
python 10_laboratory.py --case rpa --s 1.2 --order 20

Столбики растут, частичные суммы всё сильнее отклоняются от конечного решения ; её двадцатый порядок около 21.36596.
Геометрическое разложение неприменимо при , хотя уравнение самосогласования по-прежнему имеет конечное решение.
Попробуйте предсказать чётность отклонения до запуска.
Опыт 2. Уменьшаем магнитное поле в четыре раза
python 10_laboratory.py --case landau --B 10 python 10_laboratory.py --case landau --B 2.5

При 10 Тл должны получиться нм и
мэВ. При 2.5 Тл магнитная длина удвоится до 16.226 нм, а первая энергия уменьшится вдвое до 57.368 мэВ.
Это проверка двух степенных законов:
и
.
В скрипте м/с; если поменять его в коде, энергии изменятся пропорционально, а магнитная длина останется прежней.
Опыт 3. Девять петель против двух
python 10_laboratory.py --case loops --g 0.1 --order 9 python 10_laboratory.py --case loops --g 0.5 --order 9


В первом запуске исходный интеграл равен примерно 0.955576629510, и среди порядков 0–9 наименьшую ошибку даёт девятый. Во втором интеграл равен 0.838595434923, а лучший из тех же порядков — второй. На правом графике ошибка показана в логарифмическом масштабе; оранжевая точка отмечает минимум среди проверенных порядков.
Поставьте --order 15 и сначала предположите, продолжится ли улучшение при . Затем попробуйте
--g 0.2. Цель опыта — найти полезный диапазон усечения и увидеть, что номер последнего добавленного члена сам по себе не измеряет точность.
Численный интеграл — это отдельный ориентир для этой простой модели.
Контрольные ответы для продолжения опыта
При --g 0.1 --order 15 лучший среди порядков 0–15 — четырнадцатый: ошибка около . Следующий член уже слегка ухудшает ответ. При
--g 0.2 --order 15 ориентир равен 0.919366349957, а лучший среди тех же порядков — шестой.
Сравниваем ошибки всех частичных сумм, а не только размер последнего слагаемого: это разные критерии усечения.
Опыт 4. Воспроизводим одну проверку моего диплома
Для этого опыта рядом должен лежать 06_graphene.py, приведённый в разделе 7:
python 10_laboratory.py --case cutoff --N 15 30 60

Это увеличение количества уровней в пузырьке, а не количества петель. При программа должна напечатать примерно −0.02659943, −0.02714981 и −0.02753810. Измените список на
--N 30 60 120 и сравните изменение ответа с требуемой вам точностью.
Не делайте вывод о сходимости только по одной точке: слева построена зависимость по целому диапазону .
Чтобы восстановить все исходные проверки, выполните python 06_graphene.py.
Для старых Excel-данных — python 07_excel.py. Чтобы сравнить публичные записи настоящих коэффициентов — python 04_public_data.py, а затем проверить сборку числа слов — python 11_symbol_count.py. Последние проверки устанавливают согласованность опубликованных данных; они не выводят амплитуду заново из физических законов.
Полный код лабораторных опытов с параметрами: 10_laboratory.py
"""Лабораторная: меняйте аргументы команд, а не скрытые настройки. python 10_laboratory.py --case loops --g 0.1 --order 9 python 10_laboratory.py --case rpa --s 0.8 --order 20 python 10_laboratory.py --case landau --B 10 python 10_laboratory.py --case cutoff --N 15 30 60 Зависимости: numpy scipy matplotlib. Для cutoff нужен 06_graphene.py рядом. """ from pathlib import Path from fractions import Fraction from math import factorial import argparse, importlib.util, json import numpy as np from scipy.integrate import quad from scipy.constants import hbar, elementary_charge as e import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt def publication_layout(fig): """Readable labels at a 600 px article width; retain data and panel order.""" import textwrap from matplotlib.text import Text width, height = fig.get_size_inches() fig.set_size_inches(width, height * 1.22, forward=True) scale = width / 8.0 main, small = 14.0 * scale, 12.0 * scale for text in fig.findobj(Text): if text.get_text().strip(): text.set_fontsize(max(text.get_fontsize(), main)) if text.get_color() in ['#d77b27', '#ed8b36', '#df7b28', '#d77527']: text.set_color('#87470f') for ax in fig.axes: for tick in ax.get_xticklabels() + ax.get_yticklabels(): tick.set_fontsize(small) for label in [ax.xaxis.label, ax.yaxis.label]: txt = label.get_text() if '$' not in txt: label.set_text('\n'.join(textwrap.fill(s, 39) for s in txt.splitlines())) title = ax.get_title() if '$' not in title: columns = max(25, int(ax.get_position().width * 75)) ax.set_title('\n'.join(textwrap.fill(s, columns) for s in title.splitlines()), fontsize=main, pad=12) legend = ax.get_legend() if legend: for text in legend.get_texts(): text.set_fontsize(small) for line in ax.lines: line.set_linewidth(max(line.get_linewidth(), 1.8)) if fig._suptitle: txt = fig._suptitle.get_text() if '$' not in txt: fig._suptitle.set_text(textwrap.fill(txt, 57)) fig._suptitle.set_fontsize(max(main + 2, 18)) for text in fig.texts: if text is fig._suptitle: continue txt=text.get_text() if '$' not in txt: text.set_text('\n'.join(textwrap.fill(s, 68) for s in txt.splitlines())) if type(fig.get_layout_engine()).__name__ == 'PlaceHolderLayoutEngine': fig.tight_layout(rect=(.01,.18,.99,.91),h_pad=2,w_pad=2) if type(fig.get_layout_engine()).__name__ == 'ConstrainedLayoutEngine': fig.get_layout_engine().set(h_pad=.15, w_pad=.12) OUT=Path(__file__).resolve().parent plt.rcParams.update({'font.family':'DejaVu Sans','font.size':12, 'axes.spines.top':False,'axes.spines.right':False}) def coefficients(nmax): moment=lambda k:Fraction(factorial(2*k),2**k*factorial(k)) z=[(-1)**n*moment(2*n)/(24**n*factorial(n)) for n in range(nmax+1)] num=[(-1)**n*moment(2*n+1)/(24**n*factorial(n)) for n in range(nmax+1)] c=[] for n in range(nmax+1):c.append(num[n]-sum(z[k]*c[n-k] for k in range(1,n+1))) return c def run(a): fig,axes=plt.subplots(1,2,figsize=(11,5.5)); result={'case':a.case} if a.case=='loops': if a.g<=0:raise ValueError('Нужна g>0') c=coefficients(a.order); n=np.arange(a.order+1) weight=lambda x:np.exp(-x*x/2-a.g*x**4/24) exact=quad(lambda x:x*x*weight(x),0,np.inf,epsabs=1e-12,epsrel=1e-12)[0]/quad(weight,0,np.inf,epsabs=1e-12,epsrel=1e-12)[0] partial=np.cumsum([float(v)*a.g**k for k,v in enumerate(c)]) err=abs(partial-exact); best=int(np.argmin(err)) axes[0].plot(n,partial,'o-',label='Частичная сумма');axes[0].axhline(exact,color='black',ls='--',label='Численный интеграл');axes[0].set(ylabel='G',xlabel='Порядок L');axes[0].legend(fontsize=10) axes[1].semilogy(n,np.maximum(err,1e-16),'o-');axes[1].plot(best,max(err[best],1e-16),'o',color='#ed8b36',ms=12);axes[1].set(xlabel='Порядок L',ylabel='Абсолютная ошибка') fig.suptitle(f'g={a.g:g}: G={exact:.12f}; лучший из 0…{a.order} — L={best}',fontsize=16) command=f'python 10_laboratory.py --case loops --g {a.g:g} --order {a.order}' name=f'lab_loops_g{a.g:g}_L{a.order}';result.update(g=a.g,order=a.order,reference=exact,best_order=best,partial=partial.tolist()) elif a.case=='rpa': if a.s<0:raise ValueError('Для примера экранирования нужна s>=0') n=np.arange(a.order+1); terms=(-a.s)**n; partial=np.cumsum(terms); exact=1/(1+a.s) axes[0].bar(n,terms,color=['#147d92' if v>=0 else '#ed8b36' for v in terms]);axes[0].set(xlabel='Число вставок n',ylabel='Вклад (−s)ⁿ') axes[1].plot(n,partial,'o-',label='Сумма до n');axes[1].axhline(exact,color='black',ls='--',label='1/(1+s)');axes[1].set(xlabel='Максимальное n',ylabel='W / V₀');axes[1].legend(fontsize=10) assert np.allclose(partial,(1-(-a.s)**(n+1))/(1+a.s)) fig.suptitle(f's={a.s:g}: решение RPA={exact:.9f}; ряд '+('сходится' if a.s<1 else 'не сходится'),fontsize=17) command=f'python 10_laboratory.py --case rpa --s {a.s:g} --order {a.order}' name=f'lab_rpa_s{a.s:g}_N{a.order}';result.update(s=a.s,order=a.order,exact=exact,last=float(partial[-1])) elif a.case=='landau': if a.B<=0:raise ValueError('Нужно B>0') lb=np.sqrt(hbar/(e*a.B)); n=np.arange(-5,6); en=np.sign(n)*hbar*1e6/lb*np.sqrt(2*abs(n))/e*1000 for j,E in zip(n,en):axes[0].hlines(E,0,1,color='#147d92');axes[0].text(1.03,E,str(j),va='center',fontsize=10) axes[0].set(xlim=(0,1.4),ylabel='Энергия, мэВ',title=f'Уровни при B={a.B:g} Тл');axes[0].set_xticks([]) bs=np.linspace(.1,max(12,a.B),250);energy=1e6*np.sqrt(2*e*hbar*bs)/e*1000 axes[1].plot(bs,energy);axes[1].scatter([a.B],[en[6]],color='#ed8b36',s=90);axes[1].set(xlabel='B, Тл',ylabel='E₁, мэВ',title='Предсказание: E₁ ∝ √B') fig.suptitle(f'lB={lb*1e9:.3f} нм, E₁={en[6]:.3f} мэВ, vF=10⁶ м/с',fontsize=17) command=f'python 10_laboratory.py --case landau --B {a.B:g}' name=f'lab_landau_B{a.B:g}';result.update(B=a.B,lB_nm=lb*1e9,E1_meV=float(en[6])) else: spec=importlib.util.spec_from_file_location('graphene',OUT/'06_graphene.py') module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module) if min(a.N)<1:raise ValueError('N>=1') q=np.linspace(.03,5,100); vals=[] for N in a.N: y=module.sigma(q,0,N) assert np.isfinite(y).all(), 'Поляризация должна оставаться конечной' axes[0].plot(q,y,label=f'N={N}');vals.append(float(module.sigma(1.,0,N))) axes[0].set(xlabel='q',ylabel='Σ₀,N(q)',title='Граница суммы — параметр вычисления');axes[0].legend(fontsize=10) axes[1].plot(a.N,vals,'o-');axes[1].set(xlabel='N',ylabel='Σ₀,N(1)',title='Значение при q=1 ещё меняется') command='python 10_laboratory.py --case cutoff --N '+' '.join(map(str,a.N)) name='lab_cutoff_N'+'_'.join(map(str,a.N));result.update(N=a.N,sigma_q1=vals) for ax in axes:ax.grid(alpha=.18) fig.text(.5,.025,command,ha='center',fontsize=11,family='DejaVu Sans Mono',color='#147d92') fig.tight_layout(rect=(0,.08,1,.93));publication_layout(fig); fig.savefig(OUT/(name+'.png'),dpi=135, bbox_inches="tight");plt.close(fig) (OUT/(name+'.json')).write_text(json.dumps(result,ensure_ascii=False,indent=2),encoding='utf8') print(json.dumps(result,ensure_ascii=False)) print('Картинка:',OUT/(name+'.png')) if __name__=='__main__': p=argparse.ArgumentParser();p.add_argument('--case',choices=['loops','rpa','landau','cutoff'],required=True) p.add_argument('--g',type=float,default=.1);p.add_argument('--s',type=float,default=.8) p.add_argument('--order',type=int,default=9);p.add_argument('--B',type=float,default=10.) p.add_argument('--N',type=int,nargs='+',default=[15,30,60]);a=p.parse_args() max_order=200 if a.case=='rpa' else 30 if not 0<=a.order<=max_order:p.error(f'--order должен быть от 0 до {max_order}') run(a)
Опыт 5. Обнаружит ли проверка намеренно испорченную программу
Сохраните следующий листинг как 12_verify_ai.py рядом с 06_graphene.py и выполните:
python 12_verify_ai.py
На Windows можно использовать py -3.12 12_verify_ai.py. Скачивание данных не требуется. Программа создаст три картинки из начала статьи — ai_diploma_units.png, ai_mutation_matrix.png и ai_smooth_wrong.png, таблицу ai_verification_matrix.csv и подробный отчёт ai_verification_report.json. В отчёте отдельно сохранены размерностная проверка записи из диплома и результаты опыта с намеренными модификациями.
Все варианты вычисляются отдельно, файл с восстановлением notebook не переписывается.
Ожидаемый результат: контрольная строка проходит шесть условий; пять намеренных модификаций нарушают хотя бы одно условие каждая.
Надписи FAIL у испорченных вариантов — ожидаемый результат опыта, поэтому сам сценарий при правильной работе завершится с кодом 0. Скрипт отдельно проверяет, что получился именно заявленный рисунок обнаруженных отклонений.
В коде найдите список cases. У варианта wrong_width замените radial_scale=np.sqrt(2) на radial_scale=1.1. Нормировка должна остаться равной 1, а средний квадрат радиуса станет . Это можно предсказать заменой переменной в интеграле, до запуска Python. Изменённая функция снова пройдёт нормировку и не пройдёт проверку момента. Перед изменением сохраните копию исходного скрипта, чтобы сравнить оба отчёта.
Ещё один опыт можно провести без редактирования: откройте CSV и представьте, что у вас есть только столбцы «Конечные значения» и «Нормировка».
Четыре из пяти модификаций останутся незамеченными. Восстановите остальные столбцы и для каждого красного результата объясните, из какого свойства модели следует ожидание. Цель — научиться обосновывать проверки, а не накапливать число assert.
Результат последнего столбца зависит от выбранного порога, номера уровня и аргумента . Прикладная задача должна задавать собственную требуемую точность и диапазон. Единственный порог 1% при
в этом опыте не является универсальным критерием достаточного обрезания.
Полный код опыта с намеренными ошибками: 12_verify_ai.py
"""Учебный эксперимент: способны ли проверки обнаружить известные дефекты? python 12_verify_ai.py Рядом нужен 06_graphene.py. Зависимости: numpy scipy matplotlib. Дефекты внесены намеренно; это НЕ частота ошибок ИИ и НЕ история его ответов. Исходный 06_graphene.py не изменяется. Результаты: PNG, CSV и JSON. """ from pathlib import Path from dataclasses import dataclass, replace import csv, hashlib, importlib.util, json, platform import numpy as np from scipy.integrate import quad from scipy.constants import hbar, elementary_charge as e import scipy import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt def publication_layout(fig): """Readable labels at a 600 px article width; retain data and panel order.""" import textwrap from matplotlib.text import Text width, height = fig.get_size_inches() fig.set_size_inches(width, height * 1.22, forward=True) scale = width / 8.0 main, small = 14.0 * scale, 12.0 * scale for text in fig.findobj(Text): if text.get_text().strip(): text.set_fontsize(max(text.get_fontsize(), main)) if text.get_color() in ['#d77b27', '#ed8b36', '#df7b28', '#d77527']: text.set_color('#87470f') for ax in fig.axes: for tick in ax.get_xticklabels() + ax.get_yticklabels(): tick.set_fontsize(small) for label in [ax.xaxis.label, ax.yaxis.label]: txt = label.get_text() if '$' not in txt: label.set_text('\n'.join(textwrap.fill(s, 39) for s in txt.splitlines())) title = ax.get_title() if '$' not in title: columns = max(25, int(ax.get_position().width * 75)) ax.set_title('\n'.join(textwrap.fill(s, columns) for s in title.splitlines()), fontsize=main, pad=12) legend = ax.get_legend() if legend: for text in legend.get_texts(): text.set_fontsize(small) for line in ax.lines: line.set_linewidth(max(line.get_linewidth(), 1.8)) if fig._suptitle: txt = fig._suptitle.get_text() if '$' not in txt: fig._suptitle.set_text(textwrap.fill(txt, 57)) fig._suptitle.set_fontsize(max(main + 2, 18)) for text in fig.texts: if text is fig._suptitle: continue txt=text.get_text() if '$' not in txt: text.set_text('\n'.join(textwrap.fill(s, 68) for s in txt.splitlines())) if type(fig.get_layout_engine()).__name__ == 'PlaceHolderLayoutEngine': fig.tight_layout(rect=(.01,.10,.99,.91),h_pad=2,w_pad=2) if type(fig.get_layout_engine()).__name__ == 'ConstrainedLayoutEngine': fig.get_layout_engine().set(h_pad=.15, w_pad=.12) from matplotlib.colors import ListedColormap OUT=Path(__file__).resolve().parent spec=importlib.util.spec_from_file_location('graphene',OUT/'06_graphene.py') graphene=importlib.util.module_from_spec(spec) spec.loader.exec_module(graphene) plt.rcParams.update({'font.family':'DejaVu Sans','font.size':12, 'axes.spines.top':False,'axes.spines.right':False}) @dataclass(frozen=True) class Case: key: str label: str jacobian: bool=True radial_scale: float=1.0 field_power: float=0.5 rpa_sign: int=1 cutoff: int=60 baseline=Case('control','Контрольная версия\nN=60') cases=[baseline, replace(baseline,key='no_r',label='Потерян якобиан r',jacobian=False), replace(baseline,key='wrong_width',label='Неверная ширина,\nнормировка сохранена',radial_scale=np.sqrt(2)), replace(baseline,key='wrong_B',label='E₁ ∝ B вместо √B;\nпри 10 Тл совпадает',field_power=1.0), replace(baseline,key='wrong_rpa_sign',label='Неверный знак\nв знаменателе RPA',rpa_sign=-1), replace(baseline,key='small_N',label='N=15 при требовании\nизменения < 1%',cutoff=15)] def density(r,case): # a*f(a*r) сохраняет двумерную нормировку при положительном a. # a=sqrt(2) намеренно меняет ширину относительно формулы архива. a=case.radial_scale psi=a*graphene.fg(0,0,a*np.asarray(r)) measure=np.asarray(r) if case.jacobian else np.ones_like(r,dtype=float) return 2*np.pi*measure*abs(psi)**2 def energy_meV(B,case): # Все варианты проходят одну опорную точку при 10 Тл. # Неверный закон сохраняет размерность за счёт отношения B/Bref. Bref=10.0 Eref=1e6*np.sqrt(2*e*hbar*Bref)/e*1000 return Eref*(np.asarray(B)/Bref)**case.field_power def screened_ratio(s,case): return 1/(1+case.rpa_sign*np.asarray(s)) def cutoff_change(N): coarse=float(graphene.sigma(1.,0,N)) fine=float(graphene.sigma(1.,0,2*N)) return abs(coarse-fine)/abs(fine),coarse,fine TESTS=['Конечные\nзначения','Нормировка\n= 1','Момент\n⟨r²⟩ = 2', 'Масштаб\nпо полю','Статическое\nослабление','Удвоение N:\nизменение < 1%'] def evaluate(case): norm=quad(lambda r:density(r,case),0,np.inf,epsabs=1e-11,epsrel=1e-11)[0] moment=quad(lambda r:r*r*density(r,case),0,np.inf,epsabs=1e-11,epsrel=1e-11)[0] B_ratio=float(energy_meV(2.5,case)/energy_meV(10.,case)) s=np.array([.1,.5,.8]);wr=screened_ratio(s,case) delta,coarse,fine=cutoff_change(case.cutoff) finite=bool(np.isfinite(np.r_[density(np.linspace(0,5,101),case), energy_meV(np.array([2.5,10.]),case),wr, [norm,moment,delta,coarse,fine]]).all()) passed=[finite,abs(norm-1)<1e-9,abs(moment-2)<1e-9, abs(B_ratio-.5)<1e-12,bool(np.all((wr>0)&(wr<=1))),delta<.01] return {'case':case.key,'label':case.label.replace('\n',' '), 'passed':list(map(bool,passed)),'normalization':float(norm), 'radial_second_moment':float(moment),'E1_2.5T_over_10T':B_ratio, 'W_over_V0_at_s0.8':float(wr[-1]),'N':case.cutoff, 'sigma_N':coarse,'sigma_2N':fine,'relative_change_on_doubling':delta} def main(): # Показатели степеней базовых единиц (кг, м, с). # Это проверка записи из диплома, отдельно от намеренных модификаций ниже. dim_hbar=np.array([1,2,-1]);dim_v=np.array([0,1,-1]);dim_l=np.array([0,1,0]) dim_energy=np.array([1,2,-2]) dim_archive=dim_hbar-dim_v-dim_l dim_correct=dim_hbar+dim_v-dim_l assert not np.array_equal(dim_archive,dim_energy) assert np.array_equal(dim_correct,dim_energy) rows=[evaluate(case) for case in cases] assert all(rows[0]['passed']), 'Контрольная версия не прошла выбранные проверки' # Проверяем сам демонстрационный эксперимент: каждая известная модификация # должна нарушить заранее выписанное условие. Матрица включает все проверки, # в том числе те, которые данную ошибку не замечают. expected_failures={'no_r':{1,2},'wrong_width':{2},'wrong_B':{3}, 'wrong_rpa_sign':{4},'small_N':{5}} for row in rows[1:]: failed={i for i,ok in enumerate(row['passed']) if not ok} assert failed==expected_failures[row['case']],row report={'experiment':'intentionally introduced defects; not model benchmark', 'environment':{'python':platform.python_version(),'numpy':np.__version__, 'scipy':scipy.__version__,'matplotlib':matplotlib.__version__}, 'source_sha256':hashlib.sha256((OUT/'06_graphene.py').read_bytes()).hexdigest(), 'tests':[s.replace('\n',' ') for s in TESTS],'rows':rows, 'diploma_units':{'base_units':['kg','m','s'], 'hbar_div_vF_lB':dim_archive.tolist(), 'hbar_times_vF_div_lB':dim_correct.tolist(), 'energy':dim_energy.tolist(), 'scope':'error in the written energy unit; not proof that original numerical results are wrong'}, 'limitations':['cutoff check is change at one q=1, not an error bound to N=infinity', 'synthetic defects do not estimate error frequency of any AI model', 'normalization does not validate the physical model', 'passing all selected checks does not prove absence of other errors']} (OUT/'ai_verification_report.json').write_text(json.dumps(report,ensure_ascii=False,indent=2),encoding='utf8') with (OUT/'ai_verification_matrix.csv').open('w',encoding='utf-8-sig',newline='') as f: writer=csv.writer(f);writer.writerow(['Вариант']+report['tests']) for row in rows:writer.writerow([row['label']]+['PASS' if v else 'FAIL' for v in row['passed']]) fig,axes=plt.subplots(1,2,figsize=(10,4.6)) for ax in axes:ax.axis('off') axes[0].set_title('Скорость в знаменателе',fontsize=16,color='#a33d34') axes[0].text(.5,.65,r'$\hbar/(v_F l_B)$',ha='center',fontsize=31) axes[0].text(.5,.37,r'$[\hbar/(v_F l_B)]=\mathrm{kg}$',ha='center',fontsize=23) axes[0].text(.5,.1,'Не имеет размерности энергии',ha='center',fontsize=14,color='#a33d34') axes[1].set_title('Масштаб энергии из гамильтониана',fontsize=16,color='#217455') axes[1].text(.5,.65,r'$\hbar v_F/l_B$',ha='center',fontsize=31) axes[1].text(.5,.37,r'$[\hbar v_F/l_B]=\mathrm{kg}\,\mathrm{m}^2/\mathrm{s}^2$',ha='center',fontsize=21) axes[1].text(.5,.1,'Имеет размерность энергии',ha='center',fontsize=14,color='#217455') fig.suptitle('Ошибка в записи единицы энергии: проверка по СИ',fontsize=18,y=.96) fig.text(.5,.025,'Формулы перенабраны для сравнения; исходная запись — в разделе 9 диплома.\n' 'Ошибка в тексте не устанавливает автоматически ошибку всех старых численных результатов.', ha='center',fontsize=10) fig.subplots_adjust(left=.02,right=.98,top=.76,bottom=.2,wspace=.08) publication_layout(fig); fig.savefig(OUT/'ai_diploma_units.png',dpi=150, bbox_inches="tight");plt.close(fig) matrix=np.array([row['passed'] for row in rows],dtype=int) fig,ax=plt.subplots(figsize=(12,7.1)) ax.imshow(matrix,cmap=ListedColormap(['#eaa49d','#b3dbca']),vmin=0,vmax=1,aspect='auto') display_tests=['Конечные\nчисла','Норма\n= 1','Момент\n⟨r²⟩ = 2', 'Масштаб\nE(B)','Статика:\n0 < W ≤ V','N → 2N\n< 1%'] ax.set_xticks(range(len(TESTS)),display_tests,fontsize=10) ax.xaxis.tick_top();ax.tick_params(length=0,pad=9) ax.set_yticks(range(len(cases)),[c.label for c in cases],fontsize=11) for y in range(len(cases)): for x in range(len(TESTS)): ax.text(x,y,'PASS' if matrix[y,x] else 'FAIL',ha='center',va='center',fontsize=12) ax.set_xticks(np.arange(-.5,len(TESTS),1),minor=True) ax.set_yticks(np.arange(-.5,len(cases),1),minor=True) ax.grid(which='minor',color='white',linewidth=3) ax.tick_params(which='minor',length=0) fig.suptitle('Что обнаруживают проверки, а что проходит незамеченным',fontsize=17,y=.97) fig.text(.5,.06,'PASS — выполнено данное условие; FAIL — обнаружено отклонение.\n' 'Все дефекты внесены намеренно. Зелёная строка не доказывает правильность всей физики.', ha='center',fontsize=11) fig.subplots_adjust(left=.25,right=.98,top=.79,bottom=.25) publication_layout(fig); fig.savefig(OUT/'ai_mutation_matrix.png',dpi=150, bbox_inches="tight");plt.close(fig) fig,axes=plt.subplots(3,1,figsize=(8,10)) rs=np.linspace(0,4,300) for case,label in [(cases[0],'По исходной формуле'),(cases[1],'Без якобиана r'),(cases[2],'Другая ширина')]: axes[0].plot(rs,density(rs,case),label=label) axes[0].set(xlabel='r / lB',ylabel='Радиальная плотность',title='Гладкость не проверяет меру') Bs=np.linspace(.2,12,300) for case,label in [(cases[0],'E₁ ∝ √B'),(cases[3],'E₁ ∝ B')]: axes[1].plot(Bs,energy_meV(Bs,case),label=label) axes[1].scatter([10],[energy_meV(10,baseline)],color='black',zorder=5) axes[1].set(xlabel='B, Тл',ylabel='E₁, мэВ',title='Одна точка не проверяет закон') ss=np.linspace(0,.8,300) axes[2].plot(ss,screened_ratio(ss,baseline),label='1 / (1+s)') axes[2].plot(ss,screened_ratio(ss,cases[4]),label='1 / (1−s)') axes[2].axhline(1,color='gray',ls=':',label='Без экранирования') axes[2].set(xlabel='s ≥ 0',ylabel='W / V₀',title='Оба графика конечны на отрезке') for ax in axes:ax.grid(alpha=.18);ax.legend(fontsize=9) fig.suptitle('Неверная формула тоже умеет рисовать красивую кривую',fontsize=17) fig.tight_layout(rect=(0,.02,1,.9));publication_layout(fig); fig.savefig(OUT/'ai_smooth_wrong.png',dpi=150, bbox_inches="tight");plt.close(fig) for row in rows: print(row['case'],['PASS' if v else 'FAIL' for v in row['passed']], f"norm={row['normalization']:.9f}", f"moment={row['radial_second_moment']:.9f}", f"delta_N={100*row['relative_change_on_doubling']:.6f}%") print('5 намеренных модификаций обнаружены; исходный файл не менялся.') print('Размерности записи диплома:',dim_archive.tolist(),'; размерности энергии:',dim_energy.tolist()) if __name__=='__main__':main()
Опыт 6. Контракты процессов и решатель, который убедительно решает не ту задачу
Запуск нового файла:
python 13_process_checks.py
Он не требует архивного notebook. Нужны NumPy, SciPy, SymPy и Matplotlib; зависимости устанавливаются той же командой из начала лабораторной.
Рядом с программой появятся восемь рисунков deep_*.png и process_checks_report.json с результатами и версиями среды. Скрипт использует обычные assert, поэтому запускайте его без флага -O, отключающего эти проверки.
Сначала проверяем соединения.
В игрушечном примере ,
, поэтому
и
на всей положительной сетке. Точный ответ известен до написания кода. Смените сетку только у
или её метку единиц: блок обязан отказаться от вычисления. Отдельные случаи в программе уже проверяют эти отказы.
Метки обозначают степени одной заранее согласованной единицы ядра; это демонстрация контракта, а не полноценная библиотека физических единиц.
Замените в копии screened знаменатель 1 - v.values * pi.values на 1 + v.values * pi.values. В этом примере получится
: проверка аналитического ответа должна остановить программу. После опыта верните исходную формулу. Контракт не скрывает ошибку автоматическим модулем или сменой знака.
Затем испытываем численный решатель. На отрезке поставим задачу
Не будем гадать правильный ответ. Сами назначим и вычислим правую часть:
Теперь дадим решателю только , границы и коэффициенты.
В коде вторая производная заменяется центральной разностью . Разложение Тейлора показывает, что её ошибка на гладкой функции имеет порядок
. Следовательно, в подходящем режиме уменьшение шага вдвое должно уменьшать ошибку решения примерно вчетверо. При
получено:
Внутренних узлов |
Шаг |
Максимальная ошибка относительно |
|---|---|---|
31 |
1/32 |
|
63 |
1/64 |
|
127 |
1/128 |
|
255 |
1/256 |
Измеренный порядок близок к 2. Это сильнее одного совпадения на одной сетке, но всё ещё проверка выбранного семейства гладких задач.

Что менять: в manufactured увеличивайте n по последовательности 31, 63, 127, 255; затем сравните laplace_factor=1.0 и 1.1. Правую часть оставьте прежней. Нижняя панель показывает ожидаемую ошибку после такой порчи.
Главная ловушка: с laplace_factor=1.1 код решает уже . На 127 узлах невязка его собственной дискретной системы около
— на вид прекрасный результат. Но ошибка относительно требуемого
примерно
. Невязка — разность левой и правой частей проверяемого уравнения; маленькая невязка неверно составленного уравнения не доказывает решение исходной задачи. Поэтому правую часть и точное решение мы получили отдельно от сборки численного оператора.
Оставшиеся проверки связывают опыт с теорией статьи: численное интегрирование матричных элементов сравнивается с ,
и
; Фурье-образы профилей — с аналитическими формфакторами; две композиции комплексных матриц — друг с другом.
Для правильное среднее единичного оператора равно 1, а ошибочное
psi.T @ psi даёт 0. Размеры массивов совпадают в обоих случаях: нужен семантический тест комплексного сопряжения.
Программа также проверяет символный пример веса 2 и потерю константы у дилогарифма. Она не воспроизводит девятипетлевое доказательство.
Все эти проверки — небольшие независимые опоры, которые помогают понять, что должен контролировать каждый блок большого расчёта.
Полный код контрактов, изготовленного решения и восьми рисунков: 13_process_checks.py
"""Independent teaching checks and figures; run: python 13_process_checks.py. This is a verification laboratory, not a validated graphene model. Requires numpy, scipy, sympy, matplotlib. Writes beside this file. """ from dataclasses import dataclass from pathlib import Path import json import platform import numpy as np import scipy from scipy.integrate import quad from scipy.linalg import solve_banded from scipy.special import spence import sympy as sp import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt def publication_layout(fig): """Readable labels at a 600 px article width; retain data and panel order.""" import textwrap from matplotlib.text import Text width, height = fig.get_size_inches() fig.set_size_inches(width, height * 1.22, forward=True) scale = width / 8.0 main, small = 14.0 * scale, 12.0 * scale for text in fig.findobj(Text): if text.get_text().strip(): text.set_fontsize(max(text.get_fontsize(), main)) if text.get_color() in ['#d77b27', '#ed8b36', '#df7b28', '#d77527']: text.set_color('#87470f') for ax in fig.axes: for tick in ax.get_xticklabels() + ax.get_yticklabels(): tick.set_fontsize(small) for label in [ax.xaxis.label, ax.yaxis.label]: txt = label.get_text() if '$' not in txt: label.set_text('\n'.join(textwrap.fill(s, 39) for s in txt.splitlines())) title = ax.get_title() if '$' not in title: columns = max(25, int(ax.get_position().width * 75)) ax.set_title('\n'.join(textwrap.fill(s, columns) for s in title.splitlines()), fontsize=main, pad=12) legend = ax.get_legend() if legend: for text in legend.get_texts(): text.set_fontsize(small) for line in ax.lines: line.set_linewidth(max(line.get_linewidth(), 1.8)) if fig._suptitle: txt = fig._suptitle.get_text() if '$' not in txt: fig._suptitle.set_text(textwrap.fill(txt, 57)) fig._suptitle.set_fontsize(max(main + 2, 18)) for text in fig.texts: if text is fig._suptitle: continue txt=text.get_text() if '$' not in txt: text.set_text('\n'.join(textwrap.fill(s, 68) for s in txt.splitlines())) if type(fig.get_layout_engine()).__name__ == 'PlaceHolderLayoutEngine': fig.tight_layout(rect=(.01,.10,.99,.91),h_pad=2,w_pad=2) if type(fig.get_layout_engine()).__name__ == 'ConstrainedLayoutEngine': fig.get_layout_engine().set(h_pad=.15, w_pad=.12) from matplotlib.patches import FancyArrowPatch, FancyBboxPatch, Circle OUT = Path(__file__).resolve().parent plt.rcParams.update({'font.family': 'DejaVu Sans', 'font.size': 12, 'axes.spines.top': False, 'axes.spines.right': False}) C = ['#175c95', '#de7624', '#24866d', '#a74567'] report = {'environment': {'python': platform.python_version(), 'numpy': np.__version__, 'scipy': scipy.__version__, 'sympy': sp.__version__, 'matplotlib': matplotlib.__version__}} @dataclass(frozen=True) class Sampled: q: np.ndarray values: np.ndarray unit_power: int # powers of the same agreed kernel unit convention: str def screened(v: Sampled, pi: Sampled) -> Sampled: """Static scalar RPA, one agreed convention, positive q; not dynamic RPA.""" for item in (v, pi): if item.q.ndim != 1 or item.values.shape != item.q.shape: raise ValueError('Grid and values must be one-dimensional and equal-sized') if len(item.q) == 0 or not np.all(np.isfinite(item.q)): raise ValueError('Empty or nonfinite grid') if np.any(item.q <= 0) or np.any(np.diff(item.q) <= 0): raise ValueError('Expected strictly increasing positive q') if not np.all(np.isfinite(item.values)): raise ValueError('Nonfinite input') if not np.array_equal(v.q, pi.q): raise ValueError('Different grids: use an explicit checked conversion') if v.unit_power != 1 or pi.unit_power != -1: raise ValueError('V Pi must be dimensionless in the agreed kernel unit') if v.convention != pi.convention: raise ValueError('Different response conventions') if np.any(v.values <= 0) or np.any(pi.values > 0): raise ValueError('Outside the stated static sign convention') den = 1 - v.values * pi.values result = v.values / den if not np.all(np.isfinite(result)) or np.any(result <= 0): raise ArithmeticError('Nonfinite or nonpositive result') return Sampled(v.q.copy(), result, 1, v.convention) def must_reject(v, pi): try: screened(v, pi) except ValueError: return True raise AssertionError('Invalid connection accepted') qgrid = np.array([0.2, 0.7, 1.4, 3.0]) v = Sampled(qgrid, 2 / qgrid, 1, 'toy-static-v1') pi = Sampled(qgrid, -0.2 * qgrid, -1, 'toy-static-v1') w = screened(v, pi) np.testing.assert_allclose(w.values / v.values, 1 / 1.4, rtol=1e-14) zero = Sampled(qgrid, np.zeros_like(qgrid), -1, pi.convention) np.testing.assert_array_equal(screened(v, zero).values, v.values) bad_grid = must_reject(v, Sampled(qgrid * 1.01, pi.values, -1, pi.convention)) bad_units = must_reject(v, Sampled(qgrid, pi.values, 1, pi.convention)) bad_sign = must_reject(v, Sampled(qgrid, -pi.values, -1, pi.convention)) bad_convention = must_reject(v, Sampled(qgrid, pi.values, -1, 'other')) wrong_w = v.values / (1 + v.values * pi.values) assert np.any(wrong_w > v.values) report['contracts'] = {'correct_ratio': (w.values / v.values).tolist(), 'wrong_grid_rejected': bad_grid, 'wrong_units_rejected': bad_units, 'wrong_sign_rejected': bad_sign, 'wrong_convention_rejected': bad_convention, 'wrong_denominator_detected': True} # Manufactured solution: errors against a continuous exact answer, not residual alone. def manufactured(n, mu=2.0, laplace_factor=1.0): h = 1 / (n + 1) x = h * np.arange(1, n + 1) exact = np.sin(np.pi * x) + 0.1 * np.sin(3 * np.pi * x) forcing = ((np.pi**2 + mu**2) * np.sin(np.pi * x) + 0.1 * (9 * np.pi**2 + mu**2) * np.sin(3 * np.pi * x)) bands = np.zeros((3, n)) bands[0, 1:] = -laplace_factor / h**2 bands[1] = 2 * laplace_factor / h**2 + mu**2 bands[2, :-1] = -laplace_factor / h**2 u = solve_banded((1, 1), bands, forcing) padded = np.r_[0, u, 0] applied = (laplace_factor * (2 * u - padded[:-2] - padded[2:]) / h**2 + mu**2 * u) return x, exact, u, h, np.max(abs(u - exact)), np.max(abs(applied-forcing)) ns = [31, 63, 127, 255] sols = [manufactured(n) for n in ns] errors = np.array([s[4] for s in sols]) hs = np.array([s[3] for s in sols]) orders = np.log(errors[:-1] / errors[1:]) / np.log(hs[:-1] / hs[1:]) assert np.all((orders > 1.95) & (orders < 2.05)) bad = manufactured(127, laplace_factor=1.1) assert bad[4] > 0.03 and bad[5] < 1e-8 report['manufactured'] = {'n': ns, 'h': hs.tolist(), 'max_errors': errors.tolist(), 'observed_orders': orders.tolist(), 'wrong_operator_error': float(bad[4]), 'wrong_operator_residual': float(bad[5])} # Perturbation: symbolic exact eigenvalue and separate numeric matrix solver. l = sp.symbols('lam', real=True) exact_E = (1 + l / 10 - sp.sqrt((1-l/2)**2 + sp.Rational(16,25)*l**2)) / 2 series = sp.series(exact_E, l, 0, 4) assert sp.expand(series.removeO()).coeff(l, 1) == sp.Rational(3,10) assert sp.expand(series.removeO()).coeff(l, 2) == -sp.Rational(4,25) for z in [0.003, 0.02, 0.13]: numeric = np.linalg.eigvalsh([[0.3*z, 0.4*z], [0.4*z, 1-0.2*z]])[0] assert abs(numeric - float(exact_E.subs(l,z))) < 1e-14 report['perturbation'] = str(series) # Matrix elements, including vanishing odd integrands. def psi0(x): return np.pi**(-0.25) * np.exp(-x*x/2) def psi1(x): return np.sqrt(2) * x * psi0(x) states = [psi0, psi1] matrices = [np.array([[quad(lambda x: a(x)*x**power*b(x), -np.inf, np.inf, epsabs=1e-12)[0] for b in states] for a in states]) for power in [1, 2]] np.testing.assert_allclose(matrices[0], [[0,1/np.sqrt(2)],[1/np.sqrt(2),0]], atol=1e-12) np.testing.assert_allclose(matrices[1], [[0.5,0],[0,1.5]], atol=1e-12) report['matrix_elements'] = [m.tolist() for m in matrices] # Approximate delta: integrates a smooth test function, not a sampled infinite spike. delta_values = [] for eps in [0.5, 0.2, 0.08]: a = 0.6 numerical = quad(lambda z: np.exp(-z*z/2)/np.sqrt(2*np.pi) * np.cos(a + eps*z), -10, 10, epsabs=1e-12)[0] analytic = np.cos(a) * np.exp(-eps**2/2) assert abs(numerical - analytic) < 1e-12 delta_values.append([eps, numerical]) report['delta_cosine'] = {'center': 0.6, 'limit': float(np.cos(0.6)), 'values': delta_values} # Form factors: real quadrature of a profile versus its analytic transform. sigma, shift = 0.45, 1.3 def density(x, center=0): return np.exp(-(x-center)**2/(2*sigma**2))/(np.sqrt(2*np.pi)*sigma) for k in [0.0, 0.4, 1.2, 2.7, 5.3]: numeric = quad(lambda x: (density(x,shift)+density(x,-shift))/2*np.cos(k*x), -np.inf, np.inf, epsabs=1e-12)[0] assert abs(numeric - np.exp(-sigma**2*k*k/2)*np.cos(shift*k)) < 1e-11 # Off-diagonal F_01 is imaginary: do not discard the sine part. f01 = 1j * quad(lambda x: psi0(x)*np.sin(k*x)*psi1(x), -np.inf, np.inf)[0] reference = 1j*k/np.sqrt(2)*np.exp(-k*k/4) assert abs(f01-reference) < 1e-11 f10_minus = 1j * quad(lambda x: psi1(x)*np.sin(-k*x)*psi0(x), -np.inf, np.inf)[0] assert abs(f01.conjugate()-f10_minus) < 1e-11 report['form_factors'] = {'profile_and_transition_tests': 'passed', 'sigma':sigma,'a':shift} # Symbol toy: wedge coefficient is a two-dimensional determinant. x, y = sp.symbols('x y', positive=True) def wedge(a, b): return sp.simplify(sp.diff(sp.log(a), x)*sp.diff(sp.log(b), y) - sp.diff(sp.log(a), y)*sp.diff(sp.log(b), x)) assert wedge(x,y) != 0 assert sp.simplify(wedge(x,y)+wedge(y,x)) == 0 F = sp.polylog(2,x) shifted = F + sp.pi**2/6 assert sp.simplify(sp.diff(F-shifted,x)) == 0 assert sp.simplify((F-shifted).subs(x,0)) == -sp.pi**2/6 report['symbol_toy'] = {'single_word_wedge': str(wedge(x,y)), 'symmetric_words_wedge': '0', 'constant_error_detected': True} # Two genuinely different evaluation paths for the same linear process. rng = np.random.default_rng(1729) def random_complex(shape): return rng.normal(size=shape) + 1j*rng.normal(size=shape) A, B, Cmat, D = [random_complex(s) for s in [(2,3),(4,2),(3,2),(2,3)]] left = np.kron(A,B) @ np.kron(Cmat,D) right = np.kron(A@Cmat, B@D) np.testing.assert_allclose(left,right,rtol=1e-12,atol=1e-12) psi = np.array([1,1j]) / np.sqrt(2) identity = np.eye(2) correct = np.vdot(psi, identity@psi) wrong = psi.T @ identity @ psi assert abs(correct-1) < 1e-14 and abs(wrong) < 1e-14 report['composition'] = {'max_error': float(np.max(abs(left-right))), 'identity_expectation': float(correct.real), 'without_conjugation': float(wrong.real)} def save(fig, name): publication_layout(fig); fig.savefig(OUT / name, dpi=155, bbox_inches='tight', facecolor='white') plt.close(fig) fig, ax = plt.subplots(3,1,figsize=(7.5,10),layout='constrained') xx = np.linspace(-3,3,501) for j,(f,label) in enumerate([(psi0(xx)*xx*psi0(xx),r'$\psi_0 x\psi_0$: площадь 0'), (psi0(xx)*xx*psi1(xx),r'$\psi_0 x\psi_1$: площадь $1/\sqrt{2}$')]): ax[0].plot(xx,f,label=label,color=C[j]) ax[0].axhline(0,color='gray',lw=.7); ax[0].legend(fontsize=10) ax[0].set(xlabel='x',title='Подынтегральные функции') for panel,m,title in zip(ax[1:],matrices,['Матрица U = x','Матрица U = x²']): panel.imshow(m,cmap='Blues',vmin=0,vmax=1.5) for i in range(2): for j in range(2): panel.text(j,i,f'{m[i,j]:.3f}',ha='center',va='center',color='black' if m[i,j]<1 else 'white',fontsize=18) panel.set(xticks=[0,1],yticks=[0,1],xlabel='Входное состояние n',ylabel='Выходная компонента m',title=title) save(fig,'deep_matrix.png') fig, ax = plt.subplots(1,2,figsize=(12,4.2),layout='constrained') xx = np.linspace(-3,3,1501) for eps,color in zip([0.6,0.3,0.12],C): ax[0].plot(xx,np.exp(-xx**2/(2*eps**2))/(np.sqrt(2*np.pi)*eps),color=color,label=f'ε = {eps}') ax[0].set(xlabel='x',ylabel='δ_ε(x)',title='Площадь каждого пика = 1');ax[0].legend() sources=np.array([-1.2,0,1.0]); weights=np.array([0.25,0.5,0.35]) parts=weights[:,None]*np.exp(-abs(xx[None,:]-sources[:,None]))/2 for part,pos in zip(parts,sources):ax[1].plot(xx,part,'--',alpha=.65,label=f'Источник x = {pos:g}') ax[1].plot(xx,parts.sum(axis=0),color=C[0],lw=2.5,label='Сумма откликов') ax[1].set(xlabel='x',ylabel='u(x)',title='L = −d²/dx² + 1: суперпозиция');ax[1].legend(fontsize=10) save(fig,'deep_delta.png') fig, ax = plt.subplots(1,3,figsize=(12,3.8),layout='constrained') for i,panel in enumerate(ax): panel.set(xlim=(-.5,2.5),ylim=(-1.1,1.3)); panel.axis('off') for pos,name in [(0,'x'),(2,'y')]: panel.scatter([pos,pos],[-.22,.22],s=55,color=C[0]);panel.text(pos,-.65,name,ha='center',fontsize=17) if i==0: for pos in [0,2]:panel.add_patch(FancyArrowPatch((pos,-.22),(pos,.22),connectionstyle='arc3,rad=1.3',arrowstyle='-',lw=2,color=C[1])) else: for sign in [-1,1]:panel.plot([0,2],[sign*.22,sign*.22*(1 if i==1 else -1)],color=C[2],lw=2) panel.set_title(['G(x,x) G(y,y)','G(x,y)²','G(x,y)²'][i]) panel.text(1,1.05,['Несвязный вклад','Первое соединение','Второе соединение'][i],ha='center') fig.suptitle('Четыре поля → три спаривания → связный ответ 2 G(x,y)²',fontsize=15) save(fig,'deep_wick.png') fig,ax=plt.subplots(1,2,figsize=(11,4.4),layout='constrained') pts=np.array([[0,0],[1.6,-.2],[2.4,1.2],[.8,2.]]) for i in range(4): j=(i+1)%4 ax[0].annotate('',xy=pts[i],xytext=pts[j],arrowprops={'arrowstyle':'->','color':C[0],'lw':2}) mid=(pts[i]+pts[j])/2 ax[0].text(*mid,f' p{j if j else 4}',fontsize=13) ax[0].text(*(pts[i]+[.05,.1]),f'x{i}',fontsize=14) ax[0].set(xlim=(-.6,3),ylim=(-.7,2.7),title='pᵢ = xᵢ₋₁ − xᵢ; сумма pᵢ = 0');ax[0].axis('off') center=np.array([1.1,.8]) for i,pt in enumerate(pts): ax[1].plot([*pt[:1],center[0]],[pt[1],center[1]],color=C[2],lw=2) ax[1].text(*(pt+[.04,.1]),f'x{i}',fontsize=14) mid=(pt+center)/2;ax[1].text(*mid,f'D{i+1}',fontsize=12) ax[1].scatter(*center,s=60,color=C[1]);ax[1].text(*(center+[.08,-.15]),'y = −ℓ',fontsize=14) ax[1].set(xlim=(-.6,3),ylim=(-.7,2.7),title='Дуальный граф: Dᵢ = (y − xᵢ₋₁)²');ax[1].axis('off') save(fig,'deep_dual.png') fig,ax=plt.subplots(1,2,figsize=(12,4.2),layout='constrained') xx=np.linspace(-3,3,601);kk=np.linspace(0,7,501) ax[0].plot(xx,density(xx),label='Один центр',color=C[0]);ax[0].plot(xx,(density(xx,shift)+density(xx,-shift))/2,label='Два центра',color=C[1]) ax[1].plot(kk,np.exp(-sigma**2*kk**2/2),label='Один центр',color=C[0]);ax[1].plot(kk,np.exp(-sigma**2*kk**2/2)*np.cos(shift*kk),label='Два центра',color=C[1]) for panel in ax:panel.legend();panel.axhline(0,color='gray',lw=.6) ax[0].set(xlabel='x',ylabel='ρ(x)',title='Нормированные профили');ax[1].set(xlabel='k',ylabel='F(k)',title='Отклик на пространственную гармонику') save(fig,'deep_form.png') fig,ax=plt.subplots(1,2,figsize=(12,4.2),layout='constrained') ax[0].axis('off') ax[0].text(.03,.85,'Проверка интегрируемости (вес 2)',fontsize=15) ax[0].text(.04,.6,'x ⊗ y → dx ∧ dy / (xy) ≠ 0',color=C[3],fontsize=14) ax[0].text(.04,.35,'x ⊗ y + y ⊗ x → 0',color=C[2],fontsize=14) ax[0].text(.04,.12,'Порядок букв имеет значение',fontsize=12) xx=np.linspace(0,.95,301) ax[1].plot(xx,spence(1-xx),label='Li₂(x)',color=C[0]);ax[1].plot(xx,spence(1-xx)+np.pi**2/6,label='Li₂(x) + π²/6',color=C[1]) ax[1].scatter([0,0],[0,np.pi**2/6],color=[C[0],C[1]],zorder=3) ax[1].set(xlabel='x',title='Один символ, разные значения при x = 0');ax[1].legend() save(fig,'deep_symbol.png') fig,ax=plt.subplots(figsize=(8,9));ax.set(xlim=(0,8),ylim=(0,9));ax.axis('off') labels=[('Параметры и соглашения','B, vF, ε; единицы и заполнения'),('Базис и переходы','энергии, F_ab(q); порядок индексов'),('Статический отклик','Π(q); знак, нормировка, отсечение N'),('Экранирование','W = V / (1 − VΠ); та же сетка'),('Матричный элемент','P; меры интеграла, границы и веса')] tests=['Размерности, допустимые значения','Нормировка, F_ab(0), сопряжение','Предельный режим, N → 2N','Π = 0; 0 < W ≤ V','Другой метод, хвосты, сходимость'] for i,((label,detail),check) in enumerate(zip(labels,tests)): yy=7.4-i*1.55 ax.add_patch(FancyBboxPatch((.25,yy),7.5,1.25,boxstyle='round,pad=.06',fc='#eaf3fa',ec=C[0])) ax.text(.5,yy+.95,label,fontsize=16,weight='bold') ax.text(.5,yy+.57,detail,fontsize=14) ax.text(.5,yy+.2,'Проверка: '+check,fontsize=14,color=C[2]) if i<4:ax.annotate('',xy=(4,yy-.27),xytext=(4,yy-.08),arrowprops={'arrowstyle':'->','lw':1.5}) ax.text(4,.35,'Сквозная проверка:\nисходные параметры → известный ответ',ha='center',fontsize=14,weight='bold') fig.suptitle('Процессная декомпозиция: у каждого соединения есть контракт',fontsize=16) save(fig,'deep_pipeline.png') fig,ax=plt.subplots(3,1,figsize=(8,10),layout='constrained') xx,truth,uu,*_=sols[1] ax[0].plot(xx,truth,label='Заданное u*',color=C[0]);ax[0].plot(xx,uu,'--',label='Решатель',color=C[1]) ax[0].set(xlabel='x',title='Изготовленное решение');ax[0].legend() ax[1].loglog(hs,errors,'o-',label='Ошибка',color=C[0]);ax[1].loglog(hs,errors[0]*(hs/hs[0])**2,'--',label='Ориентир h²',color=C[1]) ax[1].set(xlabel='Шаг h',ylabel='max |u − u*|',title='Уменьшение шага');ax[1].legend() ax[2].plot(bad[0],bad[2]-bad[1],color=C[3]);ax[2].set(xlabel='x',ylabel='u_wrong − u*',title='Неверный коэффициент: 1 → 1.1') ax[2].text(.08,.15,'Невязка мала,\nно решено другое уравнение',transform=ax[2].transAxes,fontsize=11) save(fig,'deep_manufactured.png') report['status']='all assertions passed' (OUT/'process_checks_report.json').write_text(json.dumps(report,ensure_ascii=False,indent=2),encoding='utf8') print(json.dumps(report,ensure_ascii=False,indent=2))
Опыт 7. Проверяем дополнительные математические связи
Сохраните следующий полный листинг как 14_precision_checks.py и выполните:
python 14_precision_checks.py
Он создаёт 13 рисунков precision_*.png и журнал precision_checks_report.json. Используются те же библиотеки; запускать нужно без -O, чтобы работали assert. Проверки имеют разные основания: решение маленькой линейной системы сравнивается с RPA-дробью; точная алгебра проверяет калибровки и карту на ; квадратура сравнивается с аналитическими интегралами; рациональные кандидаты перечисляются явно.
Что менять в своей копии |
Ожидаемый результат |
Какой вывод допустим |
|---|---|---|
Значения в списке |
Старые четыре условия сохраняются; значение при |
Старые условия не определяли весь полином. |
Взять только один из модулей 11 и 13 |
При прежних границах появятся дополнительные дроби |
Недостаток данных реконструкции виден непосредственно. |
Поменять векторный потенциал Ландау на симметричный |
Ротор остаётся |
Выбор калибровки меняет представление, а не это поле. |
Поменять матрицу |
Меняется действие на вторую подсистему |
Порядок множителей и индексов является частью контракта. |
Перенести проверочную точку bootstrap обратно в уже использованный набор |
Проверка перестаёт различать показанные кандидаты |
Повторение условий построения не даёт нового свидетельства. |
В опыте с рациональными числами нельзя расширить границы и продолжать ссылаться на прежнюю гарантию: неравенство нужно проверять заново. В гауссовом рисунке выборочная ковариация только приближает точную матрицу; случайные точки иллюстрируют формулу, а не доказывают её. Такие различия между рисунком, тестом и доказательством — часть того же контроля научного кода.
Полный код 13 новых рисунков и проверок: 14_precision_checks.py
"""Thirteen explanatory figures and independent checks for the Habr article. Run with Python 3.12, without -O: python 14_precision_checks.py Requires numpy, scipy, sympy, matplotlib. Results go beside this file. These are small teaching models, not a nine-loop amplitude computation. """ from pathlib import Path from fractions import Fraction from math import gcd import json import platform import numpy as np import scipy from scipy.special import roots_legendre import sympy as sp import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt def publication_layout(fig): """Readable labels at a 600 px article width; retain data and panel order.""" import textwrap from matplotlib.text import Text width, height = fig.get_size_inches() fig.set_size_inches(width, height * 1.22, forward=True) scale = width / 8.0 main, small = 14.0 * scale, 12.0 * scale for text in fig.findobj(Text): if text.get_text().strip(): text.set_fontsize(max(text.get_fontsize(), main)) if text.get_color() in ['#d77b27', '#ed8b36', '#df7b28', '#d77527']: text.set_color('#87470f') for ax in fig.axes: for tick in ax.get_xticklabels() + ax.get_yticklabels(): tick.set_fontsize(small) for label in [ax.xaxis.label, ax.yaxis.label]: txt = label.get_text() if '$' not in txt: label.set_text('\n'.join(textwrap.fill(s, 39) for s in txt.splitlines())) title = ax.get_title() if '$' not in title: columns = max(25, int(ax.get_position().width * 75)) ax.set_title('\n'.join(textwrap.fill(s, columns) for s in title.splitlines()), fontsize=main, pad=12) legend = ax.get_legend() if legend: for text in legend.get_texts(): text.set_fontsize(small) for line in ax.lines: line.set_linewidth(max(line.get_linewidth(), 1.8)) if fig._suptitle: txt = fig._suptitle.get_text() if '$' not in txt: fig._suptitle.set_text(textwrap.fill(txt, 57)) fig._suptitle.set_fontsize(max(main + 2, 18)) for text in fig.texts: if text is fig._suptitle: continue txt=text.get_text() if '$' not in txt: text.set_text('\n'.join(textwrap.fill(s, 68) for s in txt.splitlines())) if type(fig.get_layout_engine()).__name__ == 'PlaceHolderLayoutEngine': fig.tight_layout(rect=(.01,.10,.99,.91),h_pad=2,w_pad=2) if type(fig.get_layout_engine()).__name__ == 'ConstrainedLayoutEngine': fig.get_layout_engine().set(h_pad=.15, w_pad=.12) from matplotlib.patches import FancyArrowPatch, FancyBboxPatch OUT = Path(__file__).resolve().parent FIGURES = [] COL = ['#17668b', '#df7b28', '#26816b', '#994867'] plt.rcParams.update({'font.family': 'DejaVu Sans', 'font.size': 11, 'axes.spines.top': False, 'axes.spines.right': False}) report = {'environment': {'python': platform.python_version(), 'numpy': np.__version__, 'scipy': scipy.__version__, 'sympy': sp.__version__, 'matplotlib': matplotlib.__version__}} def save(fig, name): publication_layout(fig); fig.savefig(OUT / f'precision_{name}.png', dpi=150, bbox_inches='tight', facecolor='white') FIGURES.append(f'precision_{name}.png') plt.close(fig) def box(ax, xy, label, width=2.1, height=.8, color=COL[0]): x, y = xy ax.add_patch(FancyBboxPatch((x-width/2, y-height/2), width, height, boxstyle='round,pad=.08', ec=color, fc='white', lw=1.8)) ax.text(x, y, label, ha='center', va='center', fontsize=11) def arrow(ax, start, end, color=COL[0], **kwargs): ax.add_patch(FancyArrowPatch(start, end, arrowstyle='-|>', mutation_scale=14, color=color, lw=1.7, **kwargs)) def finish_axes(axes): for ax in np.asarray(axes).flat: ax.grid(alpha=.18) # A response to the total potential is not a response to the external one. v0, pi0, uext = 2., -.4, 1. # Unknown vector: (U_total, delta_n). utot, dn = np.linalg.solve([[1, -v0], [-pi0, 1]], [uext, 0]) chi = pi0 / (1-v0*pi0) assert abs(dn/uext-chi) < 1e-14 assert abs(utot/uext-1/(1-v0*pi0)) < 1e-14 assert abs(v0+v0*chi*v0-v0/(1-v0*pi0)) < 1e-14 partial = sum(Fraction(2)*(-Fraction(4,5))**n for n in range(21)) exact = Fraction(2)/(1+Fraction(4,5)) error = exact-partial residual = (1+Fraction(4,5))*partial-2 assert residual == -(1+Fraction(4,5))*error report['response'] = {'Pi0': pi0, 'chi': chi, 'U_total': float(utot), 'partial_21': float(partial), 'error': float(error), 'residual': float(residual)} # A counterexample to uniqueness outside the restricted bootstrap ansatz. x, c = sp.symbols('x c', real=True) F = 2*x-x**2+c*x**2*(1-x)*(x-sp.Rational(1,2)) assert [F.subs(x,0), F.subs(x,1), sp.diff(F,x).subs(x,0), F.subs(x,sp.Rational(1,2))] == [0,1,2,sp.Rational(3,4)] assert sp.expand(F.subs(x,sp.Rational(1,4))) == sp.Rational(7,16)-3*c/256 report['bootstrap'] = {'held_out_quarter': str(sp.expand(F.subs(x,sp.Rational(1,4))))} # Two vector potentials give the same magnetic field, and differ by a gradient. x, y, B = sp.symbols('x y B', real=True) AL = sp.Matrix([0, B*x]) AS = sp.Matrix([-B*y/2, B*x/2]) gauge_f = B*x*y/2 assert sp.simplify(AL-AS-sp.Matrix([sp.diff(gauge_f,x),sp.diff(gauge_f,y)])) == sp.zeros(2,1) for A in [AL, AS]: assert sp.diff(A[1],x)-sp.diff(A[0],y) == B report['gauge'] = 'two curls equal B; A_L - A_S = gradient(Bxy/2)' # Projectors and the Gaussian covariance: different mathematical objects. state = np.array([np.sqrt(3)/2, .5]) P0 = np.diag([1., 0.]) assert np.array_equal(P0@P0, P0) assert abs(np.vdot(state,P0@state)-.75) < 1e-14 Amat = np.array([[2., -1.], [-1., 2.]]) cov = np.linalg.inv(Amat) rng = np.random.default_rng(1729) cloud = rng.multivariate_normal([0,0], cov, 5000) cov_empirical = np.cov(cloud.T) assert np.max(abs(cov_empirical-cov)) < .045 report['gaussian'] = {'exact_covariance': cov.tolist(), 'empirical_covariance': cov_empirical.tolist(), 'note': 'finite random sample; not a proof of Gaussian identities'} # Three-point Gauss-Legendre integrates degree <=5 exactly (up to rounding). nodes, weights = roots_legendre(3) moments = [] for degree in range(6): numeric = np.dot(weights, nodes**degree) expected = 0 if degree % 2 else 2/(degree+1) assert abs(numeric-expected) < 2e-14 moments.append(float(numeric)) report['quadrature'] = {'nodes': nodes.tolist(), 'weights': weights.tolist(), 'moments_0_to_5': moments} # Exact dual kinematic map. a,b,c are form-factor variables, not loop momenta. a, b = sp.symbols('a b', positive=True) cc = 1-a-b us = b*cc/((1-b)*(1-cc)) vs = cc*a/((1-cc)*(1-a)) ws = a*b/((1-a)*(1-b)) assert sp.factor((1-us-vs-ws)**2-4*us*vs*ws) == 0 center = [sp.simplify(t.subs({a:sp.Rational(1,3),b:sp.Rational(1,3)})) for t in [us,vs,ws]] assert center == [sp.Rational(1,4)]*3 report['dual_map'] = {'Delta': 0, 'symmetric_point': [str(t) for t in center]} def antipode_word(word): return (-1)**len(word), tuple(reversed(word)) for word in [('a',), ('a','b'), ('a','b','c')]: s1, rev = antipode_word(word) s2, original = antipode_word(rev) assert s1*s2 == 1 and original == word report['antipode'] = 'reversal squared is identity, including the signs' def candidates(modulus, residue, max_num=5, max_den=7): """Transparent exhaustive teaching oracle; not a production reconstruction.""" return [(aa,bb) for bb in range(1,max_den+1) for aa in range(-max_num,max_num+1) if gcd(abs(aa),bb)==1 and gcd(bb,modulus)==1 and (aa-residue*bb) % modulus == 0] res11 = 3*pow(5,-1,11) % 11 res13 = 3*pow(5,-1,13) % 13 combined = [r for r in range(143) if r%11==res11 and r%13==res13] assert combined == [115] one = candidates(11,res11) two = candidates(143,combined[0]) assert len(one)>1 and two == [(3,5)] assert 2*5*7 < 143 report['rational'] = {'residues': [res11,res13], 'CRT_residue': combined[0], 'candidates_mod11': one, 'candidates_mod143': two, 'bounds_A_B': [5,7], 'uniqueness_bound_2AB': 70} # A tensor product has independently constructed blocks A_ij B. AA = np.array([[1,2],[3,4]]) BB = np.array([[0,1],[1,0]]) explicit = np.block([[AA[0,0]*BB,AA[0,1]*BB], [AA[1,0]*BB,AA[1,1]*BB]]) np.testing.assert_array_equal(np.kron(AA,BB), explicit) ket0, ket1 = np.eye(2) np.testing.assert_array_equal(np.kron(np.eye(2),BB)@np.kron(ket1,ket0),np.kron(ket1,ket1)) report['tensor_product'] = explicit.tolist() # 1. Bands and hopping. Both panels are teaching models, not measured graphene data. fig, ax = plt.subplots(1,2,figsize=(12,4.8),layout='constrained') ax[0].axis('off');ax[0].set(xlim=(0,10),ylim=(0,5),title='Перескок — матричный элемент') for pos,label,col in [(2,'|A⟩',COL[0]),(8,'|B⟩',COL[1])]: ax[0].scatter(pos,3,s=950,color=col);ax[0].text(pos,3,label,ha='center',va='center',color='white',fontsize=17) arrow(ax[0],(2.7,3),(7.3,3));ax[0].text(5,3.4,'⟨A|H|B⟩ = −t',ha='center',fontsize=16) ax[0].text(5,1.5,'iℏ dψA/dτ = −t ψB\niℏ dψB/dτ = −t ψA',ha='center',fontsize=16) ax[0].text(5,.3,'t — энергия связи амплитуд',ha='center',fontsize=11) k = np.linspace(-1.7,1.7,400) for sign,col in [(1,COL[0]),(-1,COL[1])]: ax[1].plot(k,sign*np.sqrt(.6**2+k*k),color=col,label='Зона проводимости' if sign==1 else 'Валентная зона') ax[1].fill_between(k,-.6,.6,color='#dddddd',alpha=.5) ax[1].annotate('',xy=(0,.6),xytext=(0,-.6),arrowprops={'arrowstyle':'<->'}) ax[1].text(.08,0,'щель Eg',va='center');ax[1].set(xlabel='k, условные единицы',ylabel='E',title='Две зоны со щелью: учебная модель') ax[1].legend(fontsize=10,loc='upper center');save(fig,'bands') # 2. A localized packet requires a range of wave numbers. fig, ax = plt.subplots(1,2,figsize=(12,4.3),layout='constrained') xx = np.linspace(-7,7,1200);kk = np.linspace(-1,7,800);k0=3 ax[0].plot(xx,.55*np.cos(k0*xx),color='gray',alpha=.55,label='Одна плоская волна (Re ψ)') for width,col in [(1.,COL[0]),(2.,COL[1])]: psi=np.exp(-xx**2/(2*width**2))*np.cos(k0*xx) ax[0].plot(xx,psi,color=col,label=f'Пакет: ℓ = {width:g}') ax[1].plot(kk,np.exp(-width**2*(kk-k0)**2/2),color=col,label=f'ℓ = {width:g}') ax[0].set(xlabel='x',ylabel='Re ψ, произвольная нормировка',title='Локализация в пространстве') ax[1].set(xlabel='k',ylabel='|ψ̃(k)| / max',title='Широкий пакет → узкий спектр') for p in ax:p.legend(fontsize=10) finish_axes(ax);save(fig,'waves') # 3. Self-consistent response: route arrows outside labels and separate the panels. fig, ax = plt.subplots(1,2,figsize=(14,5.7),layout='constrained', gridspec_kw={'width_ratios':[1.65,1], 'wspace':.16}) ax[0].axis('off') ax[0].set(xlim=(-.2,16.7),ylim=(-.7,9),title='На какое поле отвечает Π?') for xy,label,width in [((1.6,6.5),r'$U_{\rm ext}$',2.8), ((7.3,6.5),r'$U_{\rm tot}$',3.0), ((13,6.5),r'$\delta n=\Pi U_{\rm tot}$',5.0), ((13,2.7),r'$U_{\rm ind}=V_0\delta n$',6.0)]: box(ax[0],xy,label,width=width,height=1.6) # The summing point has its own space, outside the potential boxes. ax[0].plot(4.3,6.5,'o',ms=24,mfc='white',mec=COL[0],mew=1.8) ax[0].text(4.3,6.5,'+',ha='center',va='center',fontsize=17) arrow(ax[0],(3.1,6.5),(3.93,6.5)) arrow(ax[0],(4.68,6.5),(5.72,6.5)) arrow(ax[0],(8.88,6.5),(10.42,6.5)) arrow(ax[0],(13,5.62),(13,3.58)) # Feedback follows an empty corridor; no line crosses a formula. ax[0].plot([9.92,4.3],[2.7,2.7],color=COL[0],lw=1.7) arrow(ax[0],(4.3,2.7),(4.3,6.05)) ax[0].text(8.1,.65,'ext: внешнее; tot: полное; ind: наведённое', ha='center',va='center',fontsize=11) ax[0].text(8.1,-.2,r'$\delta n=\chi U_{\rm ext}$', ha='center',va='center',fontsize=11) vv=np.linspace(0,6,200);response=pi0/(1-vv*pi0) ax[1].plot(vv,np.full_like(vv,pi0),label='Π₀ = −0.4',color=COL[1]) ax[1].plot(vv,response,label='χ = Π₀ / (1 − V₀Π₀)',color=COL[0]) ax[1].set(xlabel='V₀, согласованные единицы',ylabel='Отклик', title='Π₀ и χ различаются\nпри взаимодействии') ax[1].legend(loc='upper center',bbox_to_anchor=(.5,-.24),frameon=False) save(fig,'response') # 4. Projection and occupation are different uses of state labels. fig, ax = plt.subplots(3,1,figsize=(7.5,10),layout='constrained') p=ax[0];p.set(xlim=(-.1,1.1),ylim=(-.1,1.05),aspect='equal',title='Проектор выделяет компоненту') arrow(p,(0,0),tuple(state));arrow(p,(0,0),(state[0],0),COL[1]);p.plot([state[0]]*2,[0,state[1]],'--',color='gray') p.set(xlabel='|0⟩',ylabel='|1⟩');p.text(.03,.87,'ψ = √3/2 |0⟩ + 1/2 |1⟩',fontsize=10);p.text(.1,.15,'⟨ψ|P₀|ψ⟩ = 3/4',fontsize=11) for panel,title,counts in [(ax[1],'Фермионы: 0 или 1 в состоянии',[1,1,0]),(ax[2],'Бозоны: 0, 1, 2, …',[3,0,1])]: panel.set(xlim=(0,3),ylim=(0,3.5),title=title);panel.axis('off') for i,n in enumerate(counts): yy=.7+i;panel.hlines(yy,.3,2.7,color='gray');panel.text(.05,yy,str(i),va='center') for j in range(n):panel.scatter(.8+.6*j,yy,s=140,color=COL[0] if panel is ax[1] else COL[1]) panel.text(1.5,.05,'Метка означает полное\nодночастичное состояние',ha='center',fontsize=9) save(fig,'states') # 5. Two gauges, the same curl. fig,ax=plt.subplots(1,2,figsize=(11,4.8),layout='constrained');xy=np.linspace(-2,2,9);X,Y=np.meshgrid(xy,xy) for p,U,V,title in [(ax[0],0*X,X,'Калибровка Ландау: A = (0, Bx)'),(ax[1],-Y/2,X/2,'Симметричная: A = (−By/2, Bx/2)')]: p.quiver(X,Y,U,V,color=COL[0],scale=20);p.set(xlabel='x',ylabel='y',aspect='equal',title=title) p.text(.5,.02,'∂x Ay − ∂y Ax = B',transform=p.transAxes,ha='center',bbox={'fc':'white','ec':'none'}) fig.suptitle('Стрелки показывают A, а не движение электронов. Поле B одинаково.',fontsize=13) save(fig,'gauge') # 6. Gaussian covariance and the meaning of a two-point function. fig,ax=plt.subplots(1,2,figsize=(11,4.5),layout='constrained') ax[0].scatter(*cloud[:1600].T,s=4,alpha=.15,color=COL[0]);ax[0].set(xlabel='φ₁',ylabel='φ₂',aspect='equal',title='Гауссов вес: exp(−φᵀ A φ / 2)') ax[1].imshow(cov,cmap='Blues',vmin=0,vmax=.8) for i in range(2): for j in range(2):ax[1].text(j,i,'2/3' if i==j else '1/3',ha='center',va='center',fontsize=23,color='white' if i==j else 'black') ax[1].set(xticks=[0,1],yticks=[0,1],xticklabels=['φ₁','φ₂'],yticklabels=['φ₁','φ₂'],title='⟨φᵢφⱼ⟩ = (A⁻¹)ᵢⱼ при J = 0') save(fig,'gaussian') # 7. Quadrature nodes are not sampled area elements of equal width. fig,ax=plt.subplots(1,2,figsize=(11,4.3),layout='constrained');xx=np.linspace(-1,1,300) ax[0].plot(xx,xx**4,color=COL[0],label='f(x) = x⁴');ax[0].scatter(nodes,nodes**4,color=COL[1],zorder=4) for z in nodes:ax[0].axvline(z,ls=':',color='gray',alpha=.6) ax[0].set(xlabel='x',ylabel='f(x)',title='3 узла Гаусса на [−1, 1]');ax[0].legend() ax[1].bar(np.arange(3),weights,color=COL[0]);ax[1].set(xticks=range(3),xticklabels=['−√(3/5)','0','√(3/5)'],ylabel='Вес',title='Веса 5/9, 8/9, 5/9') fig.suptitle('Σ wᵢ xᵢ⁴ = 2/5 = ∫₋₁¹ x⁴ dx. Точность здесь следует из степени полинома.',fontsize=13) save(fig,'quadrature') # 8. Geometry, collinearity, and a complex logarithm cut are distinct notions. fig,ax=plt.subplots(3,1,figsize=(7.5,10),layout='constrained') xx=np.linspace(-1,1,101);ax[0].plot(xx,abs(xx),color=COL[0]);ax[0].plot(xx,-abs(xx),color=COL[0]) ax[0].fill_between(xx,abs(xx),1,color=COL[0],alpha=.1);ax[0].set(xlabel='x',ylabel='ct',title='Световой конус: (ct)² − x² = 0',aspect='equal') ax[1].axis('off');ax[1].set(xlim=(0,3),ylim=(-.5,2.6),title='Коллинеарный предел: θ → 0') arrow(ax[1],(.3,.3),(2.5,.3));arrow(ax[1],(.3,.3),(2.1,1.8),COL[1]);arrow(ax[1],(.3,.3),(2.4,.55),COL[2]);ax[1].text(1.2,1.15,'наклон уменьшается',rotation=35,fontsize=10) ax[2].plot([-2,0],[0,0],color=COL[3],lw=4);ax[2].axhline(0,color='gray',lw=.5);ax[2].axvline(0,color='gray',lw=.5) ax[2].scatter([-1,-1],[.13,-.13],color=[COL[0],COL[1]]);ax[2].text(-1,.55,'log(−1 + i0) = +iπ',ha='center',fontsize=10);ax[2].text(-1,-.6,'log(−1 − i0) = −iπ',ha='center',fontsize=10) ax[2].set(xlim=(-2.1,.5),ylim=(-1,1),xlabel='Re z',ylabel='Im z',title='Разрез главной ветви логарифма') save(fig,'limits') # 9. A held-out point detects freedom invisible to four earlier constraints. fig,ax=plt.subplots(1,2,figsize=(11,4.5),layout='constrained');xx=np.linspace(0,1,401) for coef,col in zip([-12,0,12],COL): ff=2*xx-xx**2+coef*xx**2*(1-xx)*(xx-.5) ax[0].plot(xx,ff,color=col,label=f'c = {coef}') ax[1].plot(xx,ff-(2*xx-xx**2),color=col,label=f'c = {coef}') ax[0].scatter([0,.5,1],[0,.75,1],color='black',zorder=4);ax[0].set(xlabel='x',ylabel='F(x)',title='Все проходят четыре прежних условия');ax[0].legend() ax[1].axvline(.25,color='gray',ls=':');ax[1].set(xlabel='x',ylabel='F(x) − (2x − x²)',title='Новая точка x = 1/4 различает ответы') finish_axes(ax);save(fig,'bootstrap') # 10. A computed parametrization of the parity-preserving surface. aa,bb=np.meshgrid(np.linspace(.025,.95,31),np.linspace(.025,.95,31));mask=aa+bb<.975 aa,bb=aa[mask],bb[mask];ccc=1-aa-bb uu=bb*ccc/((1-bb)*(1-ccc));vv=ccc*aa/((1-ccc)*(1-aa));ww=aa*bb/((1-aa)*(1-bb)) assert np.max(abs((1-uu-vv-ww)**2-4*uu*vv*ww)) < 1e-13 fig=plt.figure(figsize=(12,6));p=fig.add_subplot(121) p.scatter(aa,bb,c=aa,cmap='viridis',s=10);p.plot([0,1,0,0],[0,0,1,0],color='gray');p.scatter([1/3],[1/3],color=COL[1],s=90,edgecolor='black') p.set(xlabel='a',ylabel='b',aspect='equal',title='Формфактор: a + b + c = 1') p=fig.add_subplot(122,projection='3d');p.scatter(uu,vv,ww,c=aa,cmap='viridis',s=9);p.scatter([.25],[.25],[.25],color=COL[1],s=70,edgecolor='black') p.set(xlabel='u',ylabel='v',zlabel='w',title='Образ карты лежит на Δ = 0');p.view_init(elev=25,azim=45) fig.suptitle('Одинаковый цвет связывает точку и её образ',fontsize=17) fig.subplots_adjust(top=.77,bottom=.12,left=.06,right=.96,wspace=.28) save(fig,'dual') # 11. Word reversal and dihedral relabelling should not be confused. fig,ax=plt.subplots(1,2,figsize=(12,4.3),layout='constrained') ax[0].axis('off');ax[0].set(xlim=(0,10),ylim=(0,5),title='Антипод: порядок позиций в слове') for yy,letters in [(3.6,['a','b','c','d']),(1.2,['d','c','b','a'])]: for j,letter in enumerate(letters):box(ax[0],(1.4+2.3*j,yy),letter,width=1.1,height=.8,color=COL['abcd'.index(letter)]) arrow(ax[0],(5,3),(5,1.9));ax[0].text(6.1,2.45,'знак (−1)⁴ = +1',fontsize=11) th=np.pi/2+np.arange(3)*2*np.pi/3;pts=np.c_[np.cos(th),np.sin(th)] ax[1].plot(*np.r_[pts,pts[:1]].T,color=COL[0]);ax[1].axis('off');ax[1].set(xlim=(-1.7,1.7),ylim=(-1.6,1.6),aspect='equal',title='Диэдральная симметрия меток') for pt,letter in zip(pts,['a','b','c']):ax[1].text(*(pt*1.2),letter,ha='center',va='center',fontsize=21) ax[1].text(0,-1.45,'(a,b,c) → (b,c,a) и (a,b,c) → (c,b,a)',ha='center',fontsize=10) save(fig,'words') # 12. The bounds are part of the certificate, not discovered by fitting. fig,ax=plt.subplots(1,2,figsize=(11,4.5),layout='constrained') allpairs=[(a,b) for b in range(1,8) for a in range(-5,6) if gcd(abs(a),b)==1] for p,solutions,title in [(ax[0],one,'Один модуль 11: остаток 5'),(ax[1],two,'Два модуля: остаток 115 по 143')]: p.scatter(*np.array(allpairs).T,s=18,color='#c7cdd1') p.scatter(*np.array(solutions).T,s=100,color=COL[0]) for a,b in solutions:p.annotate(f'{a}/{b}',(a,b),xytext=(5,5),textcoords='offset points',fontsize=10) p.set(xlabel='Числитель a',ylabel='Знаменатель b',xticks=range(-5,6,2),yticks=range(1,8),title=title) fig.suptitle('Заранее задано: |a| ≤ 5, 1 ≤ b ≤ 7, дробь сокращена. 2·5·7 < 143.',fontsize=13) save(fig,'rational') # 13. Tensor-product blocks, and an action on one subsystem. fig,ax=plt.subplots(1,2,figsize=(11,4.5),layout='constrained') ax[0].imshow(explicit,cmap='Blues',vmin=0,vmax=4) for i in range(4): for j in range(4):ax[0].text(j,i,str(explicit[i,j]),ha='center',va='center',fontsize=16,color='white' if explicit[i,j]>2 else 'black') ax[0].axvline(1.5,color=COL[1],lw=3);ax[0].axhline(1.5,color=COL[1],lw=3) ax[0].set(xticks=range(4),yticks=range(4),xticklabels=['00','01','10','11'],yticklabels=['00','01','10','11'],title='A ⊗ X: четыре блока Aᵢⱼ X') ax[1].axis('off');ax[1].set(xlim=(0,10),ylim=(0,5),title='I ⊗ X меняет только второй бит') for yy,start,end in [(3.5,'|1⟩','|1⟩'),(1.5,'|0⟩','|1⟩')]: ax[1].text(.3,yy,start,va='center',fontsize=19);arrow(ax[1],(1.5,yy),(8.5,yy));ax[1].text(9,yy,end,va='center',fontsize=19) box(ax[1],(5,3.5),'I',width=.8);box(ax[1],(5,1.5),'X',width=.8,color=COL[1]) ax[1].text(5,.25,'Порядок базиса: |00⟩, |01⟩, |10⟩, |11⟩',ha='center',fontsize=11) save(fig,'tensor') report['figures'] = sorted(FIGURES) assert len(report['figures']) == 13 report['status'] = 'all assertions passed' (OUT/'precision_checks_report.json').write_text(json.dumps(report,ensure_ascii=False,indent=2),encoding='utf8') print(json.dumps(report,ensure_ascii=False,indent=2))
Код схем чтения и трёх небольших проверок
Сохраните листинг ниже как 15_reading_maps.py и запустите python 15_reading_maps.py. Он создаст шесть nav_*.png и reading_maps_report.json. Первые три рисунка — схемы чтения и контроля; численные рисунки проверяются отдельно от рисования: матричным решением, вычислением собственных значений и прямой квадратурой формфакторов.
В опыте с невязкой меняйте eps: при том же кандидате ошибка остаётся 1, а невязка равна eps. В опыте со смешиванием меняйте gap и сравнивайте отношение lam*v/gap.
В опыте с формфактором сравнивайте значения при q=0 у диагонали и перехода. Это дополнение к семи опытам, а не новый расчёт девятипетлевой амплитуды.
Полный код шести схем и проверок: 15_reading_maps.py
"""Six reading maps and independently checked teaching examples. Run: python 15_reading_maps.py (Python 3.12, without -O). Requires numpy, scipy, matplotlib. Writes PNG and JSON beside this file. The diagrams are guides to the article, not physical validation. """ from pathlib import Path import json import platform import numpy as np import scipy from scipy.integrate import quad from scipy.special import j0 import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt def publication_layout(fig): """Readable labels at a 600 px article width; retain data and panel order.""" import textwrap from matplotlib.text import Text width, height = fig.get_size_inches() fig.set_size_inches(width, height * 1.22, forward=True) scale = width / 8.0 main, small = 14.0 * scale, 12.0 * scale for text in fig.findobj(Text): if text.get_text().strip(): text.set_fontsize(max(text.get_fontsize(), main)) if text.get_color() in ['#d77b27', '#ed8b36', '#df7b28', '#d77527']: text.set_color('#87470f') for ax in fig.axes: for tick in ax.get_xticklabels() + ax.get_yticklabels(): tick.set_fontsize(small) for label in [ax.xaxis.label, ax.yaxis.label]: txt = label.get_text() if '$' not in txt: label.set_text('\n'.join(textwrap.fill(s, 39) for s in txt.splitlines())) title = ax.get_title() if '$' not in title: columns = max(25, int(ax.get_position().width * 75)) ax.set_title('\n'.join(textwrap.fill(s, columns) for s in title.splitlines()), fontsize=main, pad=12) legend = ax.get_legend() if legend: for text in legend.get_texts(): text.set_fontsize(small) for line in ax.lines: line.set_linewidth(max(line.get_linewidth(), 1.8)) if fig._suptitle: txt = fig._suptitle.get_text() if '$' not in txt: fig._suptitle.set_text(textwrap.fill(txt, 57)) fig._suptitle.set_fontsize(max(main + 2, 18)) for text in fig.texts: if text is fig._suptitle: continue txt=text.get_text() if '$' not in txt: text.set_text('\n'.join(textwrap.fill(s, 68) for s in txt.splitlines())) if type(fig.get_layout_engine()).__name__ == 'PlaceHolderLayoutEngine': fig.tight_layout(rect=(.01,.10,.99,.91),h_pad=2,w_pad=2) if type(fig.get_layout_engine()).__name__ == 'ConstrainedLayoutEngine': fig.get_layout_engine().set(h_pad=.15, w_pad=.12) from matplotlib.patches import FancyBboxPatch, FancyArrowPatch OUT=Path(__file__).resolve().parent COL=["#145e82","#d77527","#228473","#8c4974"] FIGURES=[] plt.rcParams.update({"font.family":"DejaVu Sans","font.size":13, "axes.spines.top":False,"axes.spines.right":False}) report={"environment":{"python":platform.python_version(), "numpy":np.__version__,"scipy":scipy.__version__, "matplotlib":matplotlib.__version__}} def save(fig,name): filename="nav_"+name+".png" publication_layout(fig); fig.savefig(OUT/filename,dpi=155,bbox_inches="tight",facecolor="white") plt.close(fig) FIGURES.append(filename) def arrow(ax,start,end,color=COL[0],**kw): ax.add_patch(FancyArrowPatch(start,end,arrowstyle="-|>", mutation_scale=17,lw=1.8,color=color,**kw)) def card(ax,x,y,w,h,title,detail,color=COL[0],fontsize=14): import textwrap detail='\n'.join(textwrap.fill(line,max(21,int(w*6))) for line in detail.splitlines()) ax.add_patch(FancyBboxPatch((x-w/2,y-h/2),w,h, boxstyle="round,pad=.07",fc="#f6f9fc",ec=color,lw=1.6)) ax.text(x,y+.25,title,ha="center",va="center",fontsize=fontsize, color=color,weight="bold") ax.text(x,y-.23,detail,ha="center",va="center",fontsize=13) # A route through the existing article, not new scientific claims. fig,ax=plt.subplots(figsize=(8.3,8.0)) ax.set(xlim=(0,8),ylim=(0,8));ax.axis("off") stages=[ ("Сначала: контроль кода от ИИ","Постановка → независимые опоры → проверяемый результат"), ("Разделы 1–8: восстанавливаем диплом","Графен → состояния → отклик → матричный элемент"), ("Разделы 9–10: понимаем цену петли","Граф → интеграл → полный коэффициент учебного ряда"), ("Разделы 11–16: читаем результат Claude","Bootstrap → символ → функция → пределы проверки"), ("Раздел 17: проверяем своими руками","Команда → изменение параметра → ожидаемый эффект")] for i,(title,detail) in enumerate(stages): y=7.15-1.4*i card(ax,4,y,7.2,.96,title,detail,color=COL[i%4]) if i<4:arrow(ax,(4,y-.55),(4,y-.82)) fig.suptitle("Общий план статьи",fontsize=17) save(fig,"route") fig,ax=plt.subplots(figsize=(8.6,10)) ax.set(xlim=(0,8.6),ylim=(0,10.8));ax.axis("off") card(ax,4.15,10,7.6,.9,"1–2. Постановка и схема процессов", "Уравнения, единицы, область применимости, входы и выходы") card(ax,2.05,8.45,3.6,1.2,"3. Опорные ответы", "Точный пример\n+ отложенные случаи",COL[2],12.5) card(ax,6.2,8.45,3.5,1.2,"ИИ: код блоков", "Короткие функции\nпо явным контрактам",COL[1],12.5) arrow(ax,(3.1,9.48),(2.05,9.14));arrow(ax,(5.2,9.48),(6.2,9.14)) card(ax,4.15,6.7,6.6,1.0,"4–5. Проверки формул и ответов", "Числа, символы, симметрии, независимый метод") arrow(ax,(2.05,7.76),(3.1,7.28),COL[2]);arrow(ax,(6.2,7.76),(5.2,7.28),COL[1]) card(ax,4.15,5.2,6.6,1.0,"6. Сходимость и бюджет ошибок", "Шаг, отсечение, квадратура\nРазрядность и обусловленность") card(ax,4.15,3.7,6.6,1.0,"7–8. Испытание проверяющего", "Известные дефекты + заранее отложенные случаи") card(ax,4.15,2.2,6.6,1.0,"9. Воспроизводимый пакет", "Код, данные, версии, команды, результаты и ограничения") for y in [6.7,5.2,3.7]:arrow(ax,(4.15,y-.58),(4.15,y-.92)) ax.plot([7.7,8.25,8.25,7.7],[5.2,5.2,8.45,8.45], color=COL[3],lw=1.8) arrow(ax,(7.7,8.45),(7.5,8.45),COL[3]) ax.text(8.35,6.65,"разобрать сбой",rotation=90,ha="left",va="center", color=COL[3],fontsize=10.5) ax.text(4.15,.76,"Провал проверки возвращает нас к её причине:\n" "модели, реализации, данным или обоснованию самого теста.", ha="center",fontsize=12) fig.suptitle("Как организовать научный код с помощью ИИ",fontsize=18) save(fig,"workflow") fig,ax=plt.subplots(figsize=(8.4,7.4)) ax.set(xlim=(0,8),ylim=(0,7.8));ax.axis("off") rows=[ ("Модель и её область применимости","Сравнение с экспериментом или физическим ориентиром"), ("Параметры и исходные данные","Единицы, происхождение, неопределённость измерений"), ("Реализация уравнения","Размерности, аналитические случаи, независимый метод"), ("Численное приближение","Сетка, границы, число уровней, точность квадратуры"), ("Округление и чувствительность","Разрядность, масштабирование, число обусловленности")] for i,(title,detail) in enumerate(rows): card(ax,4,7-1.34*i,7.25,1.0,title,detail,color=COL[i%4],fontsize=14) ax.text(4,.35,"Это разные источники неопределённости, а не автоматически\n" "независимые случайные ошибки, которые можно сложить по квадратам.", ha="center",fontsize=11) fig.suptitle("Что именно мы проверяем?",fontsize=18) save(fig,"errors") # Exact conceptual counterexample, independently evaluated with matrix algebra. eps=1e-8 A=np.diag([1.,eps]); b=np.array([1.,0.]) truth=np.linalg.solve(A,b) candidate=np.array([1.,1.]) residual=b-A@candidate forward=np.linalg.norm(candidate-truth) backward=np.linalg.norm(residual) condition=np.linalg.cond(A) assert np.array_equal(truth,[1.,0.]) assert forward==1 and backward==eps assert np.isclose(condition,1/eps) np.testing.assert_allclose(candidate-truth,-np.linalg.solve(A,residual)) report["residual_example"]={"epsilon":eps,"candidate":candidate.tolist(), "exact":truth.tolist(),"residual_norm":backward,"error_norm":forward, "condition_number":condition} fig,ax=plt.subplots(2,1,figsize=(8.3,7.6), gridspec_kw={"height_ratios":[1,1.1]},layout="constrained") ax[0].axis("off") ax[0].text(.05,.84,"A = diag(1, ε), b = (1, 0)",fontsize=19) ax[0].text(.05,.57,"Точное u = (1, 0) Кандидат û = (1, 1)",fontsize=17) ax[0].text(.05,.29,"r = b − Aû = (0, −ε) ε = 10⁻⁸",fontsize=17) ax[0].text(.05,.04,"A⁻¹ усиливает вторую компоненту в 10⁸ раз",fontsize=14,color=COL[3]) ax[1].barh(["Норма невязки","Норма ошибки"],[backward,forward], color=[COL[2],COL[3]]) ax[1].set(xscale="log",xlim=(1e-9,10),xlabel="Евклидова норма, логарифмическая шкала") ax[1].grid(axis="x",alpha=.25) for i,v in enumerate([backward,forward]):ax[1].text(v*1.4,i,f"{v:g}",va="center") fig.suptitle("Малая невязка может соседствовать с большой ошибкой",fontsize=16) save(fig,"condition") # Nearly degenerate perturbation: symbolic eigenvalue vs independent eigensolver. lam=np.linspace(0,.25,301); v=1. gap_results=[] fig,axes=plt.subplots(2,1,figsize=(8.3,7.2),layout="constrained") for ax,gap in zip(axes,[1.,.05]): exact=(gap-np.sqrt(gap*gap+4*(lam*v)**2))/2 second=-(lam*v)**2/gap for z in [.002,.02,.12]: value=np.linalg.eigvalsh([[0,z*v],[z*v,gap]])[0] analytic=(gap-np.sqrt(gap*gap+4*(z*v)**2))/2 assert abs(value-analytic)<2e-16 ax.plot(lam,exact,color=COL[0],lw=2,label="Точная нижняя энергия") ax.plot(lam,second,"--",color=COL[1],lw=2,label="Второй порядок") if gap<lam[-1]: ax.axvspan(gap/v,lam[-1],color=COL[3],alpha=.08) ax.text(.14,-.92,"λv / Δ ≥ 1:\nслабого смешивания нет",fontsize=12) ax.set(xlabel="λ при v = 1 в выбранных единицах",ylabel="Энергия", title=f"Зазор Δ = {gap:g}") ax.grid(alpha=.2);ax.legend(fontsize=10,loc="lower left") gap_results.append({"gap":gap,"lambda":.12, "mixing_ratio":.12*v/gap, "exact_E":float(np.linalg.eigvalsh([[0,.12*v],[.12*v,gap]])[0]), "second_order_E":-(.12*v)**2/gap}) report["small_gap"]=gap_results fig.suptitle("Важна сила смешивания относительно зазора",fontsize=17) save(fig,"gap") # Two notions of indices: a 2D radial state vs a 1D oscillator transition. qs=np.linspace(0,4.5,301) radial00=np.exp(-qs*qs/2) radial10=(1-qs*qs/2)*radial00 osc00=np.exp(-qs*qs/4) imag01=qs/np.sqrt(2)*osc00 errors=[] for q in [0.,.4,1.2,2.7]: f00=quad(lambda r:r*np.exp(-r*r/2)*j0(q*r),0,np.inf,epsabs=1e-11)[0] f10=quad(lambda r:r**3/2*np.exp(-r*r/2)*j0(q*r),0,np.inf,epsabs=1e-11)[0] trans=quad(lambda x:np.sqrt(2)*x/np.sqrt(np.pi)*np.exp(-x*x)*np.sin(q*x), -np.inf,np.inf,epsabs=1e-11)[0] refs=[np.exp(-q*q/2),(1-q*q/2)*np.exp(-q*q/2),q/np.sqrt(2)*np.exp(-q*q/4)] errors.extend(abs(np.array([f00,f10,trans])-refs)) assert max(errors)<1e-10 report["form_factor_quadrature_max_error"]=float(max(errors)) fig,axes=plt.subplots(2,1,figsize=(8.3,8),layout="constrained") axes[0].plot(qs,radial00,label="Состояние α = (0, 0)",color=COL[0],lw=2) axes[0].plot(qs,radial10,label="Состояние α = (1, 0)",color=COL[1],lw=2) axes[0].scatter([0,0],[1,1],color=COL[0],zorder=5) axes[0].set(title=r"Плотность одного состояния: $\mathcal{F}_\alpha(0)=1$", ylabel=r"$\mathcal{F}_\alpha(q)$") axes[1].plot(qs,osc00,label="F₀₀: диагональ",color=COL[0],lw=2) axes[1].plot(qs,imag01,label="Im F₀₁: переход",color=COL[3],lw=2) axes[1].scatter([0,0],[1,0],color=[COL[0],COL[3]],zorder=5) axes[1].set(title="Между двумя состояниями: Fmn(0) = δmn",ylabel="Указанная компонента F") for ax in axes: ax.set_xlabel("q, безразмерный волновой вектор") ax.axhline(0,color="gray",lw=.6);ax.grid(alpha=.2);ax.legend(fontsize=11) fig.suptitle("Две роли индексов у формфактора",fontsize=18) save(fig,"formfactor") report["figures"]=FIGURES report["status"]="all assertions passed" (OUT/"reading_maps_report.json").write_text(json.dumps(report,ensure_ascii=False,indent=2),encoding="utf8") print(json.dumps(report,ensure_ascii=False,indent=2))
Воспроизводим учебные примеры
Сохраните следующий листинг как 16_teaching_examples.py в отдельную папку и выполните python 16_teaching_examples.py. Он создаст 24 изображения teach_*.png и файл teaching_examples_report.json. Программа не использует архив диплома и не скачивает данные. Пример правила Вика проверяется двумя способами: подсчётом спариваний и прямой двумерной гауссовой квадратурой.
Она проверяет начальную задачу независимым решателем, эволюцию двух состояний матричной экспонентой, нормировки квадратурой, стационарность действия прямым интегрированием и коэффициенты диаграмм перебором спариваний. Для гауссова примера напечатанные стандартные отклонения близки к 0.05 и 0.07; это выборочные оценки, поэтому не обязаны совпадать с теоретическими числами до последнего знака.
Что менять для самостоятельной работы: в примере двух состояний измените величину связи и проверьте масштаб времени; в вариационном примере замените допустимую функцию изменения на другую с нулевыми концами и снова проверьте отсутствие линейного члена; в опыте неопределённостей используйте общий случайный источник в обоих слагаемых и сравните с независимыми источниками. Сначала выпишите ожидаемое свойство, потом меняйте программу.
Полный код учебных определений, вариаций и диаграмм: 16_teaching_examples.py
```python """Worked illustrations for the Habr mini-textbook.
Run with Python 3.12: python 16_teaching_examples.py Requires numpy, scipy, matplotlib. No network or private input files. Figures and a numerical/combinatorial report are written beside this script. All examples are explicitly simplified models, not a graphene validation. “”" from pathlib import Path from fractions import Fraction import json import math import platform import numpy as np from scipy.integrate import quad, solve_ivp from scipy.linalg import expm import matplotlib matplotlib.use(“Agg”) import matplotlib.pyplot as plt
def publication_layout(fig): “”“Readable labels at a 600 px article width; retain data and panel order.”“” import textwrap from matplotlib.text import Text width, height = fig.get_size_inches() fig.set_size_inches(width, height * 1.22, forward=True) scale = width / 8.0 main, small = 14.0 * scale, 12.0 * scale for text in fig.findobj(Text): if text.get_text().strip(): text.set_fontsize(max(text.get_fontsize(), main)) if text.get_color() in [‘#d77b27’, ‘#ed8b36’, ‘#df7b28’, ‘#d77527’]: text.set_color(‘#87470f’) for ax in fig.axes: for tick in ax.get_xticklabels() + ax.get_yticklabels(): tick.set_fontsize(small) for label in [ax.xaxis.label, ax.yaxis.label]: txt = label.get_text() if ‘’ not in title: columns = max(25, int(ax.get_position().width * 75)) ax.set_title(‘\n’.join(textwrap.fill(s, columns) for s in title.splitlines()), fontsize=main, pad=12) legend = ax.get_legend() if legend: for text in legend.get_texts(): text.set_fontsize(small) for line in ax.lines: line.set_linewidth(max(line.get_linewidth(), 1.8)) if fig._suptitle: txt = fig._suptitle.get_text() if ‘
’ not in txt: text.set_text(‘\n’.join(textwrap.fill(s, 68) for s in txt.splitlines())) if type(fig.get_layout_engine()).name == ‘PlaceHolderLayoutEngine’: fig.tight_layout(rect=(.01,.10,.99,.91),h_pad=2,w_pad=2) if type(fig.get_layout_engine()).name == ‘ConstrainedLayoutEngine’: fig.get_layout_engine().set(h_pad=.15, w_pad=.12)
from matplotlib.patches import FancyBboxPatch, FancyArrowPatch, Circle
OUT = Path(file).resolve().parent C = [“#156783”, “#d77b27”, “#258471”, “#9a466b”] plt.rcParams.update({“font.family”: “DejaVu Sans”, “font.size”: 12, “axes.spines.top”: False, “axes.spines.right”: False}) REPORT = {“python”: platform.python_version(), “figures”: []}
def save(fig, name): publication_layout(fig); fig.savefig(OUT / (“teach_” + name + “.png”), dpi=155, bbox_inches=“tight”, facecolor=“white”) plt.close(fig) REPORT[“figures”].append(“teach_” + name + “.png”)
def panels(title, n=2, height=3.5): fig, ax = plt.subplots(1, n, figsize=(10, height), constrained_layout=True) fig.suptitle(title, fontsize=17) return fig, np.atleast_1d(ax)
def arrow(ax, p, q, color=C[0], **kw): ax.add_patch(FancyArrowPatch(p, q, arrowstyle=“-|>”, mutation_scale=15, color=color, lw=1.7, **kw))
def card(ax, x, y, w, h, text, color=C[0], size=12): ax.add_patch(FancyBboxPatch((x-w/2, y-h/2), w, h, boxstyle=“round,pad=.04”, fc=“#f3f7fa”, ec=color, lw=1.5)) ax.text(x, y, text, ha=“center”, va=“center”, fontsize=size, color=“#172b3a”)
def canvas(title, height=4): fig, axs = panels(title, 1, height) ax = axs[0] ax.set(xlim=(0, 10), ylim=(0, 5)) ax.axis(“off”) return fig, ax
1. Basis: coordinates change, the represented vector does not.
fig, ax = panels(“Базис: один вектор, два набора координат”) u = np.array([3., 4.]) Q = np.array([[1, -1], [1, 1]]) / np.sqrt(2) coords = Q.T @ u assert np.allclose(Q @ coords, u) for a, B, title in zip(ax, [np.eye(2), Q], [“Обычные оси: (3, 4)”, “Поворот осей: (7/√2, 1/√2)”]): a.set(xlim=(-1.8, 4.6), ylim=(-1, 5), aspect=“equal”, title=title) a.grid(alpha=.15) for j in range(2): arrow(a, (0, 0), B[:, j], C[j]) arrow(a, (0, 0), u, C[2]) a.text(2.8, 4.35, “u”, color=C[2]) a.set_xlabel(“x”); a.set_ylabel(“y”) save(fig, “basis”)
2. Initial conditions.
t = np.linspace(0, 3, 301) fig, ax = panels(“Начальное условие выбирает одно решение”, 1) for c in [1, 2, 3]: ax[0].plot(t, cnp.exp(-t), label=f"y(0) = {c}") ax[0].set(xlabel=“время τ”, ylabel=“y(τ)”, title=“Одно уравнение: y′ = −y”) ax[0].legend() sol = solve_ivp(lambda t, y: -y, [0, 3], [2.], t_eval=t, rtol=1e-10, atol=1e-12) REPORT[“initial_max_error”] = float(np.max(abs(sol.y[0]-2np.exp(-t)))) assert REPORT[“initial_max_error”] < 1e-9 save(fig, “initial”)
3. Boundary conditions for the same differential operator.
x = np.linspace(0, 1, 501) fig, ax = panels(“Граничные условия меняют решение −u″ = 1”, 1) ax[0].plot(x, x*(1-x)/2, label=“u(0)=0, u(1)=0”, color=C[0]) ax[0].plot(x, x-x*x/2, label=“u(0)=0, u′(1)=0”, color=C[1]) ax[0].set(xlabel=“x”, ylabel=“u(x)”) ax[0].legend() save(fig, “boundary”)
4. Normalization: amplitude and probability density.
fig, ax = panels(“Нормировка: делим амплитуду на корень из нормы”) ax[0].plot(x, x, label=“f(x)=x”) ax[0].plot(x, np.sqrt(3)x, label=“ψ(x)=√3 x”) ax[1].fill_between(x, xx, alpha=.25, color=C[0], label=“∫|f|² dx = 1/3”) ax[1].plot(x, 3xx, color=C[1], label=“∫|ψ|² dx = 1”) for a in ax: a.set_xlabel(“x ∈ [0,1]”); a.legend(fontsize=10) ax[0].set_ylabel(“амплитуда”); ax[1].set_ylabel(“плотность вероятности”) assert abs(quad(lambda z: 3zz, 0, 1)[0]-1) < 1e-13 save(fig, “normalization”)
5. Absolute and relative tolerances.
fig, ax = panels(“Допуск: единицы и масштаб ответа имеют значение”, 1) q = np.logspace(-8, 3, 300) bound = 1e-8 + 1e-5q ax[0].loglog(q, bound, label=“atol + rtol |эталон|”, color=C[0]) ax[0].loglog(q, np.full_like(q, 1e-8), “–”, label=“atol = 10⁻⁸”) ax[0].loglog(q, 1e-5q, “:”, label=“rtol |эталон|; rtol = 10⁻⁵”) ax[0].set(xlabel=“модуль эталонного значения”, ylabel=“допустимое отклонение”) ax[0].legend(fontsize=10) save(fig, “tolerance”)
6. Error and residual have different units.
fig, ax = panels(“Ошибка решения и невязка — разные величины”, 1) uh = np.linspace(1.8, 2.2, 101) ax[0].plot(uh, 3*uh, color=C[0]) ax[0].axhline(6, color=C[2], ls=“–”) ax[0].plot([2, 2.01], [6, 6.03], “o”, color=C[1]) ax[0].annotate(“ошибка u: 0.01 м”, xy=(2.005, 6), xytext=(1.82, 5.7), arrowprops={“arrowstyle”: “->”}) ax[0].annotate(“невязка: 0.03 Н”, xy=(2.01, 6.015), xytext=(2.04, 6.25), arrowprops={“arrowstyle”: “->”}) ax[0].set(xlabel=“u, м”, ylabel=“k u, Н”, title=“k = 3 Н/м; b = 6 Н; точное u = 2 м”) save(fig, “residual”)
7. Significant digits and exponent are separate concepts.
fig, ax = canvas(“Мантисса: сколько значащих цифр сохранено”, 3.8) card(ax, 2.4, 3.4, 4, 1.0, “Точное десятичное число\n1.23456”) arrow(ax, (4.6, 3.4), (5.3, 3.4)) card(ax, 7.6, 3.4, 4, 1.0, “Округление до 4 цифр\n1.235”, C[1]) ax.text(5, 1.8, “Абсолютная ошибка: 0.00044\nОтносительная: 0.00044 / 1.23456 ≈ 0.0356%”, ha=“center”, va=“center”, fontsize=14) ax.text(5, .6, “Учебный десятичный формат.\nОбычный float использует основание 2.”, ha=“center”, fontsize=11) save(fig, “precision”) fig, ax = canvas(“Порядок: та же мантисса, другой масштаб”, 3.1) for pos, exponent, value in [(1.7, -3, “0.001235”), (5, 0, “1.235”), (8.3, 3, “1235”)]: card(ax, pos, 3.0, 2.9, 1.5, f"1.235 × 10^{exponent}\n\n{value}") ax.text(5, 1.3, “Число значащих цифр в записи — 4 во всех трёх случаях.”, ha=“center”, fontsize=12) save(fig, “exponent”)
8. A stable evaluation of a well-conditioned function near zero.
xx = np.logspace(-16, -1, 200) stable = xx/(np.sqrt(1+xx)+1) naive = np.sqrt(1+xx)-1 relerr = np.abs(naive-stable)/stable fig, ax = panels(“Численная устойчивость: одинаковая формула, разный расчёт”) ax[0].loglog(xx, stable, label=“x / (√(1+x)+1)”, color=C[2]) ax[0].loglog(xx, np.maximum(naive, 1e-20), “–”, label=“√(1+x) − 1”, color=C[1]) ax[0].set(xlabel=“x”, ylabel=“значение”) ax[0].legend(fontsize=10) ax[1].loglog(xx, np.maximum(relerr, 1e-17), color=C[1]) ax[1].set(xlabel=“x”, ylabel=“относительное расхождение”) REPORT[“cancellation_at_1e-16”] = {“naive”: float(np.sqrt(1+1e-16)-1), “stable”: float(1e-16/(np.sqrt(1+1e-16)+1))} assert REPORT[“cancellation_at_1e-16”][“naive”] == 0 save(fig, “stability”)
9. Conditioning with exact arithmetic.
fig, ax = panels(“Обусловленность: точный ответ чувствителен к данным”, 1) labels = [“a=1.000001\nb=1”, “a=1.000002\nb=1”] answers = [1e-6, 2e-6] ax[0].bar(labels, answers, color=[C[0], C[1]]) ax[0].set(ylabel=“точная разность a − b”, title=“a меняется примерно на 0.0001%; ответ — на 100%”) assert Fraction(“1.000002”)-Fraction(1) == 2*(Fraction(“1.000001”)-Fraction(1)) save(fig, “conditioning”)
10. Budget and independence.
fig, ax = canvas(“Бюджет ошибок: источник → оценка → действие”, 5.6) rows = [(“Входные данные”, “стандартная неопределённость”, “уточнить измерение”), (“Сетка и отсечение”, “сходимость / остаток ряда”, “уменьшить шаг / расширить сумму”), (“Округление”, “устойчивость и разрядность”, “изменить способ вычисления”), (“Физическая модель”, “допущения и данные”, “проверить применимость”)] for i, (source, estimate, action_text) in enumerate(rows): yy=4.3-i*1.16 card(ax,5,yy,9.2,.96,‘’,C[i]) ax.text(.65,yy+.22,source,fontsize=17,weight=‘bold’,color=C[i],va=‘center’) ax.text(.65,yy-.22,estimate+'\n→ '+action_text,fontsize=14,va=‘center’) save(fig, “budget”)
rng = np.random.default_rng(314159) z = rng.standard_normal((200000, 2)) e1, e2 = .03z[:, 0], .04z[:, 1] independent = e1+e2 correlated = .03z[:, 0]+.04z[:, 0] fig, ax = panels(“Складываются дисперсии, если ковариация равна нулю”) ax[0].scatter(e1[:1200], e2[:1200], s=5, alpha=.18, color=C[0]) ax[0].set(xlabel=“ε₁”, ylabel=“ε₂”, title=“Независимые источники”) for values, label, color in [(independent, “независимые: σ = 0.05”, C[0]), (correlated, “общий источник: σ = 0.07”, C[1])]: ax[1].hist(values, bins=100, density=True, histtype=“step”, label=label, color=color) ax[1].set(xlabel=“суммарная погрешность”, ylabel=“плотность”, title=“3% и 4% в одном масштабе”) ax[1].legend(fontsize=9) REPORT[“sample_std”] = {“independent”: float(np.std(independent, ddof=1)), “correlated”: float(np.std(correlated, ddof=1))} assert abs(REPORT[“sample_std”][“independent”]-.05) < .0005 assert abs(REPORT[“sample_std”][“correlated”]-.07) < .0005 save(fig, “uncertainty”)
11. Test the tester with a deliberately altered probability density.
fig, ax = plt.subplots(2,1,figsize=(8,6.5),constrained_layout=True, gridspec_kw={‘height_ratios’:[2,1]}) fig.suptitle(“Проверка проверки: нормировки недостаточно”,fontsize=19) r = np.linspace(0, 4, 400) ax[0].plot(r, rnp.exp(-rr/2), label=“ρ₀ = r exp(−r²/2)”) ax[0].plot(r, 2rnp.exp(-rr), label=“ρ₁ = 2r exp(−r²)”, color=C[1]) ax[0].set(xlabel=“r”, ylabel=“радиальная плотность”) ax[0].legend(fontsize=9) M = [] for f in [lambda r: rnp.exp(-rr/2), lambda r: 2rnp.exp(-rr)]: M.append([quad(f, 0, np.inf)[0], quad(lambda r: rrf®, 0, np.inf)[0]]) assert np.allclose(M, [[1, 2], [1, 1]], atol=1e-12) ax[1].axis(“off”) table = ax[1].table(cellText=[[“Исходная”, “1 — проходит”, “2 — проходит”], [“Изменённая”, “1 — проходит”, “1 — не проходит”]], colLabels=[“Плотность”, “Нормировка = 1”, “Средний r² = 2”], loc=“center”, cellLoc=“center”) table.auto_set_font_size(False); table.set_fontsize(16); table.scale(1, 3) REPORT[“mutation_integrals”] = M save(fig, “tester”)
12. Quantum state, operator, observable and dynamics.
fig, ax = canvas(“Уравнение Шрёдингера связывает состояние и гамильтониан”, 4.4) card(ax, 2.1, 3.8, 3.7, 1.1, “Состояние ψ(0)\nамплитуды и фазы”) card(ax, 7.8, 3.8, 3.7, 1.1, “Гамильтониан H\nоператор энергии”, C[1]) arrow(ax, (3, 3.1), (4.2, 2.7)); arrow(ax, (7, 3.1), (5.8, 2.7), C[1]) card(ax, 5, 2.1, 7.5, 1, “iℏ ∂ψ/∂τ = Hψ → ψ(τ)”, C[2], 17) arrow(ax, (5, 1.5), (5, 1.1)) card(ax, 5, .6, 8.8, .75, “Измерение A: исходы aₙ, вероятности |⟨aₙ|ψ⟩|²; среднее ⟨ψ|A|ψ⟩”, size=11) save(fig, “quantum”)
tau = np.linspace(0, 2np.pi, 400) coupling = 1.0 # Change this positive energy scale; hbar = 1. H = np.array([[0., -coupling], [-coupling, 0.]]) fig, ax = panels(“Два состояния: матрица задаёт изменение амплитуд”) ax[0].imshow(H, cmap=“RdBu”, vmin=-1, vmax=1) for i in range(2): for j in range(2): ax[0].text(j, i, “0” if i==j else “−t”, ha=“center”, va=“center”, fontsize=20, color=‘black’ if i==j else ‘white’) ax[0].set(xticks=[0, 1], xticklabels=[“A: столбец”, “B: столбец”], yticks=[0, 1], yticklabels=[“A: строка”, “B: строка”], title=“Hᵢⱼ = ⟨i|H|j⟩”) ax[1].plot(tau, np.cos(couplingtau)**2, label=“P(A)=cos²(tτ/ℏ)”) ax[1].plot(tau, np.sin(couplingtau)**2, label=“P(B)=sin²(tτ/ℏ)”) ax[1].set(xlabel=“время τ (единицы с ℏ=1)”, ylabel=“вероятность”, title=“ψ(0) = (1,0)”) ax[1].legend(fontsize=9) for tt in [.1, .7, 2.]: assert np.allclose(expm(-1jHtt)@np.array([1., 0.]), [np.cos(couplingtt), 1jnp.sin(couplingtt)]) save(fig, “two_sites”)
13. Screening: signs for a positive test charge.
fig, ax = panels(“Экранирование: перераспределение зарядов и полное поле”) a = ax[0]; a.set(xlim=(-3, 3), ylim=(-2, 2), aspect=“equal”); a.axis(“off”) a.add_patch(Circle((0, 0), .28, fc=C[1])); a.text(0, 0, “+”, color=“white”, ha=“center”, va=“center”, fontsize=23) for theta in np.linspace(0, 2np.pi, 10, endpoint=False): xx, yy = 1.1np.cos(theta), 1.1*np.sin(theta) a.text(xx, yy, “−”, ha=“center”, va=“center”, color=C[0], fontsize=23) a.text(0, -1.75, “Избыток электронов около + заряда\nСхема изменения плотности, не орбит”, ha=“center”, fontsize=10) rr = np.linspace(.2, 5, 300) ax[1].plot(rr, 1/rr, label=“1/r”) ax[1].plot(rr, np.exp(-rr)/rr, label=“exp(−κr)/r, κ=1”) ax[1].set(xlabel=“расстояние r”, ylabel=“потенциал, условные единицы”, title=“Иллюстрация: объёмная модель Томаса — Ферми”) ax[1].legend(fontsize=9) save(fig, “screening”)
14. Lagrangian field description and variation.
fig, ax = canvas(“От координат частиц к действию поля”, 4.4) for yy, text in [(4.25, “Частица: q(τ) → L(q, q̇) → S = ∫ L dτ”), (2.85, “Поле: φ(x,τ) → плотность ℒ(φ,∂φ) → S = ∫ dτ dx ℒ”), (1.45, “Меняем историю: φ → φ + εη; dS/dε при ε=0 равно 0”)]: card(ax, 5, yy, 9, .85, text, size=13) arrow(ax, (5, 3.73), (5, 3.37)); arrow(ax, (5, 2.33), (5, 1.97)) ax.text(5, .35, “При фиксированных границах η = 0 на границе; внутри η произвольна.”, ha=“center”, fontsize=11) save(fig, “lagrangian”)
Finite interval, phi*=sin(pix), J=(pi²+mu²)sin(pix), eta=sin(2pi*x).
mu = 1. eta_mode = 2 # Positive integer; eta(x) vanishes at both endpoints. def action(eps): def density(x): phi = np.sin(np.pix)+epsnp.sin(eta_modenp.pix) dp = np.pinp.cos(np.pix)+epseta_modenp.pinp.cos(eta_modenp.pix) J = (np.pi2+mu2)np.sin(np.pix) return .5(dpdp+mumuphiphi)-Jphi return quad(density, 0, 1, epsabs=1e-11)[0] eps = np.linspace(-.3, .3, 101) ss = np.array([action(e) for e in eps]) exact = action(0)+eps**2((eta_modenp.pi)2+mu2)/4 assert np.max(abs(ss-exact)) < 1e-12 fig, ax = panels(“Вариация — изменение всей функции, а не её аргумента”) for e in [-.2, 0, .2]: ax[0].plot(x, np.sin(np.pix)+enp.sin(eta_modenp.pi*x), label=f"ε = {e}") ax[0].set(xlabel=“x”, ylabel=“φ + εη”, title=“Концы всех профилей закреплены”) ax[0].legend(fontsize=10) ax[1].plot(eps, ss-action(0)); ax[1].plot(0, 0, “o”, color=C[1]) ax[1].set(xlabel=“ε”, ylabel=“S[φ+εη] − S[φ]”, title=“Линейный член равен нулю”) REPORT[“action_identity_max_error”] = float(np.max(abs(ss-exact))) save(fig, “variation”)
15. Wick pairings, retained explicitly for transparent combinatorics.
def pairings(items): if not items: yield [] return a, rest = items[0], items[1:] for i, b in enumerate(rest): for tail in pairings(rest[:i]+rest[i+1:]): yield [(a, b)]+tail
def multiplicities(groups): counts = {} for pairs in pairings(tuple(range(len(groups)))): edges = tuple(sorted(tuple(sorted((groups[a], groups[b]))) for a, b in pairs)) counts[edges] = counts.get(edges, 0)+1 return counts
six = multiplicities((“x”, “y”, “z”, “z”, “z”, “z”)) assert sorted(six.values()) == [3, 12] tadpole_count = six[tuple(sorted([(“x”, “z”), (“y”, “z”), (“z”, “z”)]))] ten = multiplicities((“x”, “y”)+(“z”,)*4+(“w”,)4) sunset_count = sum(v for edges, v in ten.items() if edges.count((“w”, “z”)) == 3 and (“x”, “y”) not in edges and (“z”, “z”) not in edges and (“w”, “w”) not in edges) assert sunset_count == 192 assert Fraction(tadpole_count, math.factorial(4)) == Fraction(1, 2) assert Fraction(sunset_count, 2math.factorial(4)**2) == Fraction(1, 6) REPORT[“wick_counts”] = {“all_six_field_pairings”: sum(six.values()), “disconnected”: 3, “tadpole”: tadpole_count, “sunset”: sunset_count, “all_ten_field_pairings”: sum(ten.values())}
fig, ax = plt.subplots(figsize=(7.2, 7.4)) ax.set(xlim=(0, 10), ylim=(0, 10)); ax.axis(‘off’) fig.suptitle(“От ряда к диаграммам”, fontsize=21) steps = [ (“1. Разложить вес взаимодействия”, r"", “Разлагаем экспоненту по степеням g.”), (“2. Выбрать порядок V”, r"
“, “V внутренних точек → V интегралов.\nВ каждой точке — четыре поля.”), (“3. Вычислить гауссово среднее”, r”
", “Разбить все поля на пары.\nПеремножить G для каждой пары.”), (“4. Записать результат рисунком”, “пара полей → линия точка взаимодействия → вершина”, “Суммировать все спаривания\nс коэффициентом из шага 2.”)] for i, (title, formula, detail) in enumerate(steps): y = 8.65-i*2.35 card(ax,5,y,9.35,1.96,‘’,C[i]) ax.text(5,y+.66,title,ha=‘center’,va=‘center’,fontsize=16,weight=‘bold’,color=C[i]) if i==3: formula=“Пара полей → линия\nТочка взаимодействия → вершина” ax.text(5,y+.08,formula,ha=‘center’,va=‘center’,fontsize=19 if i<3 else 14.5) ax.text(5,y-.57,detail,ha=‘center’,va=‘center’,fontsize=14) if i<3: arrow(ax,(5,y-1.01),(5,y-1.29)) save(fig, “series”)
All three pairings: a crossing is not a new interaction vertex.
fig, axs = plt.subplots(3,1,figsize=(7.5,9.5),constrained_layout=True) fig.suptitle(‘Правило Вика: 4 места → 3 спаривания’,fontsize=20) terms=[([(0,1),(2,3)],r’‘), ([(0,2),(1,3)],r’
’), ([(0,3),(1,2)],r’
‘)] pos=[(.5,1.25),(.5,.15),(3.6,1.25),(3.6,.15)] for n,(a,(edges,label)) in enumerate(zip(axs,terms),1): a.set(xlim=(-.1,8),ylim=(-.3,1.75));a.axis(‘off’) for c,(i,j) in zip(C,edges): a.plot([pos[i][0],pos[j][0]],[pos[i][1],pos[j][1]],color=c,lw=2.8) for j,(xx,yy) in enumerate(pos): a.plot(xx,yy,‘o’,color=’#172b3a’,ms=9) a.text(xx,yy+(.18 if j%2==0 else -.18),str(j+1),ha=‘center’,va=‘center’,fontsize=17) a.text(5.4,.9,label,ha=‘center’,fontsize=25) a.text(5.4,.3,f’Слагаемое {n}‘,ha=‘center’,fontsize=16) if n<3:a.text(2,-.28,’+',ha=‘center’,fontsize=23) save(fig,‘wick_pairs’)
Independent numerical check: a direct 2D Gaussian quadrature, not a Wick sum.
cov=np.array([[2.,1.],[1.,3.]]) chol=np.linalg.cholesky(cov) nodes,weights=np.polynomial.hermite.hermgauss(3) direct_moment=0. for i,zi in enumerate(nodes): for j,zj in enumerate(nodes): X,Y=chol@(np.sqrt(2)*np.array([zi,zj])) direct_moment+=weights[i]*weights[j]X**2Y**2/np.pi wick_moment=cov[0,0]cov[1,1]+2cov[0,1]2 assert abs(direct_moment-wick_moment)<1e-12 for count in [2,4,6,8,10]: assert sum(1 for _ in pairings(tuple(range(count))))==math.factorial(count)//(2(count//2)*math.factorial(count//2)) REPORT[‘wick_numeric’]={‘direct_quadrature’:float(direct_moment),‘pairing_sum’:float(wick_moment)} fig,ax=plt.subplots(1,2,figsize=(9,4.8),constrained_layout=True) fig.suptitle(‘Один момент: три вклада, сумма равна 8’,fontsize=19) ax[0].imshow(cov,cmap=‘Blues’,vmin=0,vmax=4) for i in range(2): for j in range(2):ax[0].text(j,i,str(int(cov[i,j])),ha=‘center’,va=‘center’,fontsize=27) ax[0].set(xticks=[0,1],xticklabels=[‘X’,‘Y’],yticks=[0,1],yticklabels=[‘X’,‘Y’],title=‘Ковариация G’) ax[1].bar([0,1,2],[6,1,1],color=[C[0],C[1],C[2]]) ax[1].set(xticks=[0,1,2],xticklabels=[‘XX · YY’,‘XY · XY’,‘XY · XY’],ylim=(0,7.3),ylabel=‘Вклад в среднее X²Y²’) for i,val in enumerate([6,1,1]):ax[1].text(i,val+.15,str(val),ha=‘center’,fontsize=22) ax[1].tick_params(axis=‘x’,labelrotation=25) save(fig,‘wick_values’)
def vertex(ax, xy, label, internal=True): ax.plot(*xy, “o”, color=C[1] if internal else C[0], ms=8) ax.text(xy[0], xy[1]-.35, label, ha=“center”, fontsize=12)
def line(ax, a, b, bend=0, color=C[0]): ax.add_patch(FancyArrowPatch(a, b, arrowstyle=“-”, color=color, lw=2, connectionstyle=f"arc3,rad={bend}"))
fig, axs = panels(“Один порядок g, разные диаграммы и коэффициенты”, 2, 4.3) for a in axs: a.set(xlim=(0, 6), ylim=(-1, 4.2)); a.axis(“off”) a = axs[0]; z0 = (3, 2) for xy, label in [((.5, 3.5), “x₁”), ((.5, .5), “x₂”), ((5.5, 3.5), “x₃”), ((5.5, .5), “x₄”)]: vertex(a, xy, label, False); line(a, xy, z0) vertex(a, z0, “z”) a.text(3, -.55, “4! спариваний / 4! = 1\n”+r"“, ha=“center”, fontsize=9) a.set_title(“Четырёхточечная связная функция: дерево”) a = axs[1] for xy, label, internal in [((.5, 1.4), “x”, False), ((3, 1.4), “z”, True), ((5.5, 1.4), “y”, False)]: vertex(a, xy, label, internal) line(a, (.5, 1.4), (3, 1.4)); line(a, (3, 1.4), (5.5, 1.4)) a.add_patch(Circle((3, 2.3), .9, fill=False, ec=C[2], lw=2)) a.text(3, -.55, “12 спариваний / 4! = 1/2\n”+r”
", ha=“center”, fontsize=9) a.set_title(“Двухточечная функция: одна петля”) save(fig, “diagrams_first”)
fig, axs = panels(“Знаменатель сокращает отдельный вакуумный пузырь”, 2, 3.8) for a in axs: a.set(xlim=(0, 6), ylim=(0, 4)); a.axis(“off”) a = axs[0] line(a, (.5, 1), (5.5, 1)); vertex(a, (.5, 1), “x”, False); vertex(a, (5.5, 1), “y”, False) for xx in [2.3, 3.7]: a.add_patch(Circle((xx, 2.8), .7, fill=False, ec=C[1], lw=2)) vertex(a, (3, 2.8), “z”) a.set_title(“Числитель: свободная линия × вакуум”) a.text(3, .05, “−g/8 · G(x,y) ∫ dz G(z,z)²”, ha=“center”, fontsize=11) a = axs[1] card(a, 3, 2.5, 5.4, 1.5, “(N₀ + gN₁) / (1 + gD₁)\n\n= N₀ + g(N₁ − N₀D₁) + …”, size=12) a.text(3, .7, “Вычитается тот же вакуумный вклад.\nГоловастик связан с x,y\nи остаётся в ответе.”, ha=“center”, fontsize=11) save(fig, “vacuum”)
fig, ax = canvas(“Две вершины и две независимые петли: sunset”, 4) z0, w0 = (3, 2.8), (7, 2.8) for xy, label, inner in [((.5, 2.8), “x”, False), (z0, “z”, True), (w0, “w”, True), ((9.5, 2.8), “y”, False)]: vertex(ax, xy, label, inner) line(ax, (.5, 2.8), z0); line(ax, w0, (9.5, 2.8)) for bend in [-.5, 0, .5]: line(ax, z0, w0, bend) ax.text(5, 4.5, “Внутренних линий I=3, вершин V=2: L=I−V+1=2”, ha=“center”, fontsize=12) ax.text(5, .85, “2 × 4 × 4 × 3! = 192 спаривания; 192 / [2!(4!)²] = 1/6”, ha=“center”, fontsize=11) ax.text(5, .2, “Вклад: g²/6 ∫ dz dw G(x,z) G(z,w)³ G(w,y)”, ha=“center”, fontsize=13) save(fig, “sunset”)
Finite one-dimensional tadpole check: G(0)=1/(2mu).
g = .2 direct = -g/2 * quad(lambda z: np.exp(-muabs(z))/(2mu) * (1/(2mu)) * np.exp(-muabs(z))/(2mu), -np.inf, np.inf, epsabs=1e-12)[0] expected = -g/(16mu**4) assert abs(direct-expected) < 1e-12 REPORT[“tadpole_at_x_equals_y_zero”] = {“quadrature”: direct, “analytic”: expected} REPORT[“limits”] = [“Figures use simplified models, not experimental validation.”, “Wick expansion is coefficient-wise; convergence is not assumed.”, “The screening potential drawn is a 3D model, not graphene’s exact kernel.”] (OUT/“teaching_examples_report.json”).write_text( json.dumps(REPORT, ensure_ascii=False, indent=2), encoding=“utf8”) print(json.dumps(REPORT, ensure_ascii=False, indent=2))
</spoiler> ### Как воспроизвести остальные иллюстрации Файл `09_visual_guide.py` создаёт 21 научную схему и график для статьи: решётку, конус, уровни, пару, пропагатор, графы, топологию, многопетлевые скелеты и пояснения bootstrap. Он также проверяет нормировки первых осцилляторных состояний. Два юмористических изображения — комикс zinoink и кадр Adult Swim с указанными источниками. Фрагменты процессных диаграмм взяты из книги Коке и Киссинджера с номерами страниц; новые научные схемы и графики строятся на Python. ```bash python 09_visual_guide.py python 01_loops.py python 03_surface.py
Первый запуск создаёт PNG, второй — в том числе анимацию ошибки по порядкам, третий — вращение поверхности . GIF цепочки пузырьков создаёт
06_graphene.py. Вращение камеры не следует воспринимать как физическую динамику системы.
Полный код научных иллюстраций: 09_visual_guide.py
"""Иллюстрации статьи. Python 3.12; numpy, scipy, matplotlib. Все схемы авторские; это модели, не фотографии эксперимента. Запуск: python 09_visual_guide.py. PNG появляются рядом с файлом. """ from pathlib import Path from math import factorial import numpy as np import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt def publication_layout(fig): """Readable labels at a 600 px article width; retain data and panel order.""" import textwrap from matplotlib.text import Text width, height = fig.get_size_inches() fig.set_size_inches(width, height * 1.22, forward=True) scale = width / 8.0 main, small = 14.0 * scale, 12.0 * scale for text in fig.findobj(Text): if text.get_text().strip(): text.set_fontsize(max(text.get_fontsize(), main)) if text.get_color() in ['#d77b27', '#ed8b36', '#df7b28', '#d77527']: text.set_color('#87470f') for ax in fig.axes: for tick in ax.get_xticklabels() + ax.get_yticklabels(): tick.set_fontsize(small) for label in [ax.xaxis.label, ax.yaxis.label]: txt = label.get_text() if '$' not in txt: label.set_text('\n'.join(textwrap.fill(s, 39) for s in txt.splitlines())) title = ax.get_title() if '$' not in title: columns = max(25, int(ax.get_position().width * 75)) ax.set_title('\n'.join(textwrap.fill(s, columns) for s in title.splitlines()), fontsize=main, pad=12) legend = ax.get_legend() if legend: for text in legend.get_texts(): text.set_fontsize(small) for line in ax.lines: line.set_linewidth(max(line.get_linewidth(), 1.8)) if fig._suptitle: txt = fig._suptitle.get_text() if '$' not in txt: fig._suptitle.set_text(textwrap.fill(txt, 57)) fig._suptitle.set_fontsize(max(main + 2, 18)) for text in fig.texts: if text is fig._suptitle: continue txt=text.get_text() if '$' not in txt: text.set_text('\n'.join(textwrap.fill(s, 68) for s in txt.splitlines())) if type(fig.get_layout_engine()).__name__ == 'PlaceHolderLayoutEngine': fig.tight_layout(rect=(.01,.10,.99,.91),h_pad=2,w_pad=2) if type(fig.get_layout_engine()).__name__ == 'ConstrainedLayoutEngine': fig.get_layout_engine().set(h_pad=.15, w_pad=.12) from matplotlib.patches import Circle, Ellipse, Polygon, FancyArrowPatch from scipy.special import eval_hermite, spence from scipy.constants import hbar, elementary_charge as e OUT = Path(__file__).resolve().parent C = ['#147d92', '#ed8b36', '#7144a5', '#24384c', '#bb4353'] plt.rcParams.update({'font.family': 'DejaVu Sans', 'font.size': 12, 'axes.spines.top': False, 'axes.spines.right': False, 'figure.facecolor': '#fffdf8', 'axes.facecolor': '#fffdf8', 'savefig.facecolor': '#fffdf8', 'axes.titleweight': 'bold'}) def save(fig, name): publication_layout(fig); fig.savefig(OUT / (name + '.png'), dpi=130, bbox_inches='tight') plt.close(fig) def canvas(title, size=(11, 5)): fig, ax = plt.subplots(figsize=size) ax.set(xlim=(0, 10), ylim=(0, 5)) ax.axis('off') ax.set_title(title, pad=16, fontsize=19) return fig, ax def arrow(ax, a, b, color=C[0], **kw): ax.add_patch(FancyArrowPatch(a, b, arrowstyle='-|>', mutation_scale=17, color=color, linewidth=2, **kw)) def box(ax, x, y, text, color=C[0], size=13): ax.text(x, y, text, ha='center', va='center', fontsize=size, bbox=dict(boxstyle='round,pad=.65', fc='white', ec=color, lw=2)) def honeycomb(): pts, sub = [], [] a1, a2 = np.array([np.sqrt(3), 0]), np.array([np.sqrt(3)/2, 1.5]) for i in range(-5, 6): for j in range(-4, 5): for s, d in enumerate([np.array([0., 0.]), np.array([0., 1.])]): p = i*a1+j*a2+d if abs(p[0]) < 5.5 and abs(p[1]) < 3.4: pts.append(p); sub.append(s) pts, sub = np.array(pts), np.array(sub) dist = np.linalg.norm(pts[:, None]-pts[None, :], axis=2) edges = np.argwhere(np.triu(np.isclose(dist, 1), 1)) return pts, sub, edges def graphene(): p, sub, edges = honeycomb() fig = plt.figure(figsize=(11, 5.7)) ax = fig.add_subplot(projection='3d') for i, j in edges: ax.plot(*p[[i,j]].T, [0, 0], color='#60788b', lw=2) ax.scatter(p[:,0], p[:,1], np.zeros(len(p)), c=[C[s] for s in sub], s=65, edgecolor='white', linewidth=.5, depthshade=False) ax.set(zlim=(-2,2)); ax.view_init(elev=34, azim=-64); ax.set_axis_off() ax.set_box_aspect((11,7,2)) ax.set_title('Графен: один атомный слой углерода', fontsize=20) fig.text(.5,.05,'Шарики — положения атомов; линии — ближайшие связи. Схема, не микрофотография.',ha='center',fontsize=12) save(fig,'guide_graphene') fig, ax = plt.subplots(figsize=(11,6)) for i,j in edges: ax.plot(*p[[i,j]].T, color='#adbac2', lw=2, zorder=1) for s in [0,1]: ax.scatter(*p[sub==s].T, s=95, color=C[s], label=f'Подрешётка {"AB"[s]}', zorder=3) # Примитивная ячейка (параллелограмм) содержит два атома с учётом границ. origin=np.array([-.15,-.25]); a1=np.array([np.sqrt(3),0]); a2=np.array([np.sqrt(3)/2,1.5]) ax.add_patch(Polygon([origin,origin+a1,origin+a1+a2,origin+a2],fc=C[2],alpha=.15,ec=C[2],lw=3)) ax.annotate('Ячейка: два атома A и B', xy=(1.2,.6),xytext=(2.3,2.6),arrowprops={'arrowstyle':'->'},fontsize=12) ax.annotate('Связь C–C ≈ 0.142 нм',xy=(0,.5),xytext=(-5,2.6),arrowprops={'arrowstyle':'->'},fontsize=12) ax.set_aspect('equal'); ax.axis('off'); ax.legend(loc='lower left',ncol=2) ax.set_title('«Соты» — геометрия атомов, не траектории электронов',fontsize=18) fig.text(.5,.02,'Идеальная решётка → модель ближайших соседей → приближение Дирака около K и K′',ha='center',fontsize=12) save(fig,'guide_lattice') fig=plt.figure(figsize=(12,5.4)); ax=fig.add_subplot(121,projection='3d') r=np.linspace(0,1,30); t=np.linspace(0,2*np.pi,60); R,T=np.meshgrid(r,t) for s,c in [(1,C[0]),(-1,C[1])]: ax.plot_surface(R*np.cos(T),R*np.sin(T),s*R,color=c,alpha=.8,linewidth=0) ax.set(xlabel='$p_x$',ylabel='$p_y$',zlabel='$E$',title=r'Конус Дирака: $E=\pm v_F |p|$') ax.set_xticks([]); ax.set_yticks([]); ax.set_zticks([]) ax=fig.add_subplot(122) dk=np.linspace(-.7,.7,300); K=4*np.pi/(3*np.sqrt(3)) band=np.abs(1+2*np.cos(np.sqrt(3)*(K+dk)/2)) for s in [1,-1]: ax.plot(dk,s*band,color=C[0],label='Ближайшие соседи' if s==1 else None) ax.plot(dk,s*1.5*np.abs(dk),'--',color=C[1],label='Линейное приближение' if s==1 else None) ax.axvspan(-.12,.12,color=C[0],alpha=.12) ax.set(xlabel=r'$(k_x-K_x)d$, $k_y=0$',ylabel='$E/t$',title='Конус работает вблизи точки K') ax.legend(fontsize=10); ax.grid(alpha=.2) fig.tight_layout(); save(fig,'guide_dispersion') def landau(): B=10.; vf=1e6; lb=np.sqrt(hbar/(e*B)); scale=hbar*vf/lb/e*1e3 fig,axes=plt.subplots(1,2,figsize=(11,5.6)) for n in range(-5,6): en=np.sign(n)*scale*np.sqrt(2*abs(n)) axes[0].hlines(en,0,1,color=C[0] if n>=0 else C[1],lw=2.5) axes[0].text(1.05,en,f'n={n}',va='center',fontsize=10) axes[0].set(xlim=(-.1,1.6),ylabel='Энергия, мэВ',title='Графен, B = 10 Тл'); axes[0].set_xticks([]) n=np.arange(6) axes[1].plot(n,n+.5,'o-',label=r'Параболическая зона: $n+1/2$',color=C[2]) axes[1].plot(n,np.sqrt(2*n),'o-',label=r'Графен: $\sqrt{2n}$',color=C[0]) axes[1].set(xlabel='Номер n ≥ 0',ylabel='Энергия в собственных единицах модели',title='Различается закон расстояний') axes[1].legend(fontsize=10); axes[1].grid(alpha=.2) fig.suptitle(f'Магнитная длина {lb*1e9:.2f} нм; E₁ = {np.sqrt(2)*scale:.1f} мэВ',fontsize=18) fig.tight_layout(); save(fig,'guide_landau') fig,axes=plt.subplots(1,2,figsize=(11,5.3)); x=np.linspace(-5,5,700) for n in range(4): phi=eval_hermite(n,x)*np.exp(-x*x/2)/np.sqrt(2**n*factorial(n)*np.sqrt(np.pi)) axes[0].plot(x,phi+n*1.3,label=f'n={n}',color=C[n]); axes[0].axhline(n*1.3,color='gray',lw=.5) assert abs(np.trapezoid(phi*phi,x)-1)<1e-6 axes[0].set(xlabel=r'$\xi=(x-x_0)/l_B$',ylabel='Функции со сдвигом для наглядности',title='Осцилляторные функции'); axes[0].legend(fontsize=10) for x0,c in [(-2,C[0]),(0,C[1]),(2,C[2])]: axes[1].plot(x,np.exp(-(x-x0)**2)/np.sqrt(np.pi),color=c,label=f'x₀/lB = {x0}') axes[1].set(xlabel='x / lB',ylabel='Плотность по x, в единицах 1/lB',title='Один уровень — разные центры');axes[1].legend(fontsize=10) fig.tight_layout();save(fig,'guide_basis') def pair_and_perturbation(): fig,ax=canvas('Электрон + незанятое место = возбуждение пары') for off,title in [(0,'До перехода'),(5.5,'После перехода')]: ax.text(off+2,4.3,title,ha='center',fontsize=16) for y,l in [(1.4,'Занятый уровень'),(3.2,'Свободный уровень')]: ax.plot([off+.4,off+3.7],[y,y],color=C[3],lw=2) for j in range(4): empty=off>0 and j==2 ax.add_patch(Circle((off+.8+j*.8,1.4),.13,fc='white' if empty else C[0],ec=C[1] if empty else C[0],lw=2)) if off>0: ax.add_patch(Circle((off+2.4,3.2),.13,fc=C[0])) ax.text(off+2.65,3.35,'электрон −e',fontsize=12) ax.text(off+2.65,1,'дырка +e',fontsize=12) arrow(ax,(off+2.4,1.65),(off+2.4,2.95),C[1]) arrow(ax,(4.1,2.3),(5.3,2.3));ax.text(5,.3,'Энергия без взаимодействия: Eₑ − Eₕ. Число электронов не изменилось.',ha='center',fontsize=12) save(fig,'guide_pair') fig,axes=plt.subplots(1,2,figsize=(11,5.2)) x=np.linspace(0,4,500); rho=2*x*np.exp(-x*x); U=-1/np.sqrt(x*x+.2**2) axes[0].plot(x,rho,label='Радиальный вес ρ(r)',color=C[0]);axes[0].plot(x,rho*U,label='Вклад ρ(r)U(r)',color=C[1]);axes[0].fill_between(x,rho*U,0,color=C[1],alpha=.2) axes[0].set(xlabel='r, условные единицы',title='Среднее — площадь под взвешенной кривой');axes[0].legend(fontsize=10) lam=np.linspace(0,1,300); exact=np.array([np.linalg.eigvalsh([[v*.3,v*.4],[v*.4,1-v*.2]])[0] for v in lam]) axes[1].plot(lam,exact,label='Точная нижняя энергия',color=C[3]);axes[1].plot(lam,.3*lam,'--',label='Первый порядок',color=C[1]);axes[1].plot(lam,.3*lam-.16*lam**2,':',lw=3,label='Второй порядок',color=C[0]) axes[1].set(xlabel='λ',ylabel='E',title='Два уровня: H₀=diag(0,1)');axes[1].legend(fontsize=10) for ax in axes: ax.grid(alpha=.2) fig.tight_layout();save(fig,'guide_perturbation') def bubbles(): fig,ax=canvas('Один и тот же пузырёк, повторённый 0, 1, 2, 3 раза',(11,6)) for n in range(4): y=4.2-n*1.1; ax.text(.2,y,f'n={n}',va='center') ax.plot([1.3,7.4],[y,y],color=C[1],lw=2) for j in range(n): xx=2.4+j*1.7 ax.add_patch(Ellipse((xx,y),1.1,.65,fc='#fffdf8',ec=C[0],lw=2.5,zorder=3)) arrow(ax,(xx-.15,y+.31),(xx+.15,y+.31),C[0],zorder=4) ax.text(7.8,y,['V₀','V₀ Π V₀','V₀ Π V₀ Π V₀','V₀ (Π V₀)³'][n],va='center',fontsize=15) ax.text(5,.2,'Сплошные дуги — электронные линии; оранжевые отрезки — кулоновское взаимодействие.',ha='center',fontsize=11) save(fig,'guide_bubbles') def graphs(): fig,axes=plt.subplots(1,3,figsize=(12,4.4)) pts=np.array([[0,0],[1,0],[1,1],[0,1]]) all_edges=[(0,1),(1,2),(2,3),(3,0),(0,2)] for n,ax in enumerate(axes): for k,(i,j) in enumerate(all_edges[:3+n]): ax.plot(*pts[[i,j]].T,color=C[0] if k<3 else C[1],lw=3) ax.scatter(*pts.T,s=110,color=C[3],zorder=3) for i,(x,y) in enumerate(pts): ax.text(x-.12,y+.1,'ABCD'[i]) ax.set(xlim=(-.4,1.4),ylim=(-.4,1.4),aspect='equal',title=f'V=4, I={3+n}, L={n}');ax.axis('off') fig.suptitle('Остовное дерево + каждое лишнее ребро = новый цикл',fontsize=18) fig.tight_layout();save(fig,'guide_graphs') fig,axes=plt.subplots(1,2,figsize=(11,5)) ax=axes[0] for x in [-1,1]: ax.add_patch(Circle((x,0),1,fc='none',ec=C[0],lw=3)) ax.plot(0,0,'o',color=C[1],ms=10);ax.text(-1,0,'F₁',ha='center');ax.text(1,0,'F₂',ha='center');ax.text(0,1.35,'F₃: внешняя область',ha='center') ax.set(xlim=(-2.5,2.5),ylim=(-1.5,1.8),aspect='equal',title='Сфера: V=1, I=2, F=3');ax.axis('off') ax=axes[1];ax.add_patch(Polygon([[0,0],[2,0],[2,2],[0,2]],fc='#dceff0',ec=C[3],lw=2)) for a,b in [((.6,0),(1.4,0)),((.6,2),(1.4,2))]:arrow(ax,a,b,C[0]) for a,b in [((0,.6),(0,1.4)),((2,.6),(2,1.4))]:arrow(ax,a,b,C[1]) for p in [(0,0),(2,0),(2,2),(0,2)]:ax.plot(*p,'o',color=C[2],ms=10) ax.text(1,1,'Одна грань F₁\nСклеить равные края',ha='center',va='center');ax.text(1,-.45,'4 угла → 1 вершина; 4 края → 2 ребра',ha='center',fontsize=11) ax.set(xlim=(-.6,2.6),ylim=(-.7,2.5),aspect='equal',title='Тор: V=1, I=2, F=1');ax.axis('off') fig.suptitle('Один граф «две петли»: L=2 на обеих поверхностях',fontsize=18) fig.tight_layout();save(fig,'guide_topology') def propagator_response(): fig,axes=plt.subplots(1,2,figsize=(11,5.3));x=np.linspace(-6,6,700); k=np.linspace(-4,4,700) for mu,c in [(.5,C[0]),(1,C[1]),(2,C[2])]: axes[0].plot(x,np.exp(-mu*np.abs(x))/(2*mu),color=c,label=f'μ={mu:g}') axes[1].plot(k,1/(k*k+mu*mu),color=c,label=f'μ={mu:g}') axes[0].set(xlabel='x',ylabel='G(x)',title='Отклик на точечный источник') axes[1].set(xlabel='k',ylabel='D(k)',title='Тот же оператор в Фурье-базисе') for ax in axes:ax.legend(fontsize=10);ax.grid(alpha=.2) fig.suptitle('Евклидова функция Грина: (−∂² + μ²)G = δ',fontsize=18) fig.tight_layout();save(fig,'guide_propagator') fig,axes=plt.subplots(1,2,figsize=(11,5.1));t=np.linspace(-3,12,600);omega=np.linspace(0,3,600);eta=.08;gap=1. response=np.where(t>=0,-2*np.sin(gap*t)*np.exp(-eta*t),0) chi=1/(omega-gap+1j*eta)-1/(omega+gap+1j*eta) axes[0].plot(t,response,color=C[0]);axes[0].axvline(0,color=C[1],ls='--');axes[0].set(xlabel='t, при ħ=1',ylabel='χᴿ(t)',title='До воздействия отклик равен нулю') axes[1].plot(omega,-chi.imag,color=C[1]);axes[1].axvline(gap,color=C[0],ls='--');axes[1].set(xlabel='ω, при ħ=1',ylabel='−Im χᴿ(ω)',title='Спектр: виден разрешённый переход') fig.suptitle('Учебная пара уровней: ΔE=1, матричный элемент=1, η=0.08',fontsize=16) for ax in axes:ax.grid(alpha=.2) fig.tight_layout();save(fig,'guide_response') def ladder(ax,L,labels=False): for j in range(L+1):ax.plot([j,j],[0,1],color=C[3],lw=2) for y in [0,1]:ax.plot([0,L],[y,y],color=C[3],lw=2);ax.scatter(range(L+1),[y]*(L+1),s=30,color=C[3],zorder=3) at=max(1,L//3) if L>1 else 0;bot=max(1,2*L//3) if L>1 else 1 starts=[(0,1),(at,1),(L,1),(L,0),(bot,0),(0,0)] ends=[(-.55,1.5),(at,1.7),(L+.55,1.5),(L+.55,-.5),(bot,-.7),(-.55,-.5)] for i,(a,b) in enumerate(zip(starts,ends)): arrow(ax,a,b,C[0]);ax.text(b[0],b[1],f'p{i+1}',ha='center',va='bottom' if b[1]>0 else 'top',fontsize=9) for j in range(L):ax.text(j+.5,.5,f'y{j+1}' if labels else str(j+1),ha='center',va='center',color=C[1],fontsize=14) ax.set(xlim=(-1,L+1),ylim=(-1.1,2.1),aspect='equal');ax.axis('off') def many_loops(): fig,axes=plt.subplots(3,1,figsize=(9,10.5),gridspec_kw={'height_ratios':[1,1,1.2]}) for ax,L in zip(axes,[1,2,9]): ladder(ax,L);ax.set_title(f'L={L}: I={3*L+1}, V={2*L+2}, I−V+1={L}',fontsize=14) fig.suptitle('Шесть внешних линий; внутри — всё больше независимых петель',fontsize=18) fig.text(.5,.01,'Планарные скелеты: показывают топологию, не полный набор ненулевых диаграмм N=4 SYM.',ha='center',fontsize=11) fig.tight_layout(rect=(0,.04,1,.95));save(fig,'guide_nine_loops') fig,ax=plt.subplots(figsize=(11,6.2));ladder(ax,3,True) edge_labels=[(.5,1.12,'D₁'),(1.5,1.12,'D₂'),(2.5,1.12,'D₃'),(.5,-.2,'D₄'),(1.5,-.2,'D₅'),(2.5,-.2,'D₆'),(-.17,.5,'D₇'),(1.17,.5,'D₈'),(2.17,.5,'D₉'),(3.2,.5,'D₁₀')] for x,y,s in edge_labels:ax.text(x,y,s,ha='center',fontsize=12,color=C[2]) for x,y,s in [(.5,1.65,'x₁'),(2.1,1.65,'x₂'),(3.7,.5,'x₃'),(2.5,-.7,'x₄'),(.8,-.7,'x₅'),(-.7,.5,'x₆')]:ax.text(x,y,s,ha='center',fontsize=13,color=C[4]) ax.set_title('Три петли → интегрирование по y₁, y₂, y₃; десять линий → десять Dⱼ',fontsize=17) fig.text(.5,.02,'pᵢ = xᵢ₋₁ − xᵢ (циклически). x и y здесь — дуальные импульсные координаты.',ha='center',fontsize=12) save(fig,'guide_integral') def wick(): fig,axes=plt.subplots(1,3,figsize=(12,4.8));points=np.array([[-1,-1],[1,-1],[1,1],[-1,1]]) for ax,pairs in zip(axes,[[(0,1),(2,3)],[(0,2),(1,3)],[(0,3),(1,2)]]): for a,b in pairs:ax.plot(*points[[a,b]].T,color=C[0],lw=3) ax.scatter(*points.T,s=110,color=C[1],zorder=3) for i,(x,y) in enumerate(points):ax.text(x*1.2,y*1.2,f'x{i+1}',ha='center',va='center') ax.set(xlim=(-1.7,1.7),ylim=(-1.7,1.7),aspect='equal');ax.axis('off') fig.suptitle('Четыре гауссовых множителя: три способа разбить на пары',fontsize=18) fig.text(.5,.04,'⟨x⁴⟩₀ = 3. Для шести множителей: 5 × 3 × 1 = 15 спариваний.',ha='center',fontsize=14) fig.tight_layout(rect=(0,.08,1,.95));save(fig,'guide_wick') fig,axes=plt.subplots(1,2,figsize=(11,4.5));x=np.linspace(-3,3,500) for g,c in [(0,C[3]),(.5,C[0]),(2,C[1])]: w=np.exp(-x*x/2-g*x**4/24);z=np.trapezoid(w,x) # Для рисунка нормировка вычислена на более широком интервале. from scipy.integrate import quad z=quad(lambda q:np.exp(-q*q/2-g*q**4/24),-np.inf,np.inf)[0] axes[0].plot(x,w/z,color=c,label=f'g={g}') axes[0].set(xlabel='x',ylabel='Нормированный вес',title='Взаимодействие подавляет хвосты');axes[0].legend(fontsize=10) axes[1].axis('off');axes[1].text(.5,.65,'6 множителей: 15 спариваний\n\n3 несвязных = ⟨x²⟩₀ ⟨x⁴⟩₀\n\n12 связных остаются',ha='center',va='center',fontsize=16,transform=axes[1].transAxes) axes[1].text(.5,.14,'Первый коэффициент: −12 / 4! = −1/2',ha='center',fontsize=14,transform=axes[1].transAxes,color=C[0]) fig.tight_layout();save(fig,'guide_zero_model') def process_and_bootstrap(): fig,ax=canvas('Шесть внешних состояний: пример процесса 2 → 4') ax.add_patch(Ellipse((5,2.5),2.1,2,fc='#dceff0',ec=C[0],lw=2));ax.text(5,2.5,'Амплитуда\nA₆',ha='center',va='center',fontsize=20) for i,y in enumerate([1.5,3.5]):arrow(ax,(.8,y),(4,2.3+i*.4));ax.text(.3,y,f'вход {i+1}',fontsize=12) for i,y in enumerate([.5,1.8,3.1,4.4]):arrow(ax,(6,2.2+i*.2),(8.8,y));ax.text(9,y,f'выход {i+1}',fontsize=12) ax.text(5,.15,'Для вероятности нужны |A₆|², фазовый объём и нормировки.',ha='center',fontsize=12);save(fig,'guide_scattering') fig,ax=canvas('Два маршрута к символу',(8,7)) box(ax,2.2,4.25,'Прямой bootstrap\nамплитуды') box(ax,7.5,4.25,'Формфактор\n+ антипод',C[1]) arrow(ax,(2.2,3.75),(4.5,3.1));arrow(ax,(7.5,3.75),(5.5,3.1),C[1]) box(ax,5,2.65,'Сравнение символов\nи контрольных коэффициентов',C[2]) arrow(ax,(5,2.12),(5,1.35),C[3]) ax.text(5,1.78,'+ константы и граничные условия',ha='center',fontsize=14, bbox=dict(facecolor='#fffdf8',edgecolor='none',pad=6)) box(ax,5,.8,'Восстановление полной функции',C[3]) save(fig,'guide_routes') fig,axes=plt.subplots(1,2,figsize=(11,5.4));x=np.linspace(0,1,350) for a,c in [(-1,C[1]),(0,C[0]),(1,C[2]),(2,C[3])]:axes[0].plot(x,x+a*x*(1-x),color=c,label=f'a={a}') axes[0].set(xlabel='x',ylabel='F(x)',title='F(0)=0 и F(1)=1 ещё недостаточно');axes[0].legend(fontsize=10) axes[1].plot(x,x+x*(1-x),color=C[0],label='a=1: после F′(0)=2') axes[1].scatter([.5],[.75],color=C[1],s=90,zorder=4,label='Отдельная проверка: F(1/2)=3/4') axes[1].plot(x,x+x*(1-x)+3*x*x*(1-x)**2,'--',color=C[2],label='Расширили пространство: b=3') axes[1].set(xlabel='x',ylabel='F(x)',title='Новые условия и новые неоднозначности');axes[1].legend(fontsize=9,loc='upper left') for ax in axes:ax.grid(alpha=.2) fig.tight_layout();save(fig,'guide_bootstrap') def symbol_counts(): fig,axes=plt.subplots(1,2,figsize=(11,5.3));x=np.linspace(.02,.95,350);f=spence(1-x) axes[0].plot(x,f,label='Li₂(x)',color=C[0]);axes[0].plot(x,f+np.pi**2/6,label='Li₂(x)+π²/6',color=C[1]);axes[0].set(xlabel='x',ylabel='F(x)',title='Один символ, разные функции');axes[0].legend(fontsize=10) axes[1].axis('off');axes[1].text(.5,.75,'d Li₂(x) = −log(1−x) d log x',ha='center',fontsize=15,transform=axes[1].transAxes) axes[1].text(.5,.48,'Символ: −(1−x) ⊗ x\n\nСначала 1−x, затем x',ha='center',fontsize=18,color=C[0],transform=axes[1].transAxes) axes[1].text(.5,.13,'Граничное значение Li₂(0)=0\nвыбирает первую кривую.',ha='center',fontsize=13,transform=axes[1].transAxes) fig.tight_layout();save(fig,'guide_symbol') counts=[6,12,636,11208,263880,4916466,92954568,1671656292,30024320034] fig,ax=plt.subplots(figsize=(11,6));L=np.arange(1,10) ax.semilogy(L,counts,'o-',color=C[0],lw=2,label='Ненулевые слова в сопоставимом представлении') ax.semilogy(L,6.**(2*L),'--',color=C[1],label='Все слова шестибуквенного алфавита: 6^(2L)') for l,n in zip(L,counts):ax.annotate(f'{n:,}'.replace(',',' '),(l,n),xytext=(0,-18),textcoords='offset points',ha='center',fontsize=9) ax.set(xlabel='Число петель L',ylabel='Число слов (логарифмическая шкала)',title='Ненулевых слов много, но это не все возможные слова');ax.set_xticks(L);ax.legend(fontsize=10);ax.grid(alpha=.2) fig.tight_layout();save(fig,'guide_counts_all') if __name__=='__main__': graphene();landau();pair_and_perturbation();bubbles();graphs() propagator_response();many_loops();wick();process_and_bootstrap();symbol_counts() print('Создана 21 иллюстрация guide_*.png; нормировки состояний проверены.')
Диплом наконец пригодился как материал, на котором можно пройти путь от уравнения до числа и проверить работу ИИ на знакомой постановке. Восстановленный расчёт дал контрольные значения; расхождения в архиве показали пределы доверия к этим значениям; опыт с намеренными ошибками показал пределы самих тестов. Такой порядок работы полезен и за пределами физики: сохранять основания модели, отделять их от реализации и делать результат воспроизводимым. После этого новость «Claude посчитал девять петель» вызывает предметные вопросы: какие величины посчитаны, какие ограничения использованы и что удалось проверить независимо?
avshkol
190+ минут размер статьи - это, наверное рекорд Хабра...
Сам перевожу и выкладываю 100-страничный: Исследование Марса человеком
перевёл почти половину, и уже 80 минут, да ещё картинки, и браузер сильно подтормаживает при редактировании статьи...
Отдельный респект за картинку с дипломом теплоэнергетика (я сам промтеплоэнергетик по первому образованию, но работаю всё-таки в энергетике, хоть и не в генерации).