Source code for swarmsim.Controllers.spatial_inputs

from swarmsim.Environments import Environment
from swarmsim.Controllers import Controller
import numpy as np
import scipy.interpolate as sp
import matplotlib.pyplot as plt
from PIL import Image, ImageDraw

from swarmsim.Populations.population import Population
from swarmsim.Utils import gaussian_input



[docs] class GaussianRepulsion(Controller): """ Implements a spatially-varying radial repulsion force with Gaussian intensity profile. This controller creates a repulsion field centered at the origin with intensity that follows a 2D Gaussian distribution. Agents experience repulsive forces that push them away from the center, with force magnitude determined by the Gaussian profile and direction determined by their position relative to the origin. The controller uses spatial interpolation to efficiently compute force values at agent positions from a discretized Gaussian field defined on a regular grid. Parameters ---------- population : Population The population of agents to be controlled by this repulsion field. environment : Environment The environment providing spatial boundaries and dimensions. config_path : str, optional Path to the YAML configuration file containing controller parameters. Default is None. Attributes ---------- disc_pts : int Number of discretization points along each axis for the Gaussian field grid. interpolator : scipy.interpolate.RegularGridInterpolator Interpolator object for efficient evaluation of the Gaussian field at arbitrary positions. Config Requirements ------------------- The YAML configuration file should contain the following parameters: dt : float Sampling time of the controller. disc_pts : int, optional Number of points in the discretization grid for the Gaussian input field. Default is ``30``. Notes ----- The repulsion force is computed as: F(x, y) = G(x, y) * (x, y) / ||x, y|| where: - G(x, y) is the Gaussian intensity at position (x, y) - (x, y) / ||x, y|| is the unit vector pointing away from the origin - The result is a radial repulsion force with Gaussian-modulated strength The Gaussian field is pre-computed on a regular grid and interpolated for efficiency. The grid spans the environment dimensions with the specified number of discretization points. Examples -------- Example YAML configuration: .. code-block:: yaml GaussianRepulsion: dt: 0.1 disc_pts: 50 """ def __init__(self, population: Population, environment: Environment, config_path=None) -> None: """ Initialize the Gaussian repulsion controller. Parameters ---------- population : Population The population to be controlled by the repulsion field. environment : Environment The environment providing spatial dimensions. config_path : str, optional Path to the configuration file. Default is None. """ super().__init__(population, environment, config_path) x_dim = environment.dimensions[0] y_dim = environment.dimensions[1] self.disc_pts = self.config.get('disc_pts', 30) # Create a grid of points x = np.linspace(-x_dim, x_dim, self.disc_pts) # disc_pts points between the bounds of the arena for x-axis y = np.linspace(-y_dim, y_dim, self.disc_pts) # disc_pts points between the bounds of the arena for y-axis # Create the 2D grid of values X, Y = np.meshgrid(x, y) # Create a grid from x and y Z = gaussian_input(np.transpose(X), np.transpose(Y),sigma_x=20.0,sigma_y=10.0) # Apply the Gaussian function on the grid # Create the RegularGridInterpolator self.interpolator = sp.RegularGridInterpolator((x, y), np.transpose(Z), method='linear')
[docs] def get_action(self): """ Compute the Gaussian repulsion forces for all agents. This method evaluates the Gaussian intensity field at each agent's position and computes radial repulsion forces pointing away from the origin. The force magnitude is modulated by the Gaussian intensity profile. Returns ------- np.ndarray Repulsion forces of shape (N, 2) where N is the number of agents. Each row contains the [x, y] force components for the corresponding agent. Notes ----- The repulsion force computation involves: 1. **Intensity Evaluation**: Sample Gaussian field at agent positions using interpolation 2. **Distance Calculation**: Compute distance from each agent to the origin 3. **Direction Calculation**: Compute unit vectors pointing away from origin 4. **Force Combination**: Multiply intensity by direction to get final forces Agents at the exact origin (distance = 0) are assigned a small offset to avoid division by zero in the direction calculation. """ rep_strength = self.interpolator(self.population.x) #Strength of repulsion from the center (Gaussian) dist = np.maximum(0.0001,(np.linalg.norm(self.population.x, axis=1))) #Distances of the agents from the origin (Nx1) rep_dir = (self.population.x)/dist[:,np.newaxis] #Versor of the position of the agent (Nx2) return rep_strength[:,np.newaxis]*rep_dir #Repulsion strength (Nx2)
#Utility Function that defines a Gaussian distribution in a 2D Spce
[docs] class LightPattern(Controller): """ Projects a spatial light pattern loaded from an image file onto the environment. This controller reads an image file and uses it to create a spatial control field that agents can sense. The image is mapped to the environment coordinates, and the blue channel intensity is used as the control signal. This enables complex spatial patterns to be used for agent guidance and control. The controller is particularly useful for biological agent models where light intensity can influence agent behavior, such as phototaxis or photophobic responses. Parameters ---------- population : Population The population of agents that will sense the light pattern. environment : Environment The environment providing spatial boundaries and coordinate mapping. config_path : str, optional Path to the YAML configuration file containing controller parameters. Default is None. Attributes ---------- interpolator : scipy.interpolate.RegularGridInterpolator Interpolator object for evaluating light intensity at arbitrary positions. environment : Environment Reference to the environment for boundary checking. Config Requirements ------------------- The YAML configuration file must contain the following parameters: dt : float Sampling time of the controller. pattern_path : str Path (relative or absolute) to the image file containing the light pattern. Supported formats include JPEG, PNG, and other common image formats. Notes ----- The controller implementation: 1. **Image Loading**: Reads the specified image file using matplotlib 2. **Coordinate Mapping**: Maps image pixels to environment coordinates 3. **Channel Extraction**: Uses the blue channel (RGB[2]) as intensity values 4. **Normalization**: Normalizes pixel values to [0, 1] range 5. **Interpolation**: Creates interpolator for continuous intensity evaluation Agents outside the environment boundaries are clamped to the boundary values to ensure consistent behavior at the edges. Examples -------- Example YAML configuration: .. code-block:: yaml LightPattern: dt: 0.1 pattern_path: ../Configuration/Config_data/BCL.jpeg """ def __init__(self, population: Population, environment: Environment, config_path: str = None) -> None: """ Initialize the light pattern controller. Parameters ---------- population : Population The population that will sense the light pattern. environment : Environment The environment providing spatial coordinate mapping. config_path : str, optional Path to the configuration file. Default is None. Raises ------ FileNotFoundError If the specified pattern image file cannot be found. KeyError If the required pattern_path parameter is missing from configuration. """ super().__init__(population, environment, config_path) pattern_path = self.config.get("pattern_path","") camera_pattern = plt.imread(pattern_path) x_dim = environment.dimensions[0] y_dim = environment.dimensions[1] self.environment = environment # Create a grid of points x = np.linspace(-x_dim/2, x_dim/2, camera_pattern.shape[1]) y = np.linspace(-y_dim/2, y_dim/2, camera_pattern.shape[0]) Z = camera_pattern[:,:,2].T/255 # Create the RegularGridInterpolator self.interpolator = sp.RegularGridInterpolator((x, y), Z, method='linear')
[docs] def get_action(self): """ Get the light intensity values at the current agent positions. This method evaluates the light pattern at each agent's current position, applying boundary clamping to ensure agents outside the environment boundaries receive the edge intensity values. Returns ------- np.ndarray Light intensity values of shape (N, 1) where N is the number of agents. Values are normalized to the range [0, 1] based on the image data. Notes ----- The method performs the following steps: 1. **Boundary Clamping**: Clip agent positions to environment boundaries 2. **Interpolation**: Evaluate light pattern at clamped positions 3. **Reshaping**: Return as column vector for compatibility Agents outside the defined pattern area receive the boundary values. """ # All agents outside the bounds are considered at the bound limit_x = self.environment.dimensions[0]/2 * np.ones([self.population.x.shape[0],1]) limit_y = self.environment.dimensions[1]/2 * np.ones([self.population.x.shape[0],1]) limits_p = np.hstack((limit_x,limit_y)) limits_n = -limits_p state = np.concatenate((self.population.x[:,[0,1],np.newaxis],limits_p[:,:,np.newaxis]),axis=2) state = np.min(state,axis=2) state = np.concatenate((state[:,:,np.newaxis],limits_n[:,:,np.newaxis]),axis=2) state = np.max(state,axis=2) #Get the light intensity at the position of the agents light_ity = self.interpolator(state) return light_ity[:,np.newaxis]
[docs] def get_action_in_space(self,positions): """ Evaluate the light pattern at specified spatial positions. This method allows querying the light intensity at arbitrary positions in the environment, useful for analysis and visualization. Parameters ---------- positions : np.ndarray Array of shape (num_positions, 2) containing [x, y] coordinates where light intensity should be evaluated. Returns ------- np.ndarray Light intensity values of shape (num_positions, 1) at the specified positions. Values are normalized to the range [0, 1] based on the image data. Notes ----- This method provides direct access to the interpolated light pattern. Useful for: - Analyzing light patterns before simulation - Creating visualizations of the control field - Implementing predictive control strategies """ light_ity = self.interpolator(positions) return light_ity[:,np.newaxis]
[docs] class Temporal_pulses(Controller): """ Implements a temporally-varying control signal with periodic on/off pulses. This controller generates a uniform control signal that alternates between 'on' (value = 1) and 'off' (value = 0) states with a specified period. The signal is spatially uniform but varies in time, creating synchronized temporal stimulation across all agents. The controller is useful for studying temporal entrainment, circadian rhythm effects, or synchronized behavioral responses in agent populations. Parameters ---------- population : Population The population of agents to receive the temporal pulse signal. Period: float, optional The period of the temporal pulses (time for one complete on/off cycle). config_path : str, optional Path to the YAML configuration file containing controller parameters. Default is None. Attributes ---------- T : float Period of the temporal pulses (time for one complete on/off cycle). current_time : float Current simulation time, updated at each control action. Config Requirements ------------------- The YAML configuration file should contain the following parameters: dt : float Sampling time of the controller. Period : float, optional Period of the temporal pulses in simulation time units. Default is ``1.0``. Notes ----- The pulse pattern follows a square wave: - First half of period (0 ≤ t mod T < T/2): Signal = 1 (ON) - Second half of period (T/2 ≤ t mod T < T): Signal = 0 (OFF) The controller maintains its own internal time counter that is incremented by `dt` at each call to `get_action()`. Examples -------- Example YAML configuration: .. code-block:: yaml Temporal_pulses: dt: 0.01 Period: 2.0 This creates pulses with 2-second period (1 second ON, 1 second OFF). """ def __init__(self, population: Population, environment: Environment, config_path: str = None) -> None: """ Initialize the temporal pulse controller. Parameters ---------- population : Population The population to receive temporal pulse signals. environment : Environment The environment (required for interface compatibility). config_path : str, optional Path to the configuration file. Default is None. """ super().__init__(population, environment, config_path) self.T = self.config.get('Period', 1.0) # Period of the temporal pulses self.current_time = 0.0 # Initialize the current time
[docs] def get_action(self): """ Generate the current temporal pulse state for all agents. This method updates the internal time counter and returns the current pulse state (1 for ON, 0 for OFF) based on the temporal position within the pulse period. Returns ------- np.ndarray Pulse state array of shape (N, 1) where N is the number of agents. All agents receive the same pulse value: 1 during ON phase, 0 during OFF phase. Notes ----- The method performs the following: 1. **Time Update**: Increment internal time by dt 2. **Phase Calculation**: Compute position within current pulse period 3. **State Determination**: Return 1 if in first half of period, 0 otherwise The pulse timing is deterministic and synchronized across all agents, making it suitable for studying collective temporal responses. """ self.current_time += self.dt # Update the current time if np.mod(self.current_time,self.T) < (self.T/2): #print("t:"+ str(self.current_time) +"YES light") u = np.ones((self.population.x.shape[0], 1)) else: #print("t:"+ str(self.current_time) +"NO light") u = np.zeros((self.population.x.shape[0], 1)) return u
[docs] def get_action_in_space(self,positions): """ Returns the light intensity at the position specified in the positions vector Arguments --------- positions : numpy.Array(num_positions, num_dimensions) The positions where you want to retrieve the value of the control action """ print("t:"+ str(np.mod(self.current_time,self.T))) if np.mod(self.current_time,self.T) < (self.T/2): u = np.ones((len(positions), 1)) else: u = np.zeros((len(positions), 1)) return u
[docs] class AngularFeedback(Controller): """ Implements dynamic visual feedback using angular light patterns behind agents. This controller creates dynamic light patterns by drawing lines behind each agent based on their current position and a specified angular width. The patterns are updated periodically and projected as visual feedback that agents can sense. The controller generates wedge-shaped light patterns extending radially outward from each agent's position, creating a dynamic feedback system that can influence agent behavior through phototaxis or photophobic responses. Parameters ---------- population : Population The population of agents for which light patterns will be generated. environment : Environment The environment providing spatial boundaries and coordinate mapping. config_path : str, optional Path to the YAML configuration file containing controller parameters. Default is None. Attributes ---------- line_width : int Width of the lines drawn in the light pattern (in pixels). angle_width : float Angular width of the wedge pattern behind each agent (in radians). agent_distance : float Distance behind each agent where the wedge pattern starts. update_time : float Time interval between pattern updates (in simulation time units). last_update : float Timestamp of the last pattern update. current_time : float Current simulation time. x : np.ndarray Grid x-coordinates for spatial discretization. y : np.ndarray Grid y-coordinates for spatial discretization. interpolator : scipy.interpolate.RegularGridInterpolator Interpolator for evaluating light intensity at arbitrary positions. img_count : int Counter for saved debug images. Config Requirements ------------------- The YAML configuration file should contain the following parameters: dt : float Sampling time of the controller. line_width : int, optional Width of the drawn lines in pixels. Default is ``3``. angle_width : float, optional Angular width of the wedge pattern in radians. Default is ``0.5 * 45 * π/180`` (22.5 degrees). agent_distance : float, optional Distance behind agents where patterns start. Default is ``10``. update_time : float, optional Time interval between pattern updates. Default is ``20 * dt``. Notes ----- The controller algorithm: 1. **Time Management**: Track simulation time and update patterns periodically 2. **Pattern Generation**: Create wedge-shaped light patterns behind each agent 3. **Image Creation**: Use PIL to draw patterns on a digital canvas 4. **Interpolation Setup**: Create interpolator for smooth light evaluation 5. **Light Evaluation**: Return light intensity at current agent positions The patterns are dynamic and respond to agent movement, creating a feedback loop where agent behavior influences the light field they experience. Examples -------- Example YAML configuration: .. code-block:: yaml AngularFeedback: dt: 0.01 line_width: 5 angle_width: 0.785 # π/4 radians (45 degrees) agent_distance: 15 update_time: 0.5 Applications include: - Dynamic visual feedback systems - Agent-responsive light environments - Collective behavior with environmental coupling - Adaptive spatial guidance systems """ def __init__(self, population, environment, config_path=None) -> None: """ Initialize the angular feedback controller. Parameters ---------- population : Population The population for which angular light patterns will be generated. environment : Environment The environment providing spatial boundaries. config_path : str, optional Path to the configuration file. Default is None. """ super().__init__(population, environment, config_path) # Set the parameters of the controller self.line_width = self.config.get('line_width', 3) # Width of the lines drawn self.angle_width = self.config.get('angle_width', 0.5*45*np.pi/180) # Width of the angle in radians self.agent_distance = self.config.get('agent_distance', 2*5) # Distance behind self.update_time = self.config.get('update_time', 20*self.dt) # Change in update time (otherwise it depends on the simulation dt) self.last_update = -self.update_time # Initialize the last update time self.current_time = 0 # Update the current time # Initialize the controller with a black Image x_dim = environment.dimensions[0] y_dim = environment.dimensions[1] self.environment = environment # Create a grid of points self.x = np.linspace(-x_dim/2, x_dim/2, self.environment.dimensions[0]) self.y = np.linspace(-y_dim/2, y_dim/2, self.environment.dimensions[1]) img = Image.new('RGB', (self.environment.dimensions[0], self.environment.dimensions[1]), (0, 0, 0)) light_pattern = np.array(img) Z = light_pattern[:,:,2].T/255 self.interpolator = sp.RegularGridInterpolator((self.x, self.y), Z, method='linear') self.img_count = 0
[docs] def get_action(self): """ Generate and evaluate the current angular light pattern. This method updates the dynamic light pattern based on current agent positions and returns the light intensity values experienced by each agent. Returns ------- np.ndarray Light intensity values of shape (N, 1) where N is the number of agents. Values represent the blue channel intensity (0-1) at each agent's position. Notes ----- The method operates in two phases: 1. **Pattern Update** (if update_time has elapsed): - Calculate wedge patterns behind each agent - Draw lines using PIL imaging library - Update the spatial interpolator with new pattern 2. **Light Evaluation** (every call): - Clip agent positions to environment boundaries - Evaluate light intensity at current positions - Return intensity values The wedge patterns are generated by drawing two lines from a point behind each agent to positions that create the specified angular width. """ state = self.population.x[:,[0,1]] # Get the position of all the agents state = np.clip(state, [-self.environment.dimensions[0]/2, -self.environment.dimensions[1]/2], [self.environment.dimensions[0]/2, self.environment.dimensions[1]/2]) # Clip the state to the limits of the environment self.current_time += self.dt # Update the current time if (self.current_time - self.last_update >= self.update_time): self.last_update = self.current_time # Update the last update time phi = np.arctan2(state[:,1], state[:,0]) # Calculate angle with respect to origin r = np.linalg.norm(state, axis=1) # Calculate the distance from the origin img = Image.new('RGB', (self.environment.dimensions[0], self.environment.dimensions[1]), (0, 0, 0)) img_draw = ImageDraw.Draw(img) x_start = (r+self.agent_distance)*np.cos(phi) + self.environment.dimensions[0] / 2 y_start = (r+self.agent_distance)*np.sin(phi) + self.environment.dimensions[1] / 2 x_incr = self.agent_distance * np.tan(self.angle_width) * np.sin(phi) y_incr = - self.agent_distance * np.tan(self.angle_width) * np.cos(phi) x_end1 = state[:,0] + x_incr + self.environment.dimensions[0] / 2 y_end1 = state[:,1] + y_incr + self.environment.dimensions[1] / 2 x_end2 = state[:,0] - x_incr + self.environment.dimensions[0] / 2 y_end2 = state[:,1] - y_incr + self.environment.dimensions[1] / 2 #x_end1 = r * np.cos(phi + self.angle_width) + self.environment.dimensions[0] / 2 #y_end1 = r * np.sin(phi + self.angle_width) + self.environment.dimensions[1] / 2 #x_end2 = r * np.cos(phi - self.angle_width) + self.environment.dimensions[0] / 2 #y_end2 = r * np.sin(phi - self.angle_width) + self.environment.dimensions[1] / 2 for i in range(0,len(state[:,1])): img_draw.line([x_start[i], y_start[i], x_end1[i], y_end1[i]], fill=(0,0,255), width=self.line_width) # blue line for the center img_draw.line([x_start[i], y_start[i], x_end2[i], y_end2[i]], fill=(0,0,255), width=self.line_width) # blue line for the center self.img_count += 1 light_pattern = np.array(img) Z = light_pattern[:,:,2].T/255 # Extract the blue channel and transpose it self.interpolator = sp.RegularGridInterpolator((self.x, self.y), Z, method='linear') #img.save("logs/" + str(self.img_count) + ".png") # Save the image for debugging light_ity = self.interpolator(state) return light_ity[:,np.newaxis]
[docs] def get_action_in_space(self,positions): """ Evaluate the angular light pattern at specified spatial positions. This method allows querying the current light pattern at arbitrary positions in the environment, useful for analysis and visualization of the dynamic feedback patterns. Parameters ---------- positions : np.ndarray Array of shape (num_positions, 2) containing [x, y] coordinates where light intensity should be evaluated. Returns ------- np.ndarray Light intensity values of shape (num_positions, 1) at the specified positions. Values represent the blue channel intensity (0-1) from the current pattern. Notes ----- This method evaluates the most recently generated angular pattern at the specified positions. The pattern reflects the agent positions at the time of the last update (controlled by update_time parameter). Useful for: - Visualizing the dynamic light patterns - Analyzing spatial feedback structures - Creating movies or animations of pattern evolution """ light_ity = self.interpolator(positions) return light_ity[:,np.newaxis]