Почему Модульные Тесты Не Работают В Научных Приложениях

В этой статье я хочу поделиться своим опытом разработки научных приложений и рассказать, почему Test-Driven Development и модульные тесты не являются панацеей, как принято считать в последнее время, по крайней мере, с точки зрения поиска ошибок в ПО.

Почему? Статья будет состоять из нескольких разделов:

  1. «Введение», в котором я расскажу, почему я решил это написать.

  2. «Алгоритм иллюстрации».

    Здесь мы кратко опишем научную проблему и алгоритм, который проиллюстрирует дальнейшее изложение.

  3. «Недостатки модульных тестов» Здесь будут приведены аргументы, объясняющие недостатки модульных тестов для научных приложений.

  4. Заключение
Сразу отмечу, что я не рассматриваю преимущества TDD как способа разработки архитектуры, а исключительно с точки зрения тестирования программ.



Введение

На неделе воздухоплавания на хабе основной статьей была Digital Airplanes [1], и интересный комментарий к ней [2] о том, что 20 лет назад не было юнит-тестов и вообще нормальных методов разработки, поэтому «тройной-тройной» Схема представляла собой общую избыточную архитектуру», в частности, написание одной и той же программы тремя разными командами на разных языках с целью устранения программных ошибок.

Однако, по мнению очевидцев ([3], кстати, это еще одна прекрасная книга замечательного ученого Ричарда Фейнмана, нашедшая вторую жизнь на Хабре), если не Test Driven Development (TDD далее), то ручное тестирование было очень важный этап развития.

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



Алгоритм иллюстрации

За последние 20 лет был разработан необычный метод моделирования гидродинамики: метод решетки-Больцмана (далее LBM) [4], [5].

Я принимал участие в реализации этого алгоритма на C++ для Института Макса Планка в Германии.

Чтобы избежать неизбежных вопросов: да, уже существуют коммерческие пакеты и пакеты с открытым исходным кодом, использующие этот метод [6]; но изначально планировалось использовать модификацию метода, которая должна была обеспечить выигрыш в производительности и потреблении памяти; однако мы последовательно от него отказывались (сначала по массопереносу, затем по теплообмену).

К тому времени было написано много кода, и мы решили продолжить собственную реализацию.

Гидродинамика, как известно, описывается уравнением Навье-Стокса.

При его моделировании можно использовать, например, метод конечных элементов [7].

К сожалению, у него есть недостатки – относительная сложность распараллеливания, невозможность моделирования многофазных потоков, сложность моделирования течений в пористых средах.

LBM лишен этих недостатков.

Для разреженных газов справедливо уравнение Больцмана [8], которое описывает, как меняется со временем плотность распределения частиц по скоростям в каждой точке пространства.

Макроскопически это уравнение эквивалентно уравнению Навье-Стокса (т. е.

после перехода к макроскопическим величинам — плотности и скорости).

Теперь следите за своими руками: несмотря на то, что уравнение Больцмана неприменимо для плотных жидкостей, если мы научимся его моделировать, мы сможем смоделировать и уравнение Навье-Стокса для этих жидкостей.

То есть мы тем самым заменяем базовый уровень абстракций (микроскопическое уравнение для плотных жидкостей) на физически неправильную реализацию (микроскопическое уравнение для разреженных газов), но так, чтобы верхний уровень (макроскопическое уравнение Навье-Стокса) описывался корректно.

.

Следующий этап – дискретизация уравнения Больцмана: по времени, по пространственным координатам (получаем пространственные узлы для моделирования) и по возможным направлениям частиц в каждом пространственном узле.

Направления выбираются особым образом и всегда указывают на какие-то соседние узлы.

Таким образом, вот особенности, критичные для дальнейших примеров: 1. алгоритм использует сложные формулы 2. алгоритм нелокален, то есть для того, чтобы вычислить состояние системы в заданном узле, необходимо используйте информацию из предыдущего шага о соседних узлах.



Недостатки модульных тестов

Наконец, давайте перейдем к тому, почему юнит-тесты не могут полностью охватить сложное научное приложение.

Основная идея раздела заключается в том, что сложные алгоритмы мешают тестированию.

самих себя.



Отсутствие простых тестов.

Для сложных алгоритмов и методов сложно придумать простые тесты.

Это, в общем-то, очевидно.

Часто бывает сложно выбрать простые входные параметры формулы, чтобы на выходе было простое и понятное число.

То есть вы, конечно, можете отдельно на бумажке или в Матлабе посчитать, какие параметры нужно подать на вход, чтобы на выходе тестируемого метода получить единицу; но и входные параметры, и выходная единица будут скорее магическими числами.

Примером является распределение Максвелла [9].

В алгоритме LBM он используется в несколько измененном виде.



Маленькая зона покрытия
Даже если вам удастся придумать простой тест для сложной формулы или алгоритма, этот тест, скорее всего, не обнаружит большую часть ошибок.

Действительно, иногда можно придумать простой и понятный тест: например, если вы тестируете, как вы написали матрицы вращения в своем 3D-движке (думаю, немало жителей Хабра баловались этим), то ротация в нулевые углы по всем осям должны возвращать единичную матрицу.

Однако эти тесты часто не могут обнаружить большинство ошибок.

Например, если вместо синуса угла поворота (в элементах матрицы поворота) использовать тангенс, то такой простой тест пройдет. Другой пример: вам нужно реализовать граничные условия для алгоритма LBM. Эти условия должны дополнять состояние узла, когда последний не полностью окружен «жидкими» соседями (например, расположен вблизи стены).

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

