Skip to content

Light-sensitive robots with sensory delay

Light-sensitive robots in a circular well, whose sensory delay decides whether they gather in the light.

  • Name: light_seeking_robots
  • Field: engineering/robotics
  • Formalism: stochastic differential equations
  • References: Mijalkov et al. 2016; Argun et al. 2021

Factory

@register_model(
    field='engineering/robotics',
    tags=('robotics', 'active matter', 'delay', 'sde'),
    references=('Mijalkov et al. 2016', 'Argun et al. 2021'),
    defaults={'until': 100.0, 'dt': 0.5},
)
def light_seeking_robots(
    n_robots: int = 20,
    *,
    delay: float = 5.0,
    V0: float = 1.0,
    V_inf: float = 0.1,
    I0: float = 1.0,
    r0: float = 1.0,
    tau: float = 1.0,
    R: float = 2.0,
) -> System:
    """Return light-sensitive robots in a circular well, whose sensory delay decides whether they gather in the light.

    Each robot moves at `V_inf + (V0 - V_inf) exp(-I(t - delay))` in a direction that diffuses with time scale `tau`,
    in the Gaussian light `I = I0 exp(-(x^2 + y^2) / r0^2)` at the centre of a well of radius `R`. The robots start
    at the centre, in directions spread around the circle, and do not interact. A positive delay drives them out of
    the light: a robot crosses the light at the fast speed it measured in the dark and lingers outside at the slow
    speed it measured in the light, so robots with a delay spend less time in the light than robots without one.

    Args:
        n_robots:
            The number of robots.

        delay:
            The sensory delay.

        V0:
            The speed in the dark.

        V_inf:
            The speed in strong light.

        I0:
            The intensity at the centre of the light.

        r0:
            The width of the light.

        tau:
            The time scale of rotational diffusion.

        R:
            The radius of the well.

    Returns:
        system:
            The model, with the group `robots` of positions `x`, `y` and directions `phi`.

    Examples:
        >>> from skcomplex.models.engineering.robotics import light_seeking_robots
        >>> light_seeking_robots(4).elements[0].check().prepare().size_
        4
    """
    headings = np.linspace(0.0, 2 * np.pi, n_robots, endpoint=False)
    robots = GroupElement(
        'robots',
        element=LightSensitiveRobotElement,
        size=n_robots,
        values={
            'x': np.zeros(n_robots),
            'y': np.zeros(n_robots),
            'phi': headings,
            'V0': V0,
            'V_inf': V_inf,
            'I0': I0,
            'r0': r0,
            'tau': tau,
            'R': R,
            'delay': delay,
        },
    )
    return System(elements=[robots])

Components

class LightSensitiveRobotElement(Element):
    """Robot in a Gaussian light field that slows down in light, reacting to the light it measured a delay ago.

    The robot moves at the speed `v = V_inf + (V0 - V_inf) exp(-I(t - delay))` in the direction `phi`, which diffuses
    with time scale `tau`, `dphi = sqrt(2 / tau) dW`. The light is `I(x, y) = I0 exp(-(x^2 + y^2) / r0^2)`, and a wall
    at radius `R` reflects the robot back into a circular well.

    Args:
        label:
            The label of the robot or the group.

        visual:
            How the element is drawn, or `None` for `default_visual_`.

        x:
            The initial horizontal position.

        y:
            The initial vertical position.

        phi:
            The initial direction of motion.

        V0:
            The speed in the dark.

        V_inf:
            The speed in strong light.

        I0:
            The intensity at the centre of the light.

        r0:
            The width of the light.

        tau:
            The time scale of rotational diffusion.

        R:
            The radius of the well.

        delay:
            The sensory delay, 0 for a robot that reacts to the light it measures now.

    Examples:
        >>> from skcomplex.models.engineering.robotics import LightSensitiveRobotElement
        >>> LightSensitiveRobotElement('robot', delay=5.0).delay
        5.0
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = None,
        *,
        x: Annotated[float, StateSpec(role='position')] = 0.0,
        y: Annotated[float, StateSpec(role='position')] = 0.0,
        phi: Annotated[float, StateSpec(role='direction')] = 0.0,
        V0: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
        V_inf: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.1,
        I0: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
        r0: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
        tau: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
        R: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 2.0,
        delay: float = 0.0,
    ) -> None:
        super().__init__(label, visual=visual)
        self.x = x
        self.y = y
        self.phi = phi
        self.V0 = V0
        self.V_inf = V_inf
        self.I0 = I0
        self.r0 = r0
        self.tau = tau
        self.R = R
        self.delay = delay

    def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
        """Return the motion at the light-dependent speed, the rotational noise, and the reflecting wall.

        Args:
            s:
                The namespace of the symbols the laws can use.

        Returns:
            laws:
                The laws of the component.
        """
        sensed = (
            LightSensitiveRobotElement._light(s, delayed(s.x, self.delay), delayed(s.y, self.delay))
            if self.delay
            else LightSensitiveRobotElement._light(s, s.x, s.y)
        )
        speed = s.V_inf + (s.V0 - s.V_inf) * exp(-sensed)
        reach = where(sqrt(s.x**2 + s.y**2) > s.R, sqrt(s.x**2 + s.y**2), s.R)
        return [
            RateLaw(s.x, speed * cos(s.phi)),
            RateLaw(s.y, speed * sin(s.phi)),
            NoiseLaw(s.phi, sqrt(2 / s.tau)),
            BoundaryLaw(s.x, s.x * s.R / reach),
            BoundaryLaw(s.y, s.y * s.R / reach),
        ]

    @staticmethod
    def _light(s: SimpleNamespace, x: Expression, y: Expression) -> Expression:
        """Return the Gaussian light intensity `I0 exp(-(x^2 + y^2) / r0^2)` at a point."""
        return s.I0 * exp(-(x**2 + y**2) / s.r0**2)

Equations

robots.x[i]: diff(x_robots(t), t) == ((V0_robots - V_inf_robots)*e^(-I0_robots*e^(-(x_robots(t - 5.0)^2 + y_robots(t - 5.0)^2)/r0_robots^2)) + V_inf_robots)*cos(phi_robots(t))
robots.y[i]: diff(y_robots(t), t) == ((V0_robots - V_inf_robots)*e^(-I0_robots*e^(-(x_robots(t - 5.0)^2 + y_robots(t - 5.0)^2)/r0_robots^2)) + V_inf_robots)*sin(phi_robots(t))
robots.phi[i]: diff(phi_robots(t), t) == 0