UNPKG

agentscape

Version:

Agentscape is a library for creating agent-based simulations. It provides a simple API for defining agents and their behavior, and for defining the environment in which the agents interact. Agentscape is designed to be flexible and extensible, allowing

283 lines 11 kB
import Agent from './Agent'; import { Angle, Color, RandomGenerator, Vector2 } from '../numbers'; /** * Derive a value from a function of the cell's current state * instead of storing it as a constant. */ export function Dynamic(f) { return function (target, propertyKey) { Object.defineProperty(target, propertyKey, { get: function () { return f(this); }, set: function (value) { return value; } }); }; } /** * A cell in a grid. The basic unit of space for a model. * May be used as-is or extended to include additional properties. * * The default cell has a `location`, `color`, and `energy`. Only the `location` is required. * The default color is white and the default energy is 0. * * Additionally, a cell has a set of `tenantAgents` which are agents that occupy the cell * at the current time step. */ export default class Cell { constructor(opts) { var _a, _b, _c; this.tenantAgents = new Set(); this.location = opts.location; this.energy = (_a = opts.energy) !== null && _a !== void 0 ? _a : 0; this.color = (_b = opts.color) !== null && _b !== void 0 ? _b : Color.fromName('white'); this.strokeColor = (_c = opts.strokeColor) !== null && _c !== void 0 ? _c : undefined; } /** * Gives the distance between this Cell and another Cell or Agent. */ distanceTo(world, other, options) { const { metric = 'euclidean' } = options !== null && options !== void 0 ? options : {}; const otherPosition = other instanceof Agent ? other.position : new Vector2(other.location); if (world.boundaryCondition === 'periodic') { const width = world.cells.length; const dx = Math.abs(this.location[0] - otherPosition.x); const dy = Math.abs(this.location[1] - otherPosition.y); const toroidalDx = Math.min(dx, width - dx); const toroidalDy = Math.min(dy, width - dy); if (metric === 'euclidean') { return Math.sqrt(toroidalDx ** 2 + toroidalDy ** 2); } else { return toroidalDx + toroidalDy; } } else { if (metric === 'euclidean') { return Math.sqrt((this.location[0] - otherPosition.x) ** 2 + (this.location[1] - otherPosition.y) ** 2); } else { return Math.abs(this.location[0] - otherPosition.x) + Math.abs(this.location[1] - otherPosition.y); } } } /** * Gets all Cells within a given square radius. */ getNeighborsInRadius(world, options) { const cells = world.cells; const neighbors = new Set(); const x = this.location[0]; const y = this.location[1]; const { radius = 1, includeSelf = false } = options !== null && options !== void 0 ? options : {}; const width = cells.length; // assert degree is a positive integer if (!Number.isInteger(radius) || radius < 1) { throw new Error('getNeighborsInRadius radius option must be a positive integer.'); } for (let i = -radius; i <= radius; i++) { for (let j = -radius; j <= radius; j++) { const newX = x + i; const newY = y + j; if (newX >= 0 && newX < width && newY >= 0 && newY < width) { neighbors.add(cells[newX][newY]); } else if (world.boundaryCondition === 'periodic') { neighbors.add(cells[(newX + width) % width][(newY + width) % width]); } } } if (!includeSelf) { neighbors.delete(this); } return Array.from(neighbors); } /** * Diffuses a property to neighboring cells. * The property function should return a number for each cell. * The cell diffuses equal shares of `rate` percentage of the property to each neighbor. */ diffuse(world, property, setter, rate, range = 1) { // Get the property value for this cell const value = property(this); const neighbors = this.getNeighborsInRadius(world, { radius: range }); // Calculate the total amount to diffuse const totalDiffusionAmount = value * rate; // Calculate the amount to diffuse to each neighbor const amountPerNeighbor = totalDiffusionAmount / neighbors.length; // Deplete the property on this cell by the total amount diffused setter(this, value - totalDiffusionAmount); // Diffuse the property to each neighbor for (const neighbor of neighbors) { const neighborValue = property(neighbor); setter(neighbor, neighborValue + amountPerNeighbor); } } /** * Performs a 2D convolution on the cell's neighbors using the given kernel. * The kernel is a 3x3 matrix of numbers. * The property function should return a number for each cell. * */ getNeighborConvolution(world, kernel, property) { const neighbors = []; const x = this.location[0]; const y = this.location[1]; const width = world.cells.length; // TODO: confirm this works with this.getNeighbors // gets the values of the neighbors in row-major order // with finite boundary conditions if (world.boundaryCondition === 'finite') { for (let i = -1; i <= 1; i++) { for (let j = -1; j <= 1; j++) { const newX = x + i; const newY = y + j; if (newX >= 0 && newX < width && newY >= 0 && newY < width) { const cell = world.cells[newX][newY]; neighbors.push(property(cell)); } else { neighbors.push(0); } } } } else { // gets the values of the neighbors in row-major order // with toroidal boundary conditions for (let i = -1; i <= 1; i++) { for (let j = -1; j <= 1; j++) { const newX = (x + i + width) % width; const newY = (y + j + width) % width; const cell = world.cells[newX][newY]; neighbors.push(property(cell)); } } } // perform the convolution let result = 0; for (let i = 0; i < kernel.length; i++) { for (let j = 0; j < kernel[i].length; j++) { result += kernel[i][j] * neighbors[i * 3 + j]; } } return result; } /** * Gets the four neighbors of the cell. * Optionally includes the four diagonal neighbors and this cell. */ getNeighbors(world, options) { const boundaryCondition = world.boundaryCondition; const { includeDiagonals = false, includeSelf = false, } = options !== null && options !== void 0 ? options : {}; const cells = world.cells; const neighbors = new Set(); const wrap = (coord, max) => (coord + max) % max; const gridWidth = world.width; const gridHeight = world.height; const x = this.location[0]; const y = this.location[1]; if (boundaryCondition === 'periodic') { // left neighbors.add(cells[wrap(x - 1, gridWidth)][wrap(y, gridHeight)]); // right neighbors.add(cells[wrap(x + 1, gridWidth)][wrap(y, gridHeight)]); // top neighbors.add(cells[wrap(x, gridHeight)][wrap(y - 1, gridHeight)]); // bottom neighbors.add(cells[wrap(x, gridHeight)][wrap(y + 1, gridHeight)]); if (includeDiagonals) { // top left neighbors.add(cells[wrap(x - 1, gridHeight)][wrap(y - 1, gridHeight)]); // top right neighbors.add(cells[wrap(x + 1, gridHeight)][wrap(y - 1, gridHeight)]); // bottom left neighbors.add(cells[wrap(x - 1, gridHeight)][wrap(y + 1, gridHeight)]); // bottom right neighbors.add(cells[wrap(x + 1, gridHeight)][wrap(y + 1, gridHeight)]); } } else if (boundaryCondition === 'finite') { // left if (x > 0) { neighbors.add(cells[x - 1][y]); } // right if (x < gridWidth - 1) { neighbors.add(cells[x + 1][y]); } // up if (y > 0) { neighbors.add(cells[x][y - 1]); } // down if (y < gridHeight - 1) { neighbors.add(cells[x][y + 1]); } if (includeDiagonals) { if (x > 0 && y > 0) { // top-left neighbors.add(cells[x - 1][y - 1]); } if (x > 0 && y < gridHeight - 1) { // bottom-left neighbors.add(cells[x - 1][y + 1]); } if (x < gridWidth - 1 && y > 0) { // top-right neighbors.add(cells[x + 1][y - 1]); } if (x < gridWidth - 1 && y < gridHeight - 1) { neighbors.add(cells[x + 1][y + 1]); } } } else { throw new Error(`Unknown boundary condition: ${boundaryCondition}`); } if (includeSelf) { neighbors.add(this); } return Array.from(neighbors); } toJSON() { const propNames = Object.getOwnPropertyNames(this); propNames.forEach((key) => { if (typeof this[key] === 'function') { delete this[key]; } }); const json = {}; propNames.forEach((key) => { if (this[key] instanceof Set) { json[key] = Array.from(this[key]); return; } if (this[key] instanceof Vector2) { json[key] = this[key].components; return; } if (this[key] instanceof RandomGenerator) { return; } if (this[key] instanceof Map) { json[key] = Array.from(this[key].entries()); return; } if (this[key] instanceof Angle) { json[key] = `${this[key].asDegrees()} deg`; return; } if (this[key] instanceof Color) { json[key] = this[key].toRGB(); return; } json[key] = this[key]; }); return json; } } //# sourceMappingURL=Cell.js.map