Это продолжение заметки о том, почему юнит-тесты плохо работают в научных приложениях; и в этой статье я хочу рассказать о трудностях поиска ошибок и дебага (научных приложений), с которыми в свое время столкнулся, и многие из которых были удивительными для меня как веб-разработчика.
Статья будет состоять из нескольких разделов:
Основная цель этой статьи—это описание собственного опыта в надежде, что кому-то он будет интересен и необычен (особенно, как мне кажется, промышленным программистам); возможно, кто-то сможет лучше подготовиться к написанию дипломов/курсовых/лабораторных. Преамбулу, постановку научной проблемы, краткое описание алгоритма можно прочитать в упомянутой статье [1]. Поэтому сразу перейду к изложению тех необычных (например, для веб-разработчиков) трудностей поиска ошибок, дабы расширить кругозор читателей и сознание в целом.
Это исключительно короткий пункт для полноты картины, в котором я лишний раз упомяну, что в параллельных программах искать ошибки и проводить дебаг намного сложнее, чем в однопоточных. А большинство ресурсоемких научных программ—параллельные.
Здесь я имею в виду, что ошибка в коде одного класса может проявлять себя в совершенно неожиданных местах. И дело не в плохой архитектуре приложения и высокой связности модулей, а в особенностях задач и моделирования. Например, если при моделировании течения жидкости в трубе профиль скоростей искажен у стенок, это вовсе не значит, что ошибка—именно в алгоритме расчета стенок, она может быть где угодно. И наоборот, если плотность распределена странно в толще жидкости, это не значит, что дело не в алгоритме обсчета стенок.
Сравните это с типичным сценарием бизнес-приложений: если при обработке покупки в интернет-магазине неправильно рассчитываются скидки на товары, то ошибка почти наверняка скрыта в коде расчета скидок на товары.
Можно возразить, что легко определить источник ошибки по истории внесения изменений. Мол, как только приложение перестало работать, искать ошибку надо в измененном коде. Тем не менее, этот подход неприменим к частям программы, добавленным впервые (например, при добавлении теплопереноса, новых граничных условий и так далее), потому что отсутствует предыдущий рабочий вариант (теплопереноса, данных граничных условий и т.п.).
Кроме того, ошибки в научных приложениях могут долгое время не проявляться. Например, если при добавлении нового типа граничных условий вдруг перестал правильно моделироваться поток Пуазейля [2], приводимый в движение силой, а не градиентом давления, то может оказаться, что дело не в новом алгоритме граничных условий, а в логике учета внешней силы, просто до этого ошибка не была критична. (см также пункт «Малая скорость возрастания ошибок» в [1]).
Одна из проблем научных алгоритмов заключается в том, что они зачастую неочевидны. Даже если вы создадите программу с прекрасной архитектурой, для алгоритма отведете специальный класс, отлично его спроектируете и напишете, то, вероятно, вы все равно не сможете избежать нескольких проблем.
Во-первых, это бессмысленные имена переменных (потому что это вспомогательные переменные из исходной научной статьи, не несущие смысловой нагрузки). Во-вторых, это неочевидные операции над переменными класса (потому что они тоже взяты из исходной статьи, в которой получены были методами темной магии типа оптимизации среднеквадратичного отклонения, альтернативы Фредгольма, вычисления пространственных гармоник плотности и т.п.).
Если вы дебажите бизнес-приложение и наблюдаете строчку типа
bool categoryIsVisible = categoryIsEnabled || productsCount > 0;
то вы сразу заметите опечатку, потому что в условии должно стоять логическое «И».
Но представьте, что вам попалась на глаза строчка (из реального проекта)
double probability = latticeVectorWeight * density * (1.0 + 3.0 * dotProduct + 9.0 / 2.0 * dotProduct * dotProduct — 3.0 / 2.0 * velocitySquare);
Вряд ли вы определите, что перепутали где-то плюс с минусом. И, кстати, имена переменных здесь осмысленные.
В этом пункте я постараюсь объяснить, что работа научных приложений намного сильнее (по сравнению с бизнес-приложениями) зависит от входных данных, параметров системы, начального ее состояния—то есть от сеттинга системы.
Основные источники зависимостей от сеттинга следующие.
1. Параметры системы очень сильно (качественно) влияют на результат работы программы, в отличие от бизнес-приложений, где параметры обычно влияют лишь количественно (например, работа CMS не будет принципиально зависеть от того, добавляет ли администратор на страницу текст длиной в пять или в десять строк)
2. Меньшая область устойчивости алгоритмов по входным данным. В бизнес-приложениях основное ограничение на данные—это отсутствие ошибок переполнения (да и кто на него обращает внимание?!). В научных алгоритмах (одно из отличий которых заключается в работе с множествами большей мощности) нужно вспоминать об устойчивости (а вслед за этим и о жесткости дифференциальных уравнений, теории устойчивости, показателях Ляпунова и т.д.) и следить за ней. Кроме того, в бизнес-приложениях все ограничения детерминированы (мол, имя при регистрации не может быть длиннее 100 символов, email должен соответствовать определенному регулярному выражению), в научных же задачах для определения рабочего диапазона входных данных часто приходится пользоваться методом проб и ошибок.
3. Все остальное (трудноформализуемое пока для меня). В частности, это перевод единиц измерения из физических в единицы измерения для программы.
Для иллюстрации этих аспектов я продемонстрирую checklist, который составил для себя после недель тщетного дебага приложения для моделирования гидродинамики. Если я не мог найти ошибку за несколько часов/дней пошагового исполнения, то сверялся с этим чеклистом.
Внимание! Он несколько далек от тематики хабра и интересов большинства читателей, так что можно его при желании пропустить и перейти к следующему пункту.
Итак, checklist:
Первый из пунктов означает, что алгоритм работает только в слабо сжимаемой жидкости, что эквивалентно малым ее скоростям (много меньше скорости звука в жидкости) (ведь течение индуцируется градиентом плотности). Когда я в первый раз забыл об этом ограничении, то потратил несколько дней на поиск ошибки в коде, ведь внешне программа работала почти правильно.
Второй из пунктов эквивалентен проверке области устойчивости алгоритма. Дело в том, что число Рейнольдса определяет, насколько движение жидкости турбулентно и неустойчиво [3]. Чем оно больше, тем течение неустойчивей, чем меньше--тем более «вязкое». Оказывается, что даже если движение никогда не будет турбулентным физически (опять же, в потоке Пуазейля), то расчеты начинают расходиться при достаточно больших числах Рейнольдса. Разумеется, пока я не наступил и на эти грабли (и не походил по ним неделю), то не задумывался о слежении за областью устойчивости.
Третий пункт специфичен только для физических расчетов и некоторых алгоритмов. Использовавшийся метод принимает входные физические величины в специальных единицах решетки (когда единица длины—это шаг равномерной пространственной решетки, и ей же пропорциональна единица времени). До тех пор, пока не наткнулся на специальную статью [4], посвященную переводу величин в этом методе, то безуспешно пытался в течение нескольких недель понять, почему программа ведет себя не совсем верно.
Стоит заметить, что второй и третий пункты едва допускают автоматическую проверку.
Это проблема совершенно невообразима в бизнес-приложениях; и заключается она в том, что часто невозможно наверняка сказать, является ли ошибкой отклонение в поведении программы (от ожидаемого).
Например, известно, что профиль скоростей при течении вязкой жидкости по цилиндрической трубе будет параболическим [2]. Тем не менее, предположим, что при моделировании жидкость у стенок трубы течет чуть быстрее, чем должна. Варианты обычно рассматриваются следующие:
Проверка через модульное тестирование первого пункта осложняется трудностями написания юнит-тестов в таких приложениях [1].
Второй пункт в данном примере легко проверить заменой алгоритма обсчета стенок. Тем не менее, может оказаться, что моделирование с новым методом тоже приводит к искаженным результатам. В таком случае можно попробовать еще парочку алгоритмов (если они в принципе имеются, и если у вас есть время их искать, разбирать и реализовывать).
Проверка третьего пункта, к сожалению, далеко не такая тривиальная. Один из вариантов—это пробовать изменять входные параметры и сеттинг системы, чтобы определить, есть ли область в фазовом пространстве начальных данных, в которой программа работает. Как ни печально, это не так просто, потому что число степеней свободы в исходных условиях при сложном моделировании очень велико (можно задавать разные физические параметры, типа вязкости и теплопроводности; начальное распределение скоростей, сил, плотностей во всей системе и т.п.). Например, в тесте с течением жидкости по трубе мне понадобилось несколько дней, чтобы попробовать начать моделирование не со стационарного распределения скоростей, а с неподвижной жидкости, разгоняемой впоследствии постоянной силой—и ошибка исчезла!
Вот, собственно, и все трудности поиска ошибок, о которых я хотел рассказать. Если у кого-то есть мысли о том, как можно избегать или эффективно справляться с такими эффектами, то буду рад услышать их.
Спасибо за прочтение!
[1] Почему юнит-тесты не работают в научных приложениях
[2] Поток Пуазейля
[3] Число Рейнольдса
[4] Перевод величин в LBM
Статья будет состоять из нескольких разделов:
- Введение, для порядка
- Трудности поиска ошибок
- Параллелизм
- Нелокальность ошибок
- Неочевидность опечаток
- Влияние сеттинга на результат
- Идентификация ошибок: ошибка или нет?
- Заключение
Введение
Основная цель этой статьи—это описание собственного опыта в надежде, что кому-то он будет интересен и необычен (особенно, как мне кажется, промышленным программистам); возможно, кто-то сможет лучше подготовиться к написанию дипломов/курсовых/лабораторных. Преамбулу, постановку научной проблемы, краткое описание алгоритма можно прочитать в упомянутой статье [1]. Поэтому сразу перейду к изложению тех необычных (например, для веб-разработчиков) трудностей поиска ошибок, дабы расширить кругозор читателей и сознание в целом.
Трудности поиска ошибок
Параллелизм
Это исключительно короткий пункт для полноты картины, в котором я лишний раз упомяну, что в параллельных программах искать ошибки и проводить дебаг намного сложнее, чем в однопоточных. А большинство ресурсоемких научных программ—параллельные.
Нелокальность ошибок
Здесь я имею в виду, что ошибка в коде одного класса может проявлять себя в совершенно неожиданных местах. И дело не в плохой архитектуре приложения и высокой связности модулей, а в особенностях задач и моделирования. Например, если при моделировании течения жидкости в трубе профиль скоростей искажен у стенок, это вовсе не значит, что ошибка—именно в алгоритме расчета стенок, она может быть где угодно. И наоборот, если плотность распределена странно в толще жидкости, это не значит, что дело не в алгоритме обсчета стенок.
Сравните это с типичным сценарием бизнес-приложений: если при обработке покупки в интернет-магазине неправильно рассчитываются скидки на товары, то ошибка почти наверняка скрыта в коде расчета скидок на товары.
Можно возразить, что легко определить источник ошибки по истории внесения изменений. Мол, как только приложение перестало работать, искать ошибку надо в измененном коде. Тем не менее, этот подход неприменим к частям программы, добавленным впервые (например, при добавлении теплопереноса, новых граничных условий и так далее), потому что отсутствует предыдущий рабочий вариант (теплопереноса, данных граничных условий и т.п.).
Кроме того, ошибки в научных приложениях могут долгое время не проявляться. Например, если при добавлении нового типа граничных условий вдруг перестал правильно моделироваться поток Пуазейля [2], приводимый в движение силой, а не градиентом давления, то может оказаться, что дело не в новом алгоритме граничных условий, а в логике учета внешней силы, просто до этого ошибка не была критична. (см также пункт «Малая скорость возрастания ошибок» в [1]).
Неочевидность опечаток
Одна из проблем научных алгоритмов заключается в том, что они зачастую неочевидны. Даже если вы создадите программу с прекрасной архитектурой, для алгоритма отведете специальный класс, отлично его спроектируете и напишете, то, вероятно, вы все равно не сможете избежать нескольких проблем.
Во-первых, это бессмысленные имена переменных (потому что это вспомогательные переменные из исходной научной статьи, не несущие смысловой нагрузки). Во-вторых, это неочевидные операции над переменными класса (потому что они тоже взяты из исходной статьи, в которой получены были методами темной магии типа оптимизации среднеквадратичного отклонения, альтернативы Фредгольма, вычисления пространственных гармоник плотности и т.п.).
Если вы дебажите бизнес-приложение и наблюдаете строчку типа
bool categoryIsVisible = categoryIsEnabled || productsCount > 0;
то вы сразу заметите опечатку, потому что в условии должно стоять логическое «И».
Но представьте, что вам попалась на глаза строчка (из реального проекта)
double probability = latticeVectorWeight * density * (1.0 + 3.0 * dotProduct + 9.0 / 2.0 * dotProduct * dotProduct — 3.0 / 2.0 * velocitySquare);
Вряд ли вы определите, что перепутали где-то плюс с минусом. И, кстати, имена переменных здесь осмысленные.
Влияние сеттинга на результат
В этом пункте я постараюсь объяснить, что работа научных приложений намного сильнее (по сравнению с бизнес-приложениями) зависит от входных данных, параметров системы, начального ее состояния—то есть от сеттинга системы.
Основные источники зависимостей от сеттинга следующие.
1. Параметры системы очень сильно (качественно) влияют на результат работы программы, в отличие от бизнес-приложений, где параметры обычно влияют лишь количественно (например, работа CMS не будет принципиально зависеть от того, добавляет ли администратор на страницу текст длиной в пять или в десять строк)
2. Меньшая область устойчивости алгоритмов по входным данным. В бизнес-приложениях основное ограничение на данные—это отсутствие ошибок переполнения (да и кто на него обращает внимание?!). В научных алгоритмах (одно из отличий которых заключается в работе с множествами большей мощности) нужно вспоминать об устойчивости (а вслед за этим и о жесткости дифференциальных уравнений, теории устойчивости, показателях Ляпунова и т.д.) и следить за ней. Кроме того, в бизнес-приложениях все ограничения детерминированы (мол, имя при регистрации не может быть длиннее 100 символов, email должен соответствовать определенному регулярному выражению), в научных же задачах для определения рабочего диапазона входных данных часто приходится пользоваться методом проб и ошибок.
3. Все остальное (трудноформализуемое пока для меня). В частности, это перевод единиц измерения из физических в единицы измерения для программы.
Для иллюстрации этих аспектов я продемонстрирую checklist, который составил для себя после недель тщетного дебага приложения для моделирования гидродинамики. Если я не мог найти ошибку за несколько часов/дней пошагового исполнения, то сверялся с этим чеклистом.
Внимание! Он несколько далек от тематики хабра и интересов большинства читателей, так что можно его при желании пропустить и перейти к следующему пункту.
Итак, checklist:
- Проверить несжимаемость
- Проверить числа Рейнольдса
- Проверить перевод величин
Первый из пунктов означает, что алгоритм работает только в слабо сжимаемой жидкости, что эквивалентно малым ее скоростям (много меньше скорости звука в жидкости) (ведь течение индуцируется градиентом плотности). Когда я в первый раз забыл об этом ограничении, то потратил несколько дней на поиск ошибки в коде, ведь внешне программа работала почти правильно.
Второй из пунктов эквивалентен проверке области устойчивости алгоритма. Дело в том, что число Рейнольдса определяет, насколько движение жидкости турбулентно и неустойчиво [3]. Чем оно больше, тем течение неустойчивей, чем меньше--тем более «вязкое». Оказывается, что даже если движение никогда не будет турбулентным физически (опять же, в потоке Пуазейля), то расчеты начинают расходиться при достаточно больших числах Рейнольдса. Разумеется, пока я не наступил и на эти грабли (и не походил по ним неделю), то не задумывался о слежении за областью устойчивости.
Третий пункт специфичен только для физических расчетов и некоторых алгоритмов. Использовавшийся метод принимает входные физические величины в специальных единицах решетки (когда единица длины—это шаг равномерной пространственной решетки, и ей же пропорциональна единица времени). До тех пор, пока не наткнулся на специальную статью [4], посвященную переводу величин в этом методе, то безуспешно пытался в течение нескольких недель понять, почему программа ведет себя не совсем верно.
Стоит заметить, что второй и третий пункты едва допускают автоматическую проверку.
Идентификация ошибок: ошибка или нет?
Это проблема совершенно невообразима в бизнес-приложениях; и заключается она в том, что часто невозможно наверняка сказать, является ли ошибкой отклонение в поведении программы (от ожидаемого).
Например, известно, что профиль скоростей при течении вязкой жидкости по цилиндрической трубе будет параболическим [2]. Тем не менее, предположим, что при моделировании жидкость у стенок трубы течет чуть быстрее, чем должна. Варианты обычно рассматриваются следующие:
- это на самом деле ошибка
- это особенность алгоритма
- это следствие неправильных или неподходящих для алгоритма входных данных (начальных условий, физических параметров) (см. «Влияние сеттинга на результат»)
Проверка через модульное тестирование первого пункта осложняется трудностями написания юнит-тестов в таких приложениях [1].
Второй пункт в данном примере легко проверить заменой алгоритма обсчета стенок. Тем не менее, может оказаться, что моделирование с новым методом тоже приводит к искаженным результатам. В таком случае можно попробовать еще парочку алгоритмов (если они в принципе имеются, и если у вас есть время их искать, разбирать и реализовывать).
Проверка третьего пункта, к сожалению, далеко не такая тривиальная. Один из вариантов—это пробовать изменять входные параметры и сеттинг системы, чтобы определить, есть ли область в фазовом пространстве начальных данных, в которой программа работает. Как ни печально, это не так просто, потому что число степеней свободы в исходных условиях при сложном моделировании очень велико (можно задавать разные физические параметры, типа вязкости и теплопроводности; начальное распределение скоростей, сил, плотностей во всей системе и т.п.). Например, в тесте с течением жидкости по трубе мне понадобилось несколько дней, чтобы попробовать начать моделирование не со стационарного распределения скоростей, а с неподвижной жидкости, разгоняемой впоследствии постоянной силой—и ошибка исчезла!
Заключение
Вот, собственно, и все трудности поиска ошибок, о которых я хотел рассказать. Если у кого-то есть мысли о том, как можно избегать или эффективно справляться с такими эффектами, то буду рад услышать их.
Спасибо за прочтение!
Ссылки:
[1] Почему юнит-тесты не работают в научных приложениях
[2] Поток Пуазейля
[3] Число Рейнольдса
[4] Перевод величин в LBM