fixed changed bug and added multi/single processing differenciation
This commit is contained in:
parent
726cc6394f
commit
be5546c833
@ -30,4 +30,7 @@ TOTAL TIME: 0.1152s # dict instead of list for cells
|
||||
SIZE: 20.2575MB # process size 53 / 75,8
|
||||
|
||||
TOTAL TIME: 0.1135s # evolve is static
|
||||
SIZE: 20.2575MB # process size 50.8 / 76 (no more cell objects in processes)
|
||||
SIZE: 20.2575MB # process size 50.8 / 76 (no more cell objects in processes)
|
||||
|
||||
TOTAL TIME: 0.1126s # fixed changed state
|
||||
SIZE: 20.8678MB
|
@ -7,29 +7,22 @@ from cellular_automaton import *
|
||||
|
||||
class TestRule(Rule):
|
||||
@staticmethod
|
||||
def evolve_cell(last_cell_state, last_neighbour_states):
|
||||
def evolve_cell(last_cell_state, neighbours_last_states):
|
||||
try:
|
||||
return last_neighbour_states[0]
|
||||
return neighbours_last_states[0]
|
||||
except IndexError:
|
||||
print("damn neighbours")
|
||||
pass
|
||||
return False
|
||||
return last_cell_state
|
||||
|
||||
|
||||
class MyState(CellState):
|
||||
class MyState(SynchronousCellState):
|
||||
def __init__(self):
|
||||
rand = random.randrange(0, 101, 1)
|
||||
init = 0
|
||||
if rand > 99:
|
||||
init = 1
|
||||
|
||||
super().__init__((float(init),), draw_first_state=False)
|
||||
init = max(.0, float(rand - 99))
|
||||
super().__init__((init,), draw_first_state=init > 0)
|
||||
|
||||
def get_state_draw_color(self, iteration):
|
||||
red = 0
|
||||
if self.get_state_of_last_iteration(iteration)[0]:
|
||||
red = 255
|
||||
return [red, 0, 0]
|
||||
state1 = self.get_state_of_iteration(iteration)[0]
|
||||
return [255 if state1 else 0, 0, 0]
|
||||
|
||||
|
||||
def make_cellular_automaton(dimension, neighborhood, rule, state_class):
|
||||
@ -43,8 +36,8 @@ if __name__ == "__main__":
|
||||
random.seed(1000)
|
||||
# best single is 400/400 with 0,2 ca speed and 0,09 redraw / multi is 300/300 with 0.083
|
||||
neighborhood = MooreNeighborhood(EdgeRule.FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS)
|
||||
ca = make_cellular_automaton(dimension=[400, 400], neighborhood=neighborhood, rule=TestRule(), state_class=MyState)
|
||||
ca_processor = CellularAutomatonProcessor(process_count=1, cellular_automaton=ca)
|
||||
ca = make_cellular_automaton(dimension=[100, 100], neighborhood=neighborhood, rule=TestRule(), state_class=MyState)
|
||||
ca_processor = CellularAutomatonMultiProcessor(cellular_automaton=ca, process_count=4)
|
||||
|
||||
ca_window = PyGameFor2D(window_size=[1000, 800], cellular_automaton=ca)
|
||||
ca_window.main_loop(cellular_automaton_processor=ca_processor, ca_iterations_per_draw=1)
|
||||
|
Binary file not shown.
@ -44,22 +44,28 @@ class CellState:
|
||||
:param iteration: Uses the iteration index, to differ between concurrent states.
|
||||
:return True if state has changed.
|
||||
"""
|
||||
changed = self._change_state(new_state, iteration)
|
||||
self._change_state_values(new_state, iteration)
|
||||
changed = self._did_state_change(iteration)
|
||||
self._dirty |= changed
|
||||
self._active[self._calculate_slot(iteration)] = False
|
||||
|
||||
return changed
|
||||
|
||||
def _change_state(self, new_state, iteration):
|
||||
def _did_state_change(self, iteration):
|
||||
for a, b in zip(self._state_slots[self._calculate_slot(iteration)],
|
||||
self._state_slots[self._calculate_slot(iteration - 1)]):
|
||||
if a != b:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _change_state_values(self, new_state, iteration):
|
||||
current_state = self.get_state_of_iteration(iteration)
|
||||
changed = False
|
||||
if len(new_state) != len(current_state):
|
||||
raise IndexError("State length may not change!")
|
||||
|
||||
for i, ns in enumerate(new_state):
|
||||
try:
|
||||
if current_state[i] != ns:
|
||||
changed = True
|
||||
current_state[i] = ns
|
||||
except IndexError:
|
||||
raise IndexError("New State length or type is invalid!")
|
||||
return changed
|
||||
|
||||
def get_state_draw_color(self, iteration):
|
||||
raise NotImplementedError
|
||||
@ -70,6 +76,9 @@ class CellState:
|
||||
|
||||
|
||||
class SynchronousCellState(CellState):
|
||||
"""
|
||||
CellState version using shared values for multi processing purpose.
|
||||
"""
|
||||
def __init__(self, initial_state=(0., ), draw_first_state=True):
|
||||
super().__init__(initial_state, draw_first_state)
|
||||
self._state_slots = [RawArray(c_float, initial_state) for i in range(self.__class__._state_save_slot_count)]
|
||||
@ -87,7 +96,12 @@ class SynchronousCellState(CellState):
|
||||
self._dirty.value = False
|
||||
|
||||
def set_state_of_iteration(self, new_state, iteration):
|
||||
changed = self._change_state(new_state, iteration)
|
||||
self._change_state_values(new_state, iteration)
|
||||
changed = self._did_state_change(iteration)
|
||||
self._dirty.value |= changed
|
||||
self._active[self._calculate_slot(iteration)].value = False
|
||||
return changed
|
||||
|
||||
@classmethod
|
||||
def _calculate_slot(cls, iteration):
|
||||
return iteration % cls._state_save_slot_count
|
||||
|
@ -25,9 +25,17 @@ class DisplayFor2D:
|
||||
self._display_info = _DisplayInfo(grid_rect[-2:], grid_rect[:2], cell_size, screen)
|
||||
|
||||
def redraw_cellular_automaton(self):
|
||||
pygame.display.update(list(_cell_redraw_rectangles(self._cellular_automaton.cells,
|
||||
0,
|
||||
self._display_info)))
|
||||
update_rects = list(self._cell_redraw_rectangles())
|
||||
pygame.display.update(update_rects)
|
||||
|
||||
def _cell_redraw_rectangles(self):
|
||||
for cell in self._cellular_automaton.cells:
|
||||
if cell.state.is_set_for_redraw():
|
||||
cell_color = cell.state.get_state_draw_color(self._cellular_automaton.evolution_iteration_index)
|
||||
cell_pos = _calculate_cell_position(self._display_info.cell_size, cell)
|
||||
surface_pos = list(map(operator.add, cell_pos, self._display_info.grid_pos))
|
||||
yield self._display_info.screen.fill(cell_color, (surface_pos, self._display_info.cell_size))
|
||||
cell.state.was_redrawn()
|
||||
|
||||
def _calculate_cell_display_size(self, grid_size):
|
||||
grid_dimension = self._cellular_automaton.dimension
|
||||
@ -89,15 +97,5 @@ class PyGameFor2D:
|
||||
print("SIZE: " + "{0:.4f}".format(size / (1024 * 1024)) + "MB")
|
||||
|
||||
|
||||
def _cell_redraw_rectangles(cells, evolution_index, display_info):
|
||||
for cell in cells:
|
||||
if cell.state.is_set_for_redraw():
|
||||
cell_color = cell.state.get_state_draw_color(evolution_index)
|
||||
cell_pos = _calculate_cell_position(display_info.cell_size, cell)
|
||||
surface_pos = list(map(operator.add, cell_pos, display_info.grid_pos))
|
||||
yield display_info.screen.fill(cell_color, (surface_pos, display_info.cell_size))
|
||||
cell.state.was_redrawn()
|
||||
|
||||
|
||||
def _calculate_cell_position(cell_size, cell):
|
||||
return list(map(operator.mul, cell_size, cell.coordinate))
|
||||
|
@ -25,8 +25,7 @@ class CAFactory:
|
||||
@staticmethod
|
||||
def _apply_neighbourhood_to_cells(cells, neighborhood, dimension):
|
||||
for cell in cells.values():
|
||||
n_coordinates = neighborhood.calculate_cell_neighbor_coordinates(cell.coordinate,
|
||||
dimension)
|
||||
n_coordinates = neighborhood.calculate_cell_neighbor_coordinates(cell.coordinate, dimension)
|
||||
cell.neighbours = [cells[_join_coordinate(coordinate)].state for coordinate in n_coordinates]
|
||||
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
from enum import Enum
|
||||
from operator import add
|
||||
|
||||
|
||||
class EdgeRule(Enum):
|
||||
@ -8,12 +9,12 @@ class EdgeRule(Enum):
|
||||
|
||||
|
||||
class Neighborhood:
|
||||
def __init__(self, neighbors: list, edge_rule: EdgeRule):
|
||||
def __init__(self, neighbours_relative: list, edge_rule: EdgeRule):
|
||||
""" Defines a neighborhood for cells.
|
||||
:param neighbors: List of relative coordinates for the neighbors.
|
||||
:param edge_rule: A EdgeRule to define, how Cells on the edge of the grid will be handled.
|
||||
:param neighbours_relative: List of relative coordinates of cells neighbours.
|
||||
:param edge_rule: EdgeRule to define, how cells on the edge of the grid will be handled.
|
||||
"""
|
||||
self._neighbors = neighbors
|
||||
self._rel_neighbors = neighbours_relative
|
||||
self.edge_rule = edge_rule
|
||||
self.grid_dimensions = []
|
||||
|
||||
@ -24,56 +25,31 @@ class Neighborhood:
|
||||
:return:
|
||||
"""
|
||||
self.grid_dimensions = grid_dimensions
|
||||
if self._does_ignore_edge_cell_rule_apply(cell_coordinate):
|
||||
return []
|
||||
else:
|
||||
return self._apply_edge_rule_to_neighbours(cell_coordinate)
|
||||
return list(self._neighbours_generator(cell_coordinate))
|
||||
|
||||
def _neighbours_generator(self, cell_coordinate):
|
||||
if not self._does_ignore_edge_cell_rule_apply(cell_coordinate):
|
||||
for rel_n in self._rel_neighbors:
|
||||
yield from self._calculate_abs_neighbour_and_decide_validity(cell_coordinate, rel_n)
|
||||
|
||||
def _calculate_abs_neighbour_and_decide_validity(self, cell_coordinate, rel_n):
|
||||
n = list(map(add, rel_n, cell_coordinate))
|
||||
n_folded = self._apply_edge_overflow(n)
|
||||
if n == n_folded or self.edge_rule == EdgeRule.FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS:
|
||||
yield n_folded
|
||||
|
||||
def _does_ignore_edge_cell_rule_apply(self, coordinate):
|
||||
if self.edge_rule == EdgeRule.IGNORE_EDGE_CELLS and self._is_coordinate_on_an_edge(coordinate):
|
||||
return True
|
||||
return False
|
||||
return self.edge_rule == EdgeRule.IGNORE_EDGE_CELLS and self._is_coordinate_on_an_edge(coordinate)
|
||||
|
||||
def _is_coordinate_on_an_edge(self, coordinate):
|
||||
for neighbor_dimension, dimension in zip(coordinate, self.grid_dimensions):
|
||||
if neighbor_dimension == 0 or neighbor_dimension == dimension - 1:
|
||||
return True
|
||||
return False
|
||||
return all(0 == ci or ci == di-1 for ci, di in zip(coordinate, self.grid_dimensions))
|
||||
|
||||
def _apply_edge_rule_to_neighbours(self, coordinate):
|
||||
remaining_neighbours = []
|
||||
for neighbour in self._neighbors:
|
||||
if not self._does_ignore_edge_cell_neighbours_rule_apply(neighbour, coordinate):
|
||||
remaining_neighbours.append(self._calculate_neighbour_coordinate(neighbour, coordinate))
|
||||
return remaining_neighbours
|
||||
|
||||
def _does_ignore_edge_cell_neighbours_rule_apply(self, neighbour, coordinate):
|
||||
if self.edge_rule == EdgeRule.IGNORE_MISSING_NEIGHBORS_OF_EDGE_CELLS:
|
||||
for rel_neighbour_dim, cell_dim, dim in zip(neighbour, coordinate, self.grid_dimensions):
|
||||
neighbor_dimension = cell_dim + rel_neighbour_dim
|
||||
if neighbor_dimension < 0 or neighbor_dimension >= dim:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _calculate_neighbour_coordinate(self, neighbour, cell_coordinate):
|
||||
new_coordinate = []
|
||||
for rel_neighbour_dim, cell_dim, dim in zip(neighbour, cell_coordinate, self.grid_dimensions):
|
||||
neighbor_dim = cell_dim + rel_neighbour_dim
|
||||
neighbor_dim = self._calculate_neighbour_dimension_of_edge_cells(dim, neighbor_dim)
|
||||
new_coordinate.append(neighbor_dim)
|
||||
return new_coordinate
|
||||
|
||||
@staticmethod
|
||||
def _calculate_neighbour_dimension_of_edge_cells(dim, neighbor_dim):
|
||||
if neighbor_dim < 0:
|
||||
neighbor_dim = dim - 1
|
||||
elif neighbor_dim >= dim:
|
||||
neighbor_dim = 0
|
||||
return neighbor_dim
|
||||
def _apply_edge_overflow(self, n):
|
||||
return list(map(lambda ni, di: (ni + di) % di, n, self.grid_dimensions))
|
||||
|
||||
|
||||
class MooreNeighborhood(Neighborhood):
|
||||
def __init__(self, edge_rule: EdgeRule):
|
||||
def __init__(self, edge_rule: EdgeRule = EdgeRule.IGNORE_EDGE_CELLS):
|
||||
super().__init__([[-1, -1], [0, -1], [1, -1],
|
||||
[-1, 0], [1, 0],
|
||||
[-1, 1], [0, 1], [1, 1]],
|
||||
|
@ -7,10 +7,10 @@ class Rule:
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def evolve_cell(last_cell_state, last_neighbour_states):
|
||||
def evolve_cell(last_cell_state, neighbours_last_states):
|
||||
""" Calculates and sets new state of 'cell'.
|
||||
:param last_cell_state: The cells current state to calculate new state for.
|
||||
:param last_neighbour_states: The cells neighbours current states.
|
||||
:param neighbours_last_states: The cells neighbours current states.
|
||||
:return: True if state changed, False if not.
|
||||
A cells evolution will only be called if it or at least one of its neighbours has changed last iteration cycle.
|
||||
"""
|
||||
|
@ -2,6 +2,7 @@ import multiprocessing
|
||||
|
||||
from cellular_automaton.ca_rule import Rule
|
||||
from cellular_automaton.ca_cell import Cell
|
||||
from ctypes import c_int
|
||||
|
||||
|
||||
class CellularAutomaton:
|
||||
@ -9,36 +10,49 @@ class CellularAutomaton:
|
||||
self.cells = cells
|
||||
self.dimension = dimension
|
||||
self.evolution_rule = evolution_rule
|
||||
self.evolution_iteration_index = multiprocessing.RawValue('i', -1)
|
||||
self.evolution_iteration_index = -1
|
||||
|
||||
|
||||
class CellularAutomatonProcessor:
|
||||
def __init__(self, cellular_automaton, process_count: int = 1):
|
||||
self.ca = cellular_automaton
|
||||
cells = {i: (c.state, c.neighbours) for i, c in enumerate(self.ca.cells)}
|
||||
self.evolve_range = range(len(self.ca.cells))
|
||||
self._evolve_method = lambda x, y: None
|
||||
|
||||
if process_count > 1:
|
||||
self.pool = multiprocessing.Pool(processes=process_count,
|
||||
initializer=_init_process,
|
||||
initargs=(cells,
|
||||
self.ca.evolution_rule,
|
||||
self.ca.evolution_iteration_index))
|
||||
self._evolve_method = self.pool.map
|
||||
else:
|
||||
_init_process(cells, self.ca.evolution_rule, self.ca.evolution_iteration_index)
|
||||
self._evolve_method = lambda x, y: list(map(x, y))
|
||||
for cell in self.ca.cells:
|
||||
del cell.neighbours
|
||||
def __init__(self, cellular_automaton):
|
||||
self._ca = cellular_automaton
|
||||
|
||||
def evolve_x_times(self, x):
|
||||
for x in range(x):
|
||||
self.evolve()
|
||||
|
||||
def evolve(self):
|
||||
self.ca.evolution_iteration_index.value += 1
|
||||
self._evolve_method(_process_routine, self.evolve_range)
|
||||
self._ca.evolution_iteration_index += 1
|
||||
i = self._ca.evolution_iteration_index
|
||||
r = self._ca.evolution_rule.evolve_cell
|
||||
list(map(lambda c: Cell.evolve_if_ready((c.state, c.neighbours), r, i), self._ca.cells))
|
||||
# print(sum(1 for c in self._ca.cells if c.state.is_set_for_redraw()))
|
||||
|
||||
|
||||
class CellularAutomatonMultiProcessor(CellularAutomatonProcessor):
|
||||
def __init__(self, cellular_automaton, process_count: int = 2):
|
||||
if process_count < 1:
|
||||
raise ValueError
|
||||
super().__init__(cellular_automaton)
|
||||
self.ca = cellular_automaton
|
||||
cells = {i: (c.state, c.neighbours) for i, c in enumerate(self.ca.cells)}
|
||||
self.evolve_range = range(len(self.ca.cells))
|
||||
self.evolution_iteration_index = multiprocessing.RawValue(c_int, -1)
|
||||
|
||||
self.pool = multiprocessing.Pool(processes=process_count,
|
||||
initializer=_init_process,
|
||||
initargs=(cells,
|
||||
self.ca.evolution_rule,
|
||||
self.evolution_iteration_index))
|
||||
self._evolve_method = self.pool.map
|
||||
|
||||
for cell in self.ca.cells:
|
||||
del cell.neighbours
|
||||
|
||||
def evolve(self):
|
||||
self.ca.evolution_iteration_index += 1
|
||||
self.evolution_iteration_index.value = self.ca.evolution_iteration_index
|
||||
self.pool.map(_process_routine, self.evolve_range)
|
||||
|
||||
|
||||
global_cells = None
|
||||
|
@ -6,36 +6,51 @@ import unittest
|
||||
|
||||
|
||||
class TestCellState(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.cell_state = cs.SynchronousCellState(initial_state=(0,), draw_first_state=False)
|
||||
|
||||
def test_get_state_with_overflow(self):
|
||||
cell_state = cs.CellState(initial_state=(0,))
|
||||
cell_state.set_state_of_iteration(new_state=(1,), iteration=0)
|
||||
self.assertEqual(tuple(cell_state.get_state_of_iteration(2)), (1,))
|
||||
self.cell_state.set_state_of_iteration(new_state=(1,), iteration=0)
|
||||
self.assertEqual(tuple(self.cell_state.get_state_of_iteration(2)), (1,))
|
||||
|
||||
def test_set_state_with_overflow(self):
|
||||
cell_state = cs.CellState(initial_state=(0,))
|
||||
cell_state.set_state_of_iteration(new_state=(1,), iteration=2)
|
||||
self.assertEqual(tuple(cell_state.get_state_of_iteration(0)), (1,))
|
||||
self.cell_state.set_state_of_iteration(new_state=(1,), iteration=3)
|
||||
self.assertEqual(tuple(self.cell_state.get_state_of_iteration(1)), (1,))
|
||||
|
||||
def test_set_state_does_not_effect_all_slots(self):
|
||||
cell_state = cs.CellState(initial_state=(0,))
|
||||
cell_state.set_state_of_iteration(new_state=(1,), iteration=0)
|
||||
self.assertEqual(tuple(cell_state.get_state_of_iteration(1)), (0,))
|
||||
self.cell_state.set_state_of_iteration(new_state=(1,), iteration=0)
|
||||
self.assertEqual(tuple(self.cell_state.get_state_of_iteration(1)), (0,))
|
||||
|
||||
def test_redraw_state_on_change(self):
|
||||
cell_state = cs.CellState(initial_state=(0,), draw_first_state=False)
|
||||
cell_state.set_state_of_iteration(new_state=(1,), iteration=0)
|
||||
self.assertTrue(cell_state.is_set_for_redraw())
|
||||
self.cell_state.set_state_of_iteration(new_state=(1,), iteration=0)
|
||||
self.assertTrue(self.cell_state.is_set_for_redraw())
|
||||
|
||||
def test_redraw_state_on_nochange(self):
|
||||
cell_state = cs.CellState(initial_state=(0,), draw_first_state=False)
|
||||
cell_state.set_state_of_iteration(new_state=(0,), iteration=0)
|
||||
self.assertFalse(cell_state.is_set_for_redraw())
|
||||
self.cell_state.set_state_of_iteration(new_state=(0,), iteration=0)
|
||||
self.assertFalse(self.cell_state.is_set_for_redraw())
|
||||
|
||||
def test_active_state_after_set(self):
|
||||
cell_state = cs.CellState(initial_state=(0,), draw_first_state=False)
|
||||
cell_state.set_state_of_iteration(new_state=(1,), iteration=0)
|
||||
self.assertFalse(cell_state.is_active(1))
|
||||
self.cell_state.set_state_of_iteration(new_state=(1,), iteration=0)
|
||||
self.assertFalse(self.cell_state.is_active(0))
|
||||
self.assertFalse(self.cell_state.is_active(1))
|
||||
|
||||
def test_set_active_for_next_iteration(self):
|
||||
self.cell_state.set_state_of_iteration(new_state=(1,), iteration=0)
|
||||
self.cell_state.set_active_for_next_iteration(0)
|
||||
self.assertFalse(self.cell_state.is_active(0))
|
||||
self.assertTrue(self.cell_state.is_active(1))
|
||||
|
||||
def test_new_state_length(self):
|
||||
self.assertRaises(IndexError, self.__set_state_with_new_length)
|
||||
|
||||
def __set_state_with_new_length(self):
|
||||
return self.cell_state.set_state_of_iteration(new_state=(1, 1), iteration=0)
|
||||
|
||||
def test_redraw_flag(self):
|
||||
self.cell_state = cs.SynchronousCellState(initial_state=(0,), draw_first_state=True)
|
||||
self.assertTrue(self.cell_state.is_set_for_redraw())
|
||||
self.cell_state.was_redrawn()
|
||||
self.assertFalse(self.cell_state.is_set_for_redraw())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
@ -45,6 +45,23 @@ class TestCAFactory(unittest.TestCase):
|
||||
c = fac.make_cells([2, 2, 2], CellState)
|
||||
self.assertEqual(list(c.keys()), ['0-0-0', '0-0-1', '0-1-0', '0-1-1', '1-0-0', '1-0-1', '1-1-0', '1-1-1'])
|
||||
|
||||
def test_apply_neighbourhood(self):
|
||||
fac = TestFac()
|
||||
cells = fac.make_cells([3, 3], CellState)
|
||||
fac.apply_neighbourhood(cells, MooreNeighborhood(EdgeRule.IGNORE_EDGE_CELLS), [3, 3])
|
||||
|
||||
neighbours = self.__create_neighbour_list_of_cell('1-1', cells)
|
||||
|
||||
self.assertEqual(set(neighbours), set(cells['1-1'].neighbours))
|
||||
|
||||
@staticmethod
|
||||
def __create_neighbour_list_of_cell(cell_id, cells):
|
||||
neighbours = []
|
||||
for c in cells.values():
|
||||
if c != cells[cell_id]:
|
||||
neighbours.append(c.state)
|
||||
return neighbours
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
@ -11,7 +11,7 @@ class TestNeighborhood(unittest.TestCase):
|
||||
for neighborhood_set in neighborhood_sets:
|
||||
neighbors = neighborhood.calculate_cell_neighbor_coordinates(neighborhood_set[0], [3, 3])
|
||||
if neighborhood_set[1] != neighbors:
|
||||
print((neighborhood_set[1]), (neighbors))
|
||||
print("Error neighbours do not fit (expected, real): ", (neighborhood_set[1]), neighbors)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user