Module crowd-control.classes.lattice

Expand source code
import numpy as np
import random 

from numpy import ndarray
from classes.cell import Cell 
from helpers import get_neighbor_coords, get_value_array, get_distance_array

class Lattice:
    """
    A Lattice is a 2D discrete grid of cells objects, initialized with empty cells.

    Attributes:
        len_x (int): Number of rows.
        len_y (int): Number of columns.
        cells (ndarray[Cell]): 2D array of cell objects
        n_cells (int): Number of cells in the lattice
    """

    def __init__(self, len_x, len_y) -> None:
        """
        Initializes empty Lattice object.

        Args:
            len_x (int): Number of rows.
            len_y (int): Number of columns.
        """
        self.len_x = len_x
        self.len_y = len_y
        self.cells = self.initialize_grid()
        self.n_cells = len_x * len_y

    def initialize_grid(self) -> ndarray[Cell]:
        """
        Create empty cell objects and return as an 2D array.
        """
        cells = [[] for _ in range(self.len_x)]
        for x in range(self.len_x):
            for y in range(self.len_y):
                cell = Cell(x, y, y, self.len_y - y)
                cells[x].append(cell)

        return np.array(cells)
    
    def load_neighbours(self):
        """
        Assign neighbors to all cell objects.
        """
        for cell in self.cells.flatten():
            neighbor_coords = get_neighbor_coords(cell.x, cell.y, self.len_x, self.len_y)
            for coords in neighbor_coords:
                neighbor = self.cells[coords]
                cell.add_neighbor(neighbor)
    
    def get_random_cell(self) -> Cell:
        """
        Select a random cell on the lattice and return it.
        """
        random_x = random.randint(0, self.len_x - 1)
        random_y = random.randint(0, self.len_y - 1)
        return self.cells[random_x, random_y]

    def get_random_empty_edge_cell(self, y, x, p) -> Cell | None:
        """
        Return unpopulated cell on edge around same row number. 
        Return None if no empty cell found.
        Used for periodic boundary conditions.

        Args:
            y (int): Column number of edge
            x (int): Row number of cell
            p (float): Probability of moving straight.
        """
        x_list = []
        for i in range(x-1, x+2):
            x_list.append(i) if i >= 0 and i < self.len_x else None

        # try to target cell on same row
        if self.cells[x, y].is_empty() and random.random() <= p:
            return self.cells[x, y]

        # otherwise pick a random one
        random.shuffle(x_list)
        for x_n in x_list:
            if self.cells[x_n, y].is_empty():
                return self.cells[x_n, y] 

        # no empty cell was found
        return None    
        
    def get_populated_cells(self) -> ndarray[Cell]:
        """
        Return all populated cell objects.
        """
        return self.cells[get_value_array(self.cells) != 0]

    def populate_corridor(self, N):
        """
        Randomly populate lattice with left-moving and right-moving agents.

        Args:
            N (int): Number of cells to populate.
        """
        assert N <= self.cells.size, 'Number of people is larger than number of cells'
        
        for _ in range(N):
            value = 1 if random.random() < 0.5 else -1
            self.populate_random_cell(value)

    def populate_random_cell(self, value):
        """
        Populate a random cell and return it.

        Args:
            value (int): Decides if cell is populated by right-mover or left-mover.
        """
        assert isinstance(value, int) and value in [-1, 1], 'value must be +/- 1 integer'

        cell = self.get_random_cell()
        while not self.populate_cell(cell, value):
            cell = self.get_random_cell()
        return cell
    
    def populate_cell(self, cell, value):
        """
        Populate cell with a left or right-moving individual if it is empty.
        Return True if successful, else False.

        Args:
            cell (Cell): Cell to populate.
            value (int): Decides if cell is populated by right-mover or left-mover.
        """
        assert isinstance(value, int) and value in [-1, 1]

        if not cell.is_empty():
            return False
        
        cell.populate(value)
        return True
    
    def calculate_lane_formation(self):
        """
        Calculates the objective function for lane formation.
        Returns phi, a number between zero and one.
        """
        phi = 0
        N = len(self.get_populated_cells())
        value_array = get_value_array(self.cells)
        for row in range(self.len_x):
            counter_left = np.count_nonzero(value_array[row] == -1)
            counter_right = np.count_nonzero(value_array[row] == 1)
            if counter_left > 1 or counter_right > 1:
                phi += ((counter_left - counter_right)**2/(counter_left + counter_right))/N
        return phi

    def __str__(self) -> str:
        return f'{get_distance_array(self.cells, 1)}'

Classes

class Lattice (len_x, len_y)

A Lattice is a 2D discrete grid of cells objects, initialized with empty cells.

Attributes

len_x : int
Number of rows.
len_y : int
Number of columns.
cells : ndarray[Cell]
2D array of cell objects
n_cells : int
Number of cells in the lattice

