Похоже, ваша проблема в том, что ваше представление ваших агентов (с использованием словарей) занимает слишком много памяти. Если это так, решение состоит в том, чтобы найти более компактное представление.
Поскольку вы работаете на объектно-ориентированном языке, типичным решением будет определение класса агента, возможно с подклассами для различных типов агентов. и используйте переменные экземпляра для хранения состояния каждого агента. Тогда ваша сетка CA будет массивом экземпляров Агента (или, возможно, пустыми для свободных ячеек). Это будет намного более компактно, чем использование словарей со строковыми ключами.
Кроме того, я бы рекомендовал не хранить положение агента в сетке как часть состояния агента, но передавать это как параметр для любых методов, которые нуждаются в этом. Это не только экономит немного памяти само по себе, но также позволяет размещать ссылки на один и тот же экземпляр агента в нескольких ячейках сетки, чтобы представлять несколько идентичных агентов. В зависимости от того, как часто такие идентичные агенты встречаются в вашем ЦС, это может сэкономить огромный объем памяти.
Обратите внимание, что если вы изменяете состояние такого повторно используемого экземпляра агента, модификация очевидно, повлияет на всех агентов в сетке, представленной этим экземпляром. По этой причине может быть хорошей идеей сделать объекты агента неизменяемыми и просто создавать новые при каждом изменении состояния агента.
Вы могли бы также захотеть сохранить кэш ( например, набор) экземпляров Агента, уже находящихся в сетке, чтобы вы могли легко проверить, идентичен ли новый агент существующему. Будет ли это действительно полезно, зависит от вашей конкретной модели c CA - с некоторым CA вы могли бы достаточно хорошо справляться с дедупликацией даже без такого кэша (вполне нормально иметь дубликат some Объекты-агенты), в то время как для других может просто не хватить идентичных агентов, чтобы сделать это стоящим. Кроме того, если вы попробуете это, обратите внимание, что вам нужно либо спроектировать кеш, чтобы использовать слабые ссылки (что может быть сложно, чтобы получить правильные данные), либо периодически очищать и перестраивать его, чтобы избежать старых объектов Агента, задерживающихся в кеше, даже после того, как они были удалены из таблицы.
Приложение на основе вашего комментария ниже, который я процитирую здесь:
Представьте себе среду, в которой температура меняется в зависимости от сезона (так что свойство на графике). Есть клетки земли и воды (так что свойства на клетках), и при достаточно низких температурах клетки воды замерзают, так что агенты животных могут использовать их для пересечения между участками земли. Представьте, что эти агенты животных охотятся на других агентов животных, чтобы съесть их (поэтому свойства агентов). Представьте, что животные агенты, которых едят, едят деревья (то есть другие агенты, обладающие свойствами) и, как правило, едят молодые саженцы (ограничивая рост деревьев), тем самым ограничивая их собственный рост (и рост агентов плотоядных).
Хорошо, давайте набросаем классы, которые вам понадобятся. (Пожалуйста, извините за любые синтаксические ошибки; я на самом деле не C# программист, и я на самом деле не тестировал этот код. Просто подумайте о нем как о C# -подобном псевдокоде или о чем-то.)
Сначала все, вам, очевидно, понадобится куча агентов. Давайте определим абстрактный суперкласс для них:
public abstract class Agent {
public abstract void Act(Grid grid, int x, int y, float time);
}
Наша CA-симуляция (для простоты я собираюсь предположить, что она стохастична c, то есть та, где агенты действуют по одному в случайный порядок, как в алгоритме Gillesp ie ), в основном будет включать многократный выбор случайной ячейки ( x , y ) в сетке, проверка, содержит ли эта ячейка агент, и, если да, вызов Act()
для этого агента. (Нам также потребуется обновить любое зависящее от времени глобальное состояние, пока мы это делаем, но давайте оставим это на потом.)
Методы Act()
для агентов будут получать ссылку на объект сетки и могут вызывать его методы для внесения изменений в состояние соседних ячеек (или даже получать ссылку на агентов в этих ячейках и вызывать их методы). напрямую). Это может включать, например, удаление другого агента из сетки (потому что он только что был съеден), добавление нового агента (воспроизведение), изменение местоположения действующего агента (движение) или даже удаление этого агента из сетки (например, потому что он умер или умер от старость). Для иллюстрации давайте нарисуем несколько классов агентов:
public class Sapling : Agent {
private static readonly double MATURATION_TIME = 10; // arbitrary time delay
private double birthTime; // could make this a float to save memory
public Sapling(double time) => birthTime = time;
public override void Act(Grid grid, int x, int y, double time) {
// if the sapling is old enough, it replaces itself with a tree
if (time >= birthTime + MATURATION_TIME) {
grid.SetAgentAt(x, y, Tree.INSTANCE);
}
}
}
public class Tree : Agent {
public static readonly Tree INSTANCE = new Tree();
public override void Act(Grid grid, int x, int y, double time) {
// trees create saplings in nearby land cells
(int x2, int y2) = grid.RandomNeighborOf(x, y);
if (grid.GetAgentAt(x2, y2) == null && grid.CellTypeAt(x2, y2) == CellType.Land) {
grid.SetAgentAt(x2, y2, new Sapling(time));
}
}
}
Для краткости я оставлю реализацию агентов по животным в качестве упражнения. Кроме того, реализации Tree и Sapling, приведенные выше, являются довольно грубыми и могут быть улучшены различными способами, но они должны, по крайней мере, проиллюстрировать концепцию.
Стоит отметить, что для минимизации использования памяти классы агентов выше иметь как можно меньше внутреннего состояния. В частности, агенты не сохраняют свое собственное местоположение в сетке, а получают его в качестве аргументов метода act()
. Так как из-за пропуска местоположения мой класс Tree полностью лишился состояния, я пошел дальше и использовал один и тот же глобальный экземпляр Tree для всех деревьев в сетке! Хотя это не всегда возможно, но когда это так, это может сэкономить много памяти.
Теперь, как насчет сетки? Базовая реализация c (на мгновение игнорируя различные типы ячеек) выглядела бы примерно так:
public class Grid {
private readonly int width, height;
private readonly Agent?[,] agents;
public Grid(int w, int h) {
width = w;
height = h;
agents = new Agent?[w, h];
}
// TODO: handle grid edges
public Agent? GetAgentAt(int x, int y) => agents[x, y];
public void SetAgentAt(int x, int y, Agent? agent) => agents[x, y] = agent;
}
Теперь, как насчет типов ячеек? У вас есть несколько способов справиться с ними.
Один из способов - заставить сетку хранить массив объектов Cell вместо агентов, и каждая ячейка должна хранить свое состояние и (возможно) агента. Но для оптимизации использования памяти, вероятно, лучше просто иметь отдельный 2D-массив, хранящий типы ячеек, что-то вроде этого:
public enum CellType : byte { Land, Water, Ice }
public class Grid {
private readonly Random rng = new Random();
private readonly int width, height;
private readonly Agent?[,] agents;
private readonly CellType[,] cells; // TODO: init in constructor?
private float temperature = 20; // global temperature in Celsius
// ...
public CellType CellTypeAt(int x, int y) {
CellType type = cells[x,y];
if (type == CellType.Water && temperature < 0) return CellType.Ice;
else return type;
}
}
Обратите внимание, что перечисление CellType основано на байтах, которое должно сохранять массив, хранящий их немного компактнее, чем если бы они были основаны на int.
Теперь давайте наконец посмотрим на симуляцию основного CA l oop. По своей сути c это может выглядеть так:
Grid grid = new Grid(width, height);
grid.SetAgentAt(width / 2, height / 2, Tree.INSTANCE);
// average simulation time per loop iteration, assuming that each
// actor on the grid acts once per unit time step on average
double dt = 1 / (width * height);
for (double t = 0; t < maxTime; t += dt) {
(int x, int y) = grid.GetRandomLocation();
Agent? agent = grid.GetAgentAt(x, y);
if (agent != null) agent.Act(grid, x, y, t);
// TODO: update temperature here?
}
(Технически, чтобы правильно реализовать алгоритм Gillesp ie, приращение времени моделирования между итерациями должно составлять , экспоненциально распределенное случайное число со средним dt
, не постоянным приращением. Однако, поскольку в каждой итерации выбирается только один субъект из одной из width * height
ячеек, количество итераций между действиями одного и того же действующего лица - это , геометрически распределенное со средним width * height
, и умножение его на dt = 1 / (width * height)
дает превосходное приближение для экспоненциального распределения со средним 1. Что является многословным способом сказать, что на практике использование шаг с постоянным временем - это прекрасно.)
Поскольку это становится достаточно длинным, я позволю вам продолжить отсюда. Я просто отмечу, что существует множество способов дальнейшего расширения и / или оптимизации алгоритма, который я набросал выше.
Например, вы можете ускорить симуляцию, сохранив список всех местоположений сетки, которые содержит живого актера и случайную выборку актеров из этого списка (но тогда вам также необходимо масштабировать временной шаг обратно пропорционально длине списка). Кроме того, вы можете решить, что хотите, чтобы у одних актеров больше шансов действовать, чем у других; в то время как простой способ сделать это - просто использовать выборку отклонения (то есть, чтобы актер делал что-то, только если rng.Sample() < prob
для некоторых prob
между 0 и 1), более эффективный способ состоял бы в том, чтобы поддерживать несколько списков местоположений в зависимости от тип актера там.