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