У меня нет опыта в разработке сложных симуляций, но могу поделиться своими мыслями, если интересно.
В общем, имхо, вне зависимости от того, какой язык/фреймворк ты будешь использовать, твоя основная проблема будет в аллокациях. При заявленных размерах, надо будет очень жёстко их бюджетировать и профилировать.
Самая очевидная проблема: так как симуляция обычно вычисляется как
x_{n+1} = f(x_n), то среда и агенты должны быть иммутабельными, то есть к концу вычисления нового шага у тебя в памяти будут две копии среды - от предыдущего шага и от нового. Если (условно) у тебя одно состояние занимает 10 Гб, то два состояния будут занимать 20 и меньше ты не сделаешь никак. Можно конечно пытаться как-то хранить распределённо, но это чистый кастом, вряд ли фреймворки из коробки позволят тебе это сделать за просто так.
Но это только одна сторона проблемы, менее тривиальный аспект заключается в том, что все промежуточные вычисления могут аллоцировать и это нужно ловить постоянно. Каждый аллоцированный байт нужно умножать на 10 млн агентов и он превращается в 10 мегабайт. Каждый аллоцированный килобайт превращается в 10 гигабайт и вычисление будет падать с Out Of Memory.
Например: ты вызываешь какую-нибудь невинную функцию, типа
neighbours(agent) , которая возвращает массив соседей данного агента. Под капотом она может быть устроена как-то так
function neighbours(agent)
1. Создать массив, в котором будем накапливать результат
2. с помощью какого-нибудь более-менее продвинутого пространственного индекса обежать окрестность агента, если в ней находятся другие агенты, то `push!` их в промежуточный массив
3. Вернуть массив агентов
Понятно, что начинка может меняться, но основная мысль, которую хочу сказать - в этом подходе аллоцируется массив, потом он ещё несколько раз переаллоцируется когда
push! пытается его растянуть.
Хотя массив будет хранить только указатели на агентов, это всё равно 8 байт на агента + оверхед от самого массива. Если у тебя в среднем 10 соседей, то это уже получится в районе 100 байт на один вызов. Умножаем на 10 миллионов агентов, и...
Правильная версия разумеется в состоит в создании контейнера, который будет переиспользоваться
function neighbours!(neighbours_container, agent)
1. создаем инкрементную переменную i, которая указывает куда будет записываться новый сосед.
2. Пробежать по окрестности агента, находя соседей, записываем их в neighbours_container, инкрементим переменную i
3. Возвращаем neighbours_container (хотя это уже опционально)
Это в свою очередь правда создаст сложности, когда эту штуку надо будет паралелить, потому что тогда надо будет создавать столько копий этого контейнера, сколько процессов будет запускаться и следить за тем, чтобы они не перепутались случайно.