Configure behavior for objects#

Each object in the simulation can be assigned a behavior independently to simulate different scenarios. The behavior of the object can be configured by specifying the behavior parameters in the YAML configuration file.

IR-SIM supports two types of behavior configuration:

  • behavior: Controls individual object movement (e.g., dash, rvo, sfm)

  • group_behavior: Coordinates behavior for all objects in a group (e.g., orca)

Behavior Configuration Parameters#

Built-in individual behaviors are dash (move directly to the goal), rvo (reciprocal velocity obstacles for multi-agent collision avoidance), and sfm (Social Force Model, a reactive pedestrian-style avoidance method). By default, moving objects have no behavior and remain static unless an external command is supplied.

Behaviors are registered per kinematics, so not every (kinematics, behavior) pair exists:

Kinematics

Available behaviors

diff

dash, rvo, sfm

omni

dash, rvo, sfm

omni_angular

dash

acker

dash

If you assign a behavior that is not registered for the kinematics (for example rvo on acker), object construction will fail. Use a custom behavior to fill the gaps.

The example of RVO behavior is shown below:

import irsim

env = irsim.make()

for i in range(1000):
    env.step()
    env.render(0.05)

    if env.done():
        break

env.end()
world:
  height: 10  # the height of the world
  width: 10   # the width of the world

robot:
  - number: 10
    distribution: {name: 'circle', radius: 4.0, center: [5, 5]}  
    kinematics: {name: 'diff'}
    shape: 
      - {name: 'circle', radius: 0.2} 
    behavior: {name: 'rvo', vxmax: 1.5, vymax: 1.5, acce: 1.0, factor: 1.0}
    vel_min: [-3, -3.0]
    vel_max: [3, 3.0]
    color: 'cycle'   # one palette color per robot, in order
    arrive_mode: position
    goal_threshold: 0.15
    plot:
      show_trail: true
      show_goal: true
      trail_fill: true
      trail_alpha: 0.2
      show_trajectory: false

Important Behavior Parameters Explained#

Common Parameters:

  • name: Behavior type ('dash', 'rvo', 'sfm', or custom name)

  • wander: Random goal generation after reaching current goal (default: False)

  • loop: Loop through waypoints continuously when reaching the last goal (default: False)

  • target_roles: Filter objects for behavior ('all', 'robot', 'obstacle')

RVO-specific Parameters:

  • vxmax/vymax: Maximum velocities in x/y directions (default: 1.5)

  • acce: Maximum change of velocity per step (default: 1.0). Candidates are sampled within acce of the current velocity and clipped to vxmax/vymax; a velocity already beyond those limits brakes to the nearest admissible value

  • factor: Collision penalty weight (default: 1.0, higher = more conservative)

  • mode: Algorithm variant - 'rvo' (default), 'hrvo', or 'vo'

  • neighbor_threshold: Detection range for nearby objects (default: 3.0 meters)

SFM-specific Parameters (anisotropic Moussaid-Helbing 2009 variant):

  • vmax: Speed cap applied after force integration (default: 1.5)

  • neighbor_threshold: Social-interaction cutoff distance (default: 10.0)

  • relaxation_time: Goal-pull time constant tau (default: 0.5)

  • force_factor_desired: Weight on the goal-seeking force (default: 1.0)

  • force_factor_social: Weight on inter-agent repulsion (default: 2.1)

  • force_factor_obstacle: Weight on obstacle repulsion (default: 10.0)

  • sigma_obstacle: Exponential decay length of obstacle force (default: 0.8)

  • lambda_importance: Relative weight of velocity vs. position in the interaction direction (default: 2.0)

  • gamma: Sets the interaction range B = gamma * ||t|| (default: 0.35)

  • n_angular/n_velocity: Angular sharpness exponents for the sideways and slowdown components (defaults: 2.0, 3.0)

Dash-specific Parameters:

  • angle_tolerance: Orientation alignment tolerance for diff/acker (default: 0.1 radians)

Full list of behavior parameters can be found in the YAML Configuration.

SFM (Social Force Model)#

sfm is a reactive avoidance behavior that treats each agent as a Newtonian particle under three forces: a goal-pull, an anisotropic neighbor repulsion (people on the agent’s motion path push back more than people behind), and an exponential obstacle repulsion. The implementation uses the anisotropic variant from Moussaid, Helbing et al. (2009).

sfm complements rvo/orca: RVO and ORCA are geometric and provably collision-free under their assumptions, while SFM is behavioral and produces more human-looking trajectories, including occasional brush-bys, hesitations, and the asymmetric reaction to neighbors ahead vs. behind. Use SFM when crowd realism matters; use RVO/ORCA when collision-freeness is the priority.

The example below is usage/23sfm_world/sfm_world.yaml: twenty-four pedestrians crossing a +-shaped intersection of two 6.6 m corridors. Each lane carries a head-on pair (W↔E in blue/red, S↔N in green/orange), and the L-shaped wall corners enter SFM as line obstacles so agents bend around them instead of cutting the corner.

Note

Place the YAML file in the same directory as the main Python script.

import irsim

env = irsim.make()

while not env.done():
    env.step()
    env.render(0.01)

env.end()
world:
  height: 20
  width: 20
  step_time: 0.1
  offset: [-10, -10]
  collision_mode: 'unobstructed'   # SFM is reactive; allow brief overlap
  control_mode: 'auto'

robot:
  - number: 24
    distribution: {name: 'manual'}
    kinematics: {name: 'diff'}
    shape: [{name: 'circle', radius: 0.25}]
    # 12 horizontal head-on pairs + 12 vertical head-on pairs, interleaved
    # so adjacent indices share a lane (ir-sim steps objects sequentially,
    # so the pair coupling depends on adjacency in env.objects).
    state:
      - [-9, -2.6, 0]
      - [ 9, -2.601, 3.14]
      - [-9, -1.6, 0]
      - [ 9, -1.601, 3.14]
      # ... (six horizontal lanes + six vertical lanes, see usage file)
    goal:
      - [ 9, -2.6, 0]
      - [-9, -2.601, 3.14]
      - [ 9, -1.6, 0]
      - [-9, -1.601, 3.14]
      # ...
    color: 'cycle'   # one palette color per pedestrian, in order
    behavior:
      name: 'sfm'
      loop: true
      vmax: 0.7
      neighbor_threshold: 5.0
      force_factor_social: 6.0
      force_factor_obstacle: 8.0       # walls must hold during loop-reset
      lambda_importance: 1.0           # cleaner repulsion along line of centres
      gamma: 0.5
      sigma_obstacle: 0.6              # wall felt across most of the 6.6 m corridor
      n_angular: 1.5                   # agents commit to a side earlier
      n_velocity: 2.5
      safety_radius: 0.12              # 0.24 m personal bubble
    vel_min: [-1.5, -3.0]
    vel_max: [ 1.5,  3.0]
    arrive_mode: position
    goal_threshold: 1.0                # loop-reset fires before the wall stalls the goal approach
    plot: {show_trail: true, show_goal: true, trail_fill: true, trail_alpha: 0.2, keep_trail_length: 25}

obstacle:
  - number: 4
    distribution: {name: 'manual'}
    state: [0, 0, 0]
    unobstructed: true
    shape:
      - {name: 'linestring', vertices: [[-10,  3.3], [-3.3,  3.3], [-3.3,  10]]}
      - {name: 'linestring', vertices: [[ 10,  3.3], [ 3.3,  3.3], [ 3.3,  10]]}
      - {name: 'linestring', vertices: [[-10, -3.3], [-3.3, -3.3], [-3.3, -10]]}
      - {name: 'linestring', vertices: [[ 10, -3.3], [ 3.3, -3.3], [ 3.3, -10]]}

See usage/23sfm_world/ for the full runnable file (24-agent cross corridor plus a separate two-robot head-on pass). For crowds, sfm is also available as a group behavior that steps all members at once and can model pedestrians walking together.

RVO with Line Obstacles#

rvo automatically avoids linestring-shaped obstacles in addition to circular agents. Each line segment between consecutive vertices of a linestring object is treated as a static velocity obstacle, so RVO agents can navigate around walls or polylines without extra configuration:

robot:
  - number: 6
    kinematics: {name: 'diff'}
    shape: {name: 'circle', radius: 0.2}
    distribution: {name: 'circle', radius: 4.0, center: [5, 5]}
    behavior: {name: 'rvo'}

obstacle:
  - shape: {name: 'linestring', vertices: [[2, 2], [2, 8], [8, 8]]}

This works for both omni and diff kinematics; non-linestring obstacles continue to participate as agent neighbors. A neighbor is a disc carrying its current velocity, zero when it is stopped, and the cones are the same whether the robot itself is moving or at rest.

Group Behavior#

While behavior controls individual object movement, group_behavior enables coordinated behavior for all objects within the same group. Group behavior computes actions for all members in a single step, making it more efficient than individual behaviors for coordinated scenarios.

Behavior vs Group Behavior#

Feature

behavior

group_behavior

Scope

Individual object

All objects in a group

Computation

Per-object each step

All members at once

Use case

Simple navigation

Coordinated multi-agent

Available

dash, rvo, sfm (kinematics-dependent: see matrix above)

orca, sfm (omni, diff)

ORCA (Optimal Reciprocal Collision Avoidance)#

ORCA is a classical built-in group-level collision avoidance algorithm that computes optimal velocities for multiple agents simultaneously. It ensures smooth, collision-free navigation even with hundreds of agents.

ORCA supports both omni and diff kinematics. It plans a holonomic velocity (vx, vy) for every member: omni robots use it directly, while diff robots map it to a (linear, angular) command, so a differential-drive robot turns toward the planned direction and slows down when it is not yet aligned. Set kinematics: {name: 'diff'} on the group to use the differential-drive variant; all ORCA parameters below stay the same. Each member’s preferred velocity points at its goal at the agent’s speed cap: the fastest speed the member can hold in every direction (the smaller component of vel_max for omni, the linear limit for diff), lowered to maxSpeed when that is set, so members cruise at full speed when nothing is in the way.

Note

ORCA requires the pyrvo library, which is a python binding for the ORCA C++ algorithm. Install it using:

pip install pyrvo

Basic ORCA Configuration#

import irsim

env = irsim.make()

while True:
    env.step()
    env.render()

    if env.done():
        break

env.end()
world:
  height: 290
  width: 290
  step_time: 0.1
  sample_time: 0.7
  offset: [-140, -140]

robot:
  - number: 200
    color: 'cycle'   # one palette color per robot, in order
    kinematics: {name: 'omni'}
    shape: {name: 'circle', radius: 1.0}
    distribution: {name: 'circle', radius: 140}
    goal_threshold: 0.1
    vel_max: [2, 2]
    vel_min: [-2, -2]
    plot:
      show_trajectory: True
      show_goal: True
      keep_traj_length: 50
    group_behavior:
      name: 'orca'
      neighborDist: 15.0
      maxNeighbors: 10
      timeHorizon: 20.0
      timeHorizonObst: 10.0
      safe_radius: 0.5
      maxSpeed: 2.0

ORCA Parameters Explained#

Parameter

Type

Default

Description

name

str

-

Must be 'orca'

neighborDist

float

15.0

Maximum distance to consider other agents as neighbors

maxNeighbors

int

10

Maximum number of neighbors to consider for collision avoidance

timeHorizon

float

20.0

Time horizon for agent-agent collision avoidance (seconds)

timeHorizonObst

float

10.0

Time horizon for agent-obstacle collision avoidance (seconds)

safe_radius

float

0.1

Additional safety margin added to agent radius

maxSpeed

float

None

Speed cap of each agent and the speed it heads for its goal at; never above the fastest speed the robot can hold in every direction (smaller vel_max component for omni, linear limit for diff), which is the default

wander

bool

False

Generate random goals when current goal is reached

loop

bool

False

Loop through waypoints continuously when reaching the last goal

range_low

list

-

Lower bounds for random goal generation [x, y, theta]

range_high

list

-

Upper bounds for random goal generation [x, y, theta]

ORCA with Random Wandering#

For continuous simulation where agents keep moving after reaching their goals:

import irsim

env = irsim.make()

for i in range(1000):
    env.step()
    env.render()

    if env.done():
        break

env.end()
world:
  height: 290
  width: 290
  step_time: 0.1
  sample_time: 0.7
  offset: [-140, -140]

robot:
  - number: 100
    color: 'cycle'   # one palette color per robot, in order
    kinematics: {name: 'omni'}
    shape: {name: 'circle', radius: 1.0}
    distribution: {name: 'random', range_low: [-100, -100, -3.14], range_high: [100, 100, 3.14]}
    goal_threshold: 0.1
    vel_max: [3, 3]
    vel_min: [-3, -3]
    plot:
      show_trajectory: True
      show_goal: True
      keep_traj_length: 50
    group_behavior:
      name: 'orca'
      neighborDist: 15.0
      maxNeighbors: 10
      timeHorizon: 20.0
      timeHorizonObst: 10.0
      safe_radius: 0.5
      maxSpeed: 2.0
      wander: True
      range_low: [-100, -100, -3.14]
      range_high: [100, 100, 3.14]

Waypoint Loop Navigation#

For patrol scenarios where robots continuously navigate through predefined waypoints:

robot:
  kinematics: {name: diff}
  goal: [[1, 1], [5, 1], [5, 5], [1, 5]]
  behavior:
    name: dash
    loop: true

When loop: true, the robot will navigate through all waypoints and restart from the first waypoint upon reaching the last one.

Tip

Use group_behavior in YAML for most cases as it handles initialization automatically. For full control, such as custom collision logic or an external planner, drive each robot yourself by passing velocity commands to env.step(action) instead of setting a behavior.

SFM as a Group Behavior#

sfm is also available as a group behavior for omni and diff kinematics. It uses the same physics and parameter names as the per-object sfm behavior, evaluated for every member at once by SocialForceModelBatch:

  • Synchronous update. All members read one state snapshot, so the crowd no longer depends on the order in which objects are listed in the YAML. A single member follows exactly the same trajectory as under behavior: sfm.

  • Vectorized. The pairwise social force is one NumPy pass over all member pairs instead of a Python loop per member, so large crowds stay cheap.

  • Social groups. social_groups optionally partitions the members into pedestrians walking together, which adds the three group forces of Moussaid et al. (2010): a coherence pull toward the group’s centre of mass, an intra-group repulsion once two members’ discs touch, and a gaze brake that slows a member down when the rest of the group leaves its field of view.

Objects outside the group still take part: linestring obstacles enter as walls, and every other object as a circular neighbour that pushes members but is not moved by them. target_roles, loop and wander work as for the per-object behavior. A member without a goal stands still but still yields to neighbours.

The batched evaluation and the social-group forces follow PySocialForce, a NumPy implementation of the extended social force model, using the force definitions of pedsim_ros.

The example below is usage/23sfm_world/sfm_group_world.yaml: two pairs and two triples walk a corridor in opposite directions, overlapping by one lane, while two singles thread through the middle. Each group slides sideways as a unit to pass the oncoming one.

import irsim

env = irsim.make()

while not env.done():
    env.step()
    env.render(0.01)

env.end()
world:
  height: 10
  width: 20
  step_time: 0.1
  sample_time: 0.1
  offset: [-10, -5]
  collision_mode: 'unobstructed'   # SFM is reactive; let pedestrians overlap briefly
  control_mode: 'auto'

robot:
  - number: 12
    distribution: {name: 'manual'}
    kinematics: {name: 'diff'}
    shape: [{name: 'circle', radius: 0.25}]
    state:
      - [-9,  1.5, 0]
      - [-9,  2.2, 0]
      - [-9, -1.5, 0]
      - [-9, -2.2, 0]
      - [-9, -2.9, 0]
      - [ 9, -2.4, 3.14]
      - [ 9, -3.1, 3.14]
      - [ 9,  2.4, 3.14]
      - [ 9,  3.1, 3.14]
      - [ 9,  3.8, 3.14]
      - [-9,  0.3, 0]
      - [ 9, -0.3, 3.14]
    goal:
      - [ 9,  1.5, 0]
      - [ 9,  2.2, 0]
      - [ 9, -1.5, 0]
      - [ 9, -2.2, 0]
      - [ 9, -2.9, 0]
      - [-9, -2.4, 3.14]
      - [-9, -3.1, 3.14]
      - [-9,  2.4, 3.14]
      - [-9,  3.1, 3.14]
      - [-9,  3.8, 3.14]
      - [ 9,  0.3, 0]
      - [-9, -0.3, 3.14]
    color: 'cycle'   # one palette color per pedestrian, in order
    group_behavior:
      name: 'sfm'
      vmax: 1.0
      neighbor_threshold: 5.0
      force_factor_social: 4.0         # head-on stiffness for a 12-agent corridor
      force_factor_obstacle: 5.0
      sigma_obstacle: 0.3              # walls only matter within ~1 m
      gamma: 0.5
      safety_radius: 0.15              # 0.3 m personal bubble for vmax=1.0
      social_groups: [[0, 1], [2, 3, 4], [5, 6], [7, 8, 9]]
      force_factor_coherence: 2.0      # pull toward the group centre
      force_factor_group_repulsion: 1.0
      group_repulsion_threshold: 0.7   # members push apart once their discs touch
      force_factor_gaze: 3.0           # leader brakes when the group leaves its view
      gaze_vision_angle: 90.0
    vel_min: [-1.5, -3.0]
    vel_max: [ 1.5,  3.0]
    arrive_mode: position
    goal_threshold: 0.5
    plot:
      show_trail: true
      show_goal: true
      trail_fill: true
      trail_alpha: 0.2
      keep_trail_length: 25


obstacle:
  - shape: {name: 'linestring', vertices: [[-10,  5], [10,  5]]}
    state: [0, 0, 0]
    unobstructed: true
  - shape: {name: 'linestring', vertices: [[-10, -5], [10, -5]]}
    state: [0, 0, 0]
    unobstructed: true

Group SFM Parameters#

All per-object SFM parameters (vmax, neighbor_threshold, relaxation_time, force_factor_desired, force_factor_social, force_factor_obstacle, sigma_obstacle, lambda_importance, gamma, n_angular, n_velocity, safety_radius) keep their names and defaults. The group version adds:

Parameter

Type

Default

Description

name

str

-

Must be 'sfm'

social_groups

list of list of int

None

Member indices, in YAML group order, of pedestrians walking together. Each member may appear in at most one group; omit to disable the group forces

force_factor_coherence

float

2.0

Weight of the pull toward the group’s centre of mass. It ramps in smoothly once a member is further than (size - 1) / 2 m from the centre

force_factor_group_repulsion

float

1.0

Weight of the push between two group members that come too close

group_repulsion_threshold

float

None

Centre-to-centre distance below which two members repel each other. None uses the sum of their radii, so members push apart as soon as their discs touch

force_factor_gaze

float

3.0

Weight of the brake -alpha * v applied when the other members’ centre of mass leaves the field of view; alpha is the missing head rotation in radians

gaze_vision_angle

float

90.0

Half-angle, in degrees, of the field of view around the walking direction

target_roles

str

'all'

Which outside objects members react to: 'all', 'robot' or 'obstacle'

wander

bool

False

Generate random goals when the current goal is reached

loop

bool

False

Loop through waypoints continuously when reaching the last goal

Custom Group Behavior#

You can create custom group behaviors for coordinated multi-agent control. IR-SIM provides two registration patterns:

  • @register_group_behavior: Function-based (called every step)

  • @register_group_behavior_class: Class-based (initialized once, then called every step)

Function-based vs Class-based Behaviors#

Aspect

Function-based

Class-based

Decorator

@register_group_behavior

@register_group_behavior_class

Initialization

None (stateless)

Once at start

State

No persistent state

Can maintain state between steps

Use case

Simple calculations

Complex algorithms with initialization

Example

Simple formation control

ORCA (requires simulator setup)

Custom Group Behavior Example (Class-based)#

Here’s an example of a custom group behavior that implements simple formation control:

# custom_group_methods.py
from typing import Any
import numpy as np
from irsim.lib.behavior.behavior_registry import register_group_behavior_class
from irsim.world.object_base import ObjectBase


@register_group_behavior_class("omni", "formation")
def init_formation_behavior(members: list[ObjectBase], **kwargs: Any):
    """Registered initializer returning a class-based handler."""
    return FormationGroupBehavior(members, **kwargs)


class FormationGroupBehavior:
    """
    Class-based formation group behavior.
    Maintains formation shape while moving toward goals.
    """

    def __init__(
        self,
        members: list[ObjectBase],
        formation_gain: float = 1.0,
        goal_gain: float = 0.5,
        **kwargs: Any,
    ) -> None:
        """
        Initialize formation behavior.

        Args:
            members: List of group members
            formation_gain: Weight for formation keeping
            goal_gain: Weight for goal reaching
        """
        self._formation_gain = formation_gain
        self._goal_gain = goal_gain

        # Calculate initial formation offsets (relative positions)
        if members:
            centroid = np.mean([m.state[:2, 0] for m in members], axis=0)
            self._offsets = [m.state[:2, 0] - centroid for m in members]
        else:
            self._offsets = []

    def __call__(self, members: list[ObjectBase], **kwargs: Any) -> list[np.ndarray]:
        """
        Generate velocities for all members to maintain formation.

        Args:
            members: Current group members
            **kwargs: Additional parameters

        Returns:
            List of velocity arrays for each member
        """
        if not members:
            return []

        velocities = []

        # Calculate current centroid
        centroid = np.mean([m.state[:2, 0] for m in members], axis=0)

        # Calculate target centroid (average of all goals)
        goals = [m.goal[:2, 0] for m in members if m.goal is not None]
        if goals:
            target_centroid = np.mean(goals, axis=0)
        else:
            target_centroid = centroid

        for i, member in enumerate(members):
            # Desired position in formation
            desired_pos = (
                centroid + self._offsets[i]
                if i < len(self._offsets)
                else member.state[:2, 0]
            )

            # Formation keeping velocity
            formation_vel = self._formation_gain * (desired_pos - member.state[:2, 0])

            # Goal reaching velocity
            goal_vel = self._goal_gain * (target_centroid - centroid)

            # Combined velocity
            vel = formation_vel + goal_vel

            # Clip to max speed
            speed = np.linalg.norm(vel)
            if speed > member.max_speed:
                vel = vel / speed * member.max_speed

            velocities.append(np.c_[vel])

        return velocities

Using Custom Group Behavior#

import irsim

env = irsim.make()
env.load_behavior("custom_group_methods")

for i in range(500):
    env.step()
    env.render()

    if env.done():
        break

env.end()
world:
  height: 20
  width: 20

robot:
  - number: 5
    color: 'cycle'   # one palette color per robot, in order
    kinematics: {name: 'omni'}
    shape: {name: 'circle', radius: 0.3}
    distribution: {name: 'manual'}
    state: [[10, 13, 0], [7.15, 10.93, 0], [8.24, 7.57, 0], [11.76, 7.57, 0], [12.85, 10.93, 0]]   # a ring of radius 3 around (10, 10)
    goal: [15, 15, 0]   # shared goal: the formation moves its centroid there
    vel_max: [2, 2]
    vel_min: [-2, -2]
    group_behavior:
      name: 'formation'
      formation_gain: 1.0
      goal_gain: 0.5

Function-based Group Behavior Example#

For simpler behaviors without initialization requirements:

# custom_group_methods.py
from typing import Any
import numpy as np
from irsim.lib.behavior.behavior_registry import register_group_behavior
from irsim.world.object_base import ObjectBase


@register_group_behavior("omni", "swarm")
def beh_omni_swarm(members: list[ObjectBase], **kwargs: Any) -> list[np.ndarray]:
    """
    Simple swarm behavior - all members move toward group centroid.

    Args:
        members: List of group members
        **kwargs: Additional parameters (cohesion_gain, goal_gain)

    Returns:
        List of velocity arrays for each member
    """
    if not members:
        return []

    cohesion_gain = kwargs.get("cohesion_gain", 0.5)
    goal_gain = kwargs.get("goal_gain", 1.0)

    # Calculate centroid
    centroid = np.mean([m.state[:2, 0] for m in members], axis=0)

    velocities = []
    for member in members:
        # Cohesion: move toward centroid
        cohesion_vel = cohesion_gain * (centroid - member.state[:2, 0])

        # Goal: move toward individual goal
        if member.goal is not None:
            goal_vel = goal_gain * (member.goal[:2, 0] - member.state[:2, 0])
        else:
            goal_vel = np.zeros(2)

        vel = cohesion_vel + goal_vel

        # Clip to max speed
        speed = np.linalg.norm(vel)
        if speed > member.max_speed:
            vel = vel / speed * member.max_speed

        velocities.append(np.c_[vel])

    return velocities

Important

  • Use @register_group_behavior_class when your behavior needs initialization (e.g., setting up external libraries, computing initial states)

  • Use @register_group_behavior for simple stateless behaviors

  • The first argument is the kinematics type ('omni', 'omni_angular', 'diff', 'acker')

  • The second argument is the behavior name used in YAML group_behavior: {name: 'your_name'}

Advanced Configuration for Custom Behavior#

Custom Behavior Function#

If you want to create a custom behavior for the object, you should first define your own behavior in a custom python script (e.g. custom_behavior_methods.py) as shown below:

from irsim.lib import register_behavior
from irsim.util.util import relative_position, WrapToPi
import numpy as np


@register_behavior("diff", "dash_custom")
def beh_diff_dash(ego_object, objects, **kwargs):

    state = ego_object.state
    goal = ego_object.goal
    goal_threshold = ego_object.goal_threshold
    _, max_vel = ego_object.get_vel_range()
    angle_tolerance = kwargs.get("angle_tolerance", 0.1)

    behavior_vel = DiffDash2(state, goal, max_vel, goal_threshold, angle_tolerance)

    return behavior_vel


def DiffDash2(state, goal, max_vel, goal_threshold=0.1, angle_tolerance=0.2):

    distance, radian = relative_position(state, goal)

    if distance < goal_threshold:
        return np.zeros((2, 1))

    diff_radian = WrapToPi(radian - state[2, 0])
    linear = max_vel[0, 0] * np.cos(diff_radian)

    if abs(diff_radian) < angle_tolerance:
        angular = 0
    else:
        angular = max_vel[1, 0] * np.sign(diff_radian)

    return np.array([[linear], [angular]])

The ego_object is the object that you want to control, and the objects is the list of all objects in the simulation. The custom behavior function should return the velocity of the object. You can obtain the state, goal, and other properties from the objects. DiffDash2 is the custom behavior function that calculates the dash velocity of the object.

Important

You must use the @register_behavior decorator to register the custom behavior. The first argument of the decorator is the name of the kinematics (diff, acker, omni, omni_angular), and the second argument is the name of the behavior used in YAML file.

Register Behavior#

The python script and YAML to run the simulation with the custom behavior is shown below:

import irsim

env = irsim.make()
env.load_behavior("custom_behavior_methods")

for i in range(1000):
    env.step()
    env.render(0.01)

    if env.done():
        break

env.end(5)

load_behavior function loads the custom behavior methods from the python script. custom_behavior_methods is the name of the python script that contains the custom behavior function.

Note

Please place the script with the name custom_behavior_methods in the same directory as the main python script.

The final two robots below use the custom dash_custom behavior alongside built-in RVO and dash robots for comparison.

world:
  height: 10  # the height of the world
  width: 10   # the width of the world

robot:
  - number: 3
    distribution: {name: 'circle', radius: 4.0, center: [5, 5]}
    kinematics: {name: 'diff'}
    shape: [{name: 'circle', radius: 0.2}]
    behavior: {name: 'rvo', vxmax: 1.0, vymax: 1.0, acce: 1.0, factor: 1.0}
    vel_min: [-3, -3.0]
    vel_max: [3, 3.0]
    color: ['#0072B2', '#D55E00', '#009E73']
    arrive_mode: position
    goal_threshold: 0.2
    plot:
      show_trail: true
      show_goal: true
      trail_fill: true
      trail_alpha: 0.2
      show_trajectory: false

  - number: 2
    distribution: {name: 'circle', radius: 4.0, center: [5, 5]}
    kinematics: {name: 'diff'}
    shape: [{name: 'circle', radius: 0.2}]
    behavior: {name: 'dash', vxmax: 1.5, vymax: 1.5, acce: 1.0, factor: 1.0}
    vel_min: [-3, -3.0]
    vel_max: [3, 3.0]
    color: ['#E69F00', '#CC79A7']
    arrive_mode: position
    goal_threshold: 0.2
    plot:
      show_trail: true
      show_goal: true
      trail_fill: true
      trail_alpha: 0.2
      show_trajectory: false

  - number: 2
    distribution: {name: 'random'}
    kinematics: {name: 'diff'}
    shape: [{name: 'circle', radius: 0.2}]
    behavior: {name: 'dash_custom'}
    vel_min: [-3, -3.0]
    vel_max: [3, 3.0]
    color: ['#56B4E9', '#117733']
    arrive_mode: position
    goal_threshold: 0.2
    plot:
      show_trail: true
      show_goal: true
      trail_fill: true
      trail_alpha: 0.2
      show_trajectory: false

Custom Behavior Class (Stateful)#

For behaviors that require initialization or need to maintain state between steps, use the @register_behavior_class decorator. This is useful when:

  • Your behavior needs one-time setup (e.g., loading models, initializing planners)

  • You need to track history or state across simulation steps

  • You want to cache computations for efficiency

Function-based vs Class-based Individual Behaviors#

Aspect

@register_behavior

@register_behavior_class

Execution

Called every step

__init__ once, __call__ every step

State

Stateless

Can maintain internal state

Setup

None

One-time initialization in __init__

Use case

Simple reactive behaviors

Complex planners, learning-based control

Class-based Behavior Example#

# custom_behavior_methods.py
from typing import Any
import numpy as np
from irsim.lib.behavior.behavior_registry import register_behavior_class


@register_behavior_class("diff", "smooth_dash")
def init_smooth_dash(object_info, **kwargs):
    """Registered initializer returning a class-based handler."""
    return SmoothDashBehavior(object_info, **kwargs)


class SmoothDashBehavior:
    """
    Class-based smooth dash behavior with velocity smoothing.
    Maintains previous velocity for smooth acceleration.
    """

    def __init__(self, object_info, smoothing: float = 0.3, **kwargs):
        """
        Initialize smooth dash behavior.

        Args:
            object_info: Object information from ObjectBase
            smoothing: Smoothing factor (0-1), higher = smoother
        """
        self._smoothing = smoothing
        self._prev_vel = np.zeros((2, 1))
        self._object_info = object_info

    def __call__(
        self,
        ego_object,
        external_objects: list,
        **kwargs: Any,
    ) -> np.ndarray:
        """
        Generate smoothed velocity toward goal.

        Args:
            ego_object: The controlled object
            external_objects: Other objects in environment
            **kwargs: Additional parameters

        Returns:
            Smoothed velocity array (2x1)
        """
        if ego_object.goal is None:
            return np.zeros((2, 1))

        # Calculate desired velocity (simple dash)
        state = ego_object.state
        goal = ego_object.goal
        _, max_vel = ego_object.get_vel_range()

        diff = goal[:2, 0] - state[:2, 0]
        distance = np.linalg.norm(diff)

        if distance < ego_object.goal_threshold:
            target_vel = np.zeros((2, 1))
        else:
            # Convert to diff drive velocities
            angle_to_goal = np.arctan2(diff[1], diff[0])
            diff_angle = angle_to_goal - state[2, 0]
            diff_angle = np.arctan2(np.sin(diff_angle), np.cos(diff_angle))

            linear = max_vel[0, 0] * np.cos(diff_angle)
            angular = (
                max_vel[1, 0] * np.sign(diff_angle) if abs(diff_angle) > 0.1 else 0
            )

            target_vel = np.array([[linear], [angular]])

        # Apply smoothing (exponential moving average)
        smoothed_vel = (
            self._smoothing * self._prev_vel + (1 - self._smoothing) * target_vel
        )
        self._prev_vel = smoothed_vel

        return smoothed_vel

Using Class-based Behavior#

import irsim

env = irsim.make()
env.load_behavior("custom_behavior_methods")

for i in range(500):
    env.step()
    env.render(0.05)

    if env.done():
        break

env.end()
world:
  height: 10
  width: 10

robot:
  # Robot using class-based smooth_dash behavior
  - kinematics: {name: 'diff'}
    shape: {name: 'circle', radius: 0.2}
    state: [2, 2, 0]
    goal: [8, 8]
    color: '#0072B2'
    vel_max: [2, 2]
    vel_min: [-2, -2]
    goal_threshold: 0.3
    behavior:
      name: 'smooth_dash'
      smoothing: 0.5
    plot:
      show_goal: true
      show_trajectory: true
      keep_traj_length: 200

  # Robot using regular dash behavior for comparison
  - kinematics: {name: 'diff'}
    shape: {name: 'circle', radius: 0.2}
    state: [2, 3, 0]
    goal: [8, 7]
    color: '#D55E00'
    vel_max: [2, 2]
    vel_min: [-2, -2]
    goal_threshold: 0.3
    behavior: {name: 'dash'}
    plot:
      show_goal: true
      show_trajectory: true
      keep_traj_length: 200

Behavior Registration Summary#

IR-SIM provides four behavior registration decorators:

Decorator

Scope

Type

Use Case

@register_behavior

Individual

Function

Simple per-object behaviors

@register_behavior_class

Individual

Class

Stateful per-object behaviors

@register_group_behavior

Group

Function

Simple multi-agent coordination

@register_group_behavior_class

Group

Class

Complex multi-agent algorithms (e.g., ORCA)

All decorators take two arguments:

  1. Kinematics type: 'omni', 'omni_angular', 'diff', or 'acker'

  2. Behavior name: The name used in YAML configuration

Tip

Choose the right decorator based on your needs:

  • Start with function-based (@register_behavior or @register_group_behavior) for simple behaviors

  • Use class-based when you need initialization, state, or complex algorithms

  • Use group behaviors when actions depend on multiple agents together

External Controller Examples (CBF / C3BF)#

Not every controller needs to be wired through the behavior registry. The repository ships standalone safety-filter controllers under usage/21cbf_world/:

  • cbf_qp.py / cbf_world.py: distance-based Control Barrier Function safety filter solved as a QP. Wraps a nominal goal-seeking command and enforces collision avoidance against circular obstacles.

  • c3bf_qp.py / c3bf_world.py: Collision-Cone CBF (C3BF) variant that also accounts for relative velocity, useful for dynamic obstacles.

Both examples support omni and diff kinematics, read the kinematics from YAML, and require the cvxpy solver (pip install cvxpy). See usage/21cbf_world/README.md for the full math, supported obstacle types, and known limitations.