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
JavaScript
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