Однако оказывается, что этот тест пропускает большую часть ошибок.

Можно возразить, что в бизнес-приложениях тесты также не предназначены для охвата всех возможных входных параметров.

Однако отличие научных приложений состоит в том, что они обычно оперируют множествами разной мощности [10].

Бизнес-приложения в основном работают с конечными или счетными множествами (конечная мощность или мощность алеф-ноль: логические переменные, строки, целые числа), тогда как научные приложения работают с несчетными множествами (действительными или комплексными числами).



Низкий уровень ошибок
Поскольку типичные наборы несчетны, сразу обнаружить ошибку сложно.

Дело в том, что если вы перепутаете «или» и «и» в методе, использующем только логические переменные, то в его выводе сразу же получите что-то не так.

А если перепутать плюс с минусом в формуле, работающей при малых значениях входных параметров, то ошибка вполне может не обнаружиться после одного вызова метода с этой формулой.

Чтобы обнаружить несоответствие, пришлось бы вызвать этот метод несколько сотен тысяч раз.

Также стоит помнить, что в тесте, работающем с реальными числами, нужно подумать, как избежать ложных срабатываний.

Пример: в одном из важных циклов алгоритма LBM continue перепутали с прерыванием.

Однако все испытания прошли.

Ошибка была обнаружена только после ручного тестирования алгоритма на простой системе - потоке Пуазейля [11], когда после 100 000 итераций, или 5 минут (!) моделирования узловой системы 32x64 (начиная с стационарного состояния), скорость распределение оказалось несколько асимметричным (максимальная относительная ошибка около 1e-10).



Отсутствие модульности
Для многих методов невозможно создавать модульные тесты.

Сложные алгоритмы физического моделирования редко бывают локальными.

То есть каждый пространственный узел в симуляции может взаимодействовать с соседними узлами, и избежать этого невозможно, и это делает модульное тестирование невозможным.

Например, для проверки типичного шага алгоритма LBM или типичных граничных условий в 2D-пространстве необходима система как минимум из 3х3 узлов.

В них нужно задавать плотности, скорости, температуры, проводить предварительные расчеты, задавать контекст моделирования (например, различные поставщики физических свойств и моделей) и т. д. В результате модульность теста постепенно скрывается в дымке.

интеграции.

Еще один (не столь типичный) пример: если вам нужно протестировать алгоритм столкновения в физическом игровом движке, то вы должны указать как минимум два тела, которые будут сталкиваться, их координаты, скорости, геометрию и т.д., что опять же превращает юнит-тест в интеграционный тест.

Большие фасады
Если алгоритм нетривиален, то один публичный метод класса, реализующего этот алгоритм, может скрывать 10-15 методов и 300-400 строк сложного кода [12].

Даже так: 300-400 строк жесткого, беспощадного, неанализируемого кода.

В этом классе может быть 5 переменных типа a, b, gamma, вызовы методов LAPACK [13], библиотек BLAS, генерация случайных чисел и черт знает что еще.

И вы можете всей душой хотеть назвать переменную «а» как-нибудь нормально, потому что знаете, что при разработке корпоративного приложения вас будут долго ругать за такое имя, но вы не можете, потому что так оно и есть.

называется в оригинальной статье и потому не несет в себе никакого очевидного смысла.

Лучшее, что вы можете сделать, — это включить ссылку на статью в начало урока.

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

Столкнувшись с такой ситуацией, лучшим решением оказалось переписать этот алгоритм в MATLAB и сравнить расчеты построчно.



Заключение

Надеюсь, мне удалось привести достаточно веские аргументы и убедить читателей хаба, что зачастую написание одной и той же программы на разных языках разными командами — отличное и (в случае жизненно важных систем) неизбежное дополнение к юнит-тестированию, TDD , разработка через интерфейс, Agile и другие современные методы.

Спасибо за прочтение!

Ссылки

[1] Цифровые самолеты [2] Комментарий к статье Цифровые самолеты [3] Почему вас волнует, что о вас думают другие? , глава «Многострадальное приложение» [4] Решетчатый метод Больцмана [5] Книги по ЛБМ .

Некоторые доступны через Google Books. [6] Решатель Больцмана для параллельной решетки [7] Метод конечных элементов [8] Уравнение Больцмана [9] Распределение Максвелла-Больцмана [10] Мощность [11] Поток Пуазейля [12] Узор Фасада [13] ЛАПАК P.S. Допускаю, что для большинства читателей сайта алгоритм несколько экзотичен, однако считаю это лучшей иллюстрацией.

В конце концов, где бы мы были без научных алгоритмов в статье о них? П.

П.

С.

Ссылки на научные статьи не даю, поскольку все они доступны только по подписке.

П.

П.

С.

Спасибо пользователю СычевИгорь за приглашение!

УПД

Почитав комментарии, думаю, что надо избавиться от фобии магических чисел и написать табличные тесты для всех модулей (типа BoltzmannDistributionProvider) для сравнения с расчетами из Матлабы или расчетами рабочего состояния приложения.

А также добавить подобные системные тесты (например, один раз посчитать динамику узловой системы 5х5, записать в файл и каждый раз сравнивать результаты прогона с этим файлом).

Спасибо всем за советы! Теги: #модульное тестирование #наука #метод решетки-Больцмана #численные методы #научные исследования #разработка веб-сайтов

Вместе с данным постом часто просматривают: