Skip to content

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, from Element

Interactions:

  • ExplorationInteraction, from Interaction
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))