Initializes empty Lattice object.

Args

len_x : int
Number of rows.
len_y : int
Number of columns.
Expand source code
class Lattice:
    """
    A Lattice is a 2D discrete grid of cells objects, initialized with empty cells.

    Attributes:
        len_x (int): Number of rows.
        len_y (int): Number of columns.
        cells (ndarray[Cell]): 2D array of cell objects
        n_cells (int): Number of cells in the lattice
    """

    def __init__(self, len_x, len_y) -> None:
        """
        Initializes empty Lattice object.

        Args:
            len_x (int): Number of rows.
            len_y (int): Number of columns.
        """
        self.len_x = len_x
        self.len_y = len_y
        self.cells = self.initialize_grid()
        self.n_cells = len_x * len_y

    def initialize_grid(self) -> ndarray[Cell]:
        """
        Create empty cell objects and return as an 2D array.
        """
        cells = [[] for _ in range(self.len_x)]
        for x in range(self.len_x):
            for y in range(self.len_y):
                cell = Cell(x, y, y, self.len_y - y)
                cells[x].append(cell)

        return np.array(cells)
    
    def load_neighbours(self):
        """
        Assign neighbors to all cell objects.
        """
        for cell in self.cells.flatten():
            neighbor_coords = get_neighbor_coords(cell.x, cell.y, self.len_x, self.len_y)
            for coords in neighbor_coords:
                neighbor = self.cells[coords]
                cell.add_neighbor(neighbor)
    
    def get_random_cell(self) -> Cell:
        """
        Select a random cell on the lattice and return it.
        """
        random_x = random.randint(0, self.len_x - 1)
        random_y = random.randint(0, self.len_y - 1)
        return self.cells[random_x, random_y]

    def get_random_empty_edge_cell(self, y, x, p) -> Cell | None:
        """
        Return unpopulated cell on edge around same row number. 
        Return None if no empty cell found.
        Used for periodic boundary conditions.

        Args:
            y (int): Column number of edge
            x (int): Row number of cell
            p (float): Probability of moving straight.
        """
        x_list = []
        for i in range(x-1, x+2):
            x_list.append(i) if i >= 0 and i < self.len_x else None

        # try to target cell on same row
        if self.cells[x, y].is_empty() and random.random() <= p:
            return self.cells[x, y]

        # otherwise pick a random one
        random.shuffle(x_list)
        for x_n in x_list:
            if self.cells[x_n, y].is_empty():
                return self.cells[x_n, y] 

        # no empty cell was found
        return None    
        
    def get_populated_cells(self) -> ndarray[Cell]:
        """
        Return all populated cell objects.
        """
        return self.cells[get_value_array(self.cells) != 0]

    def populate_corridor(self, N):
        """
        Randomly populate lattice with left-moving and right-moving agents.

        Args:
            N (int): Number of cells to populate.
        """
        assert N <= self.cells.size, 'Number of people is larger than number of cells'
        
        for _ in range(N):
            value = 1 if random.random() < 0.5 else -1
            self.populate_random_cell(value)

    def populate_random_cell(self, value):
        """
        Populate a random cell and return it.

        Args:
            value (int): Decides if cell is populated by right-mover or left-mover.
        """
        assert isinstance(value, int) and value in [-1, 1], 'value must be +/- 1 integer'

        cell = self.get_random_cell()
        while not self.populate_cell(cell, value):
            cell = self.get_random_cell()
        return cell
    
    def populate_cell(self, cell, value):
        """
        Populate cell with a left or right-moving individual if it is empty.
        Return True if successful, else False.

        Args:
            cell (Cell): Cell to populate.
            value (int): Decides if cell is populated by right-mover or left-mover.
        """
        assert isinstance(value, int) and value in [-1, 1]

        if not cell.is_empty():
            return False
        
        cell.populate(value)
        return True
    
    def calculate_lane_formation(self):
        """
        Calculates the objective function for lane formation.
        Returns phi, a number between zero and one.
        """
        phi = 0
        N = len(self.get_populated_cells())
        value_array = get_value_array(self.cells)
        for row in range(self.len_x):
            counter_left = np.count_nonzero(value_array[row] == -1)
            counter_right = np.count_nonzero(value_array[row] == 1)
            if counter_left > 1 or counter_right > 1:
                phi += ((counter_left - counter_right)**2/(counter_left + counter_right))/N
        return phi

    def __str__(self) -> str:
        return f'{get_distance_array(self.cells, 1)}'

Methods

def calculate_lane_formation(self)

Calculates the objective function for lane formation. Returns phi, a number between zero and one.

Expand source code
def calculate_lane_formation(self):
    """
    Calculates the objective function for lane formation.
    Returns phi, a number between zero and one.
    """
    phi = 0
    N = len(self.get_populated_cells())
    value_array = get_value_array(self.cells)
    for row in range(self.len_x):
        counter_left = np.count_nonzero(value_array[row] == -1)
        counter_right = np.count_nonzero(value_array[row] == 1)
        if counter_left > 1 or counter_right > 1:
            phi += ((counter_left - counter_right)**2/(counter_left + counter_right))/N
    return phi
def get_populated_cells(self) ‑> numpy.ndarray[classes.cell.Cell]

Return all populated cell objects.

Expand source code
def get_populated_cells(self) -> ndarray[Cell]:
    """
    Return all populated cell objects.
    """
    return self.cells[get_value_array(self.cells) != 0]
def get_random_cell(self) ‑> classes.cell.Cell

Select a random cell on the lattice and return it.

Expand source code
def get_random_cell(self) -> Cell:
    """
    Select a random cell on the lattice and return it.
    """
    random_x = random.randint(0, self.len_x - 1)
    random_y = random.randint(0, self.len_y - 1)
    return self.cells[random_x, random_y]
def get_random_empty_edge_cell(self, y, x, p) ‑> classes.cell.Cell | None

Return unpopulated cell on edge around same row number. Return None if no empty cell found. Used for periodic boundary conditions.

Args

y : int
Column number of edge
x : int
Row number of cell
p : float
Probability of moving straight.
Expand source code
def get_random_empty_edge_cell(self, y, x, p) -> Cell | None:
    """
    Return unpopulated cell on edge around same row number. 
    Return None if no empty cell found.
    Used for periodic boundary conditions.

    Args:
        y (int): Column number of edge
        x (int): Row number of cell
        p (float): Probability of moving straight.
    """
    x_list = []
    for i in range(x-1, x+2):
        x_list.append(i) if i >= 0 and i < self.len_x else None

    # try to target cell on same row
    if self.cells[x, y].is_empty() and random.random() <= p:
        return self.cells[x, y]

    # otherwise pick a random one
    random.shuffle(x_list)
    for x_n in x_list:
        if self.cells[x_n, y].is_empty():
            return self.cells[x_n, y] 

    # no empty cell was found
    return None    
def initialize_grid(self) ‑> numpy.ndarray[classes.cell.Cell]

Create empty cell objects and return as an 2D array.

Expand source code
def initialize_grid(self) -> ndarray[Cell]:
    """
    Create empty cell objects and return as an 2D array.
    """
    cells = [[] for _ in range(self.len_x)]
    for x in range(self.len_x):
        for y in range(self.len_y):
            cell = Cell(x, y, y, self.len_y - y)
            cells[x].append(cell)

    return np.array(cells)
def load_neighbours(self)

Assign neighbors to all cell objects.

Expand source code
def load_neighbours(self):
    """
    Assign neighbors to all cell objects.
    """
    for cell in self.cells.flatten():
        neighbor_coords = get_neighbor_coords(cell.x, cell.y, self.len_x, self.len_y)
        for coords in neighbor_coords:
            neighbor = self.cells[coords]
            cell.add_neighbor(neighbor)
def populate_cell(self, cell, value)

Populate cell with a left or right-moving individual if it is empty. Return True if successful, else False.

Args

cell : Cell
Cell to populate.
value : int
Decides if cell is populated by right-mover or left-mover.
Expand source code
def populate_cell(self, cell, value):
    """
    Populate cell with a left or right-moving individual if it is empty.
    Return True if successful, else False.

    Args:
        cell (Cell): Cell to populate.
        value (int): Decides if cell is populated by right-mover or left-mover.
    """
    assert isinstance(value, int) and value in [-1, 1]

    if not cell.is_empty():
        return False
    
    cell.populate(value)
    return True
def populate_corridor(self, N)

Randomly populate lattice with left-moving and right-moving agents.

Args

N : int
Number of cells to populate.
Expand source code
def populate_corridor(self, N):
    """
    Randomly populate lattice with left-moving and right-moving agents.

    Args:
        N (int): Number of cells to populate.
    """
    assert N <= self.cells.size, 'Number of people is larger than number of cells'
    
    for _ in range(N):
        value = 1 if random.random() < 0.5 else -1
        self.populate_random_cell(value)
def populate_random_cell(self, value)

Populate a random cell and return it.

Args

value : int
Decides if cell is populated by right-mover or left-mover.
Expand source code
def populate_random_cell(self, value):
    """
    Populate a random cell and return it.

    Args:
        value (int): Decides if cell is populated by right-mover or left-mover.
    """
    assert isinstance(value, int) and value in [-1, 1], 'value must be +/- 1 integer'

    cell = self.get_random_cell()
    while not self.populate_cell(cell, value):
        cell = self.get_random_cell()
    return cell