Exploration and preferential return
The exploration and preferential return model of human mobility of Song and coauthors.
- Name:
exploration_preferential_return - Field:
social/organizations - Formalism: agent-based system
- References: Song, Koren, Wang, and Barabási 2010; Alessandretti, Aslak, and Lehmann 2020
Select this system
from skcomplex.systems import create_simulation, create_system, describe
system = create_system('exploration_preferential_return')
entry = describe('exploration_preferential_return')
trajectory = create_simulation('exploration_preferential_return').prepare().run().trajectory_
Factory
@register_system(
field='social/organizations',
tags=('agents', 'human mobility', 'exploration', 'preferential return'),
references=('Song, Koren, Wang, and Barabási 2010', 'Alessandretti, Aslak, and Lehmann 2020'),
defaults={'until': 500},
)
def exploration_preferential_return(
n_individuals: int = 10,
*,
rho: float = 0.6,
gamma: float = 0.21,
beta: float = 1.55,
least_jump: float = 1.0,
) -> System:
"""Return the exploration and preferential return model of human mobility of Song and coauthors.
At every displacement an individual who has visited `S` distinct locations explores a new place with the
probability `rho S^-gamma`, at a distance drawn from the power law `P(r) ~ r^-beta`, and otherwise returns to a
visited location chosen in proportion to its number of visits (arXiv 2109.07381 Methods, the baseline of Fig. 2).
The number of distinct locations grows as `n^(1 / (1 + gamma))` after `n` displacements, and the visits of the
locations follow Zipf's law. The defaults `rho = 0.6` and `gamma = 0.21` are the values of Song and coauthors, and
every individual starts at the origin.
Args:
n_individuals:
The number of individuals.
rho:
The factor `rho` of the probability of exploring.
gamma:
The exponent `gamma` of the decay of the probability of exploring.
beta:
The exponent `beta` of the power law of the jump lengths, above 1.
least_jump:
The least jump length.
Returns:
system:
The system, with the group `individuals`.
Examples:
>>> from skcomplex.systems.social.organizations import exploration_preferential_return
>>> exploration_preferential_return(3).elements[0].prepare().size_
3
"""
individuals = GroupElement('individuals', element=ExplorerElement, size=n_individuals)
displacement = ExplorationInteraction(
'displacement',
target='individuals',
rho=rho,
gamma=gamma,
beta=beta,
least_jump=least_jump,
)
return System(elements=[individuals], interactions=[displacement])
Components
Elements:
ExplorerElement, fromElement
Interactions:
ExplorationInteraction, fromInteraction
class ExplorerElement(Element):
"""Individual of the mobility systems, with its position, its number of distinct locations, and its last level.
Args:
label:
The label of the individual or the group.
visual:
How the agent is drawn, or `None` for `default_visual_`.
x:
The first coordinate of the current location.
y:
The second coordinate of the current location.
distinct:
The number of distinct locations visited.
level:
The level-distance of the last displacement in a hierarchy of containers, 0 without one.
Examples:
>>> from skcomplex.systems.social.organizations import ExplorerElement
>>> ExplorerElement('individual').distinct
1.0
"""
def __init__(
self,
label: str,
visual: VisualSpec | None = None,
*,
x: Annotated[float, StateSpec()] = 0.0,
y: Annotated[float, StateSpec()] = 0.0,
distinct: Annotated[float, StateSpec(domain=NN)] = 1.0,
level: Annotated[float, StateSpec(domain=NN)] = 0.0,
) -> None:
super().__init__(label, visual=visual)
self.x = x
self.y = y
self.distinct = distinct
self.level = level
@classmethod
def _get_view(cls) -> type[ExplorerView]:
"""Return the class of the views of the agents, whose attributes are the states."""
return ExplorerView
class ExplorationInteraction(Interaction):
"""Exploration and preferential return of Song and coauthors, one displacement per individual and step.
An individual who has visited `S` distinct locations explores a new one with the probability `rho S^-gamma`, at a
distance drawn from the power law `P(r) ~ r^-beta` above `least_jump` in a uniform direction, and otherwise returns
to a visited location with the probability proportional to its number of visits.
Args:
label:
The label of the interaction.
target:
The path of the group of individuals.
rho:
The factor `rho` of the probability of exploring.
gamma:
The exponent `gamma` of the decay of the probability of exploring.
beta:
The exponent `beta` of the power law of the jump lengths, above 1.
least_jump:
The least jump length.
Examples:
>>> from skcomplex.systems.social.organizations import ExplorationInteraction
>>> ExplorationInteraction('displacement', target='individuals').rho
0.6
"""
def __init__(
self,
label: str,
*,
target: Annotated[str, RoleSpec()],
rho: Annotated[float, ParameterSpec(domain=RealSet.closed(0, 1))] = 0.6,
gamma: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.21,
beta: Annotated[float, ParameterSpec(domain=RealSet.open(1, oo))] = 1.55,
least_jump: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
) -> None:
super().__init__(label)
self.target = target
self.rho = rho
self.gamma = gamma
self.beta = beta
self.least_jump = least_jump
def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
"""Return the displacement rule.
Args:
s:
The namespace of the symbols the laws can use.
Returns:
laws:
The laws of the component.
"""
return [RuleLaw(self.move)]
def move(self, system: SystemView) -> None:
"""Displace every individual once, to a new location or back to a visited one.
Args:
system:
The system.
"""
rng = system.rng
memory = system.storage
for individual in system.agents(self.target, ExplorerView):
places = memory.setdefault(f'places {individual.id}', [(individual.x, individual.y)])
visits = memory.setdefault(f'visits {individual.id}', [1.0])
if not isinstance(places, list) or not isinstance(visits, list):
continue
if rng.random() < self.rho * len(places) ** -self.gamma:
length = self.least_jump * (1 - rng.random()) ** (-1 / (self.beta - 1))
angle = 2 * np.pi * rng.random()
place = (individual.x + length * np.cos(angle), individual.y + length * np.sin(angle))
places.append(place)
visits.append(1.0)
else:
weights = np.asarray(visits)
index = int(rng.choice(len(places), p=weights / weights.sum()))
visits[index] += 1.0
place = places[index]
individual.x, individual.y = float(place[0]), float(place[1])
individual.distinct = float(len(places))