Skip to content

Organizations

Organizations: hierarchies, queues, workplaces, and institutions.

ArmsRaceElement(label, visual=None, *, a=10.0, b=10.5, margin=0.1, adjustment_time=2.0, perception_delay=1.0)

Bases: Element

Arms race element.

Source code in src/skcomplex/systems/social/organizations/_traps.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    a: Annotated[object, StateSpec(domain=RealSet.closed_open(0, oo), quantity='arms')] = 10.0,
    b: Annotated[object, StateSpec(domain=RealSet.closed_open(0, oo), quantity='arms')] = 10.5,
    margin: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.1,
    adjustment_time: Annotated[object, ParameterSpec(domain=RealSet.open(0, oo))] = 2.0,
    perception_delay: Annotated[object, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
) -> None:
    super().__init__(label, visual=visual)
    self.a = a
    self.b = b
    self.margin = margin
    self.adjustment_time = adjustment_time
    self.perception_delay = perception_delay

AssemblyInteraction(label, *, target, interruption=0.01)

Bases: Interaction

Assembly of the Hora and Tempus fable, one action per step that an interruption can undo.

Every step every watchmaker takes one action on the task in hand, unless a customer calls, with the probability interruption, and the unfinished task falls to pieces. A finished task is stable, and a watchmaker who finishes the last task of a watch sells it and starts the next one.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the group of watchmakers.

required
interruption Annotated[float, ParameterSpec(domain=closed(0, 1))]

The probability that a watchmaker is interrupted at an action.

0.01

Examples:

>>> from skcomplex.systems.social.organizations import AssemblyInteraction
>>> AssemblyInteraction('assembly', target='watchmakers').interruption
0.01
Source code in src/skcomplex/systems/social/organizations/_watchmakers.py
109
110
111
112
113
114
115
116
117
118
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
    interruption: Annotated[float, ParameterSpec(domain=RealSet.closed(0, 1))] = 0.01,
) -> None:
    super().__init__(label)
    self.target = target
    self.interruption = interruption

assemble(system)

Take one action of every watchmaker, or undo the task in hand at an interruption.

Parameters:

Name Type Description Default
system SystemView

The system.

required
Source code in src/skcomplex/systems/social/organizations/_watchmakers.py
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
def assemble(self, system: SystemView) -> None:
    """Take one action of every watchmaker, or undo the task in hand at an interruption.

    Args:
        system:
            The system.
    """
    for watchmaker in system.agents(self.target, WatchmakerView):
        if system.rng.random() < self.interruption:
            watchmaker.progress = 0.0
            continue
        watchmaker.progress += 1.0
        if watchmaker.progress < watchmaker.task_size:
            continue
        watchmaker.progress = 0.0
        watchmaker.done += 1.0
        if watchmaker.done >= watchmaker.tasks:
            watchmaker.done = 0.0
            watchmaker.watches += 1.0

BurdenElement(label, visual=None, *, capability=0.5, intervention=0.0, desired=1.0, fix_rate=1.0, build_time=5.0, solution_delay=2.0, erosion=0.5)

Bases: Element

Burden element.

Source code in src/skcomplex/systems/social/organizations/_traps.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    capability: Annotated[object, StateSpec(domain=RealSet.closed_open(0, oo), quantity='capability')] = 0.5,
    intervention: Annotated[object, StateSpec(domain=RealSet.closed_open(0, oo), quantity='intervention')] = 0.0,
    desired: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
    fix_rate: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
    build_time: Annotated[object, ParameterSpec(domain=RealSet.open(0, oo))] = 5.0,
    solution_delay: Annotated[object, ParameterSpec(domain=RealSet.open(0, oo))] = 2.0,
    erosion: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.5,
) -> None:
    super().__init__(label, visual=visual)
    self.capability = capability
    self.intervention = intervention
    self.desired = desired
    self.fix_rate = fix_rate
    self.build_time = build_time
    self.solution_delay = solution_delay
    self.erosion = erosion

CityDwellerElement(label, visual=MEETINGS_VISUAL, *, angle=0.0, age=0.0, degree=0.0, meetings=0.0)

Bases: Element

Resident of a growing city, with an angle of similarity, an age, and the meetings of the last step.

Parameters:

Name Type Description Default
label str

The label of the group.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

MEETINGS_VISUAL
angle Annotated[float, StateSpec()]

The angle of the resident on the circle, its similarity coordinate.

0.0
age Annotated[float, StateSpec(domain=closed_open(0, oo))]

The number of steps since the resident joined.

0.0
degree Annotated[float, StateSpec(domain=closed_open(0, oo))]

The number of neighbours of the resident at the start of the last step.

0.0
meetings Annotated[float, StateSpec(domain=closed_open(0, oo))]

The number of neighbours the resident met in the last step.

0.0

Examples:

>>> from skcomplex.systems.social.organizations import CityDwellerElement
>>> CityDwellerElement('residents', angle=1.5).angle
1.5
Source code in src/skcomplex/systems/social/organizations/_urban_interactions.py
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = MEETINGS_VISUAL,
    *,
    angle: Annotated[float, StateSpec()] = 0.0,
    age: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
    degree: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
    meetings: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.angle = angle
    self.age = age
    self.degree = degree
    self.meetings = meetings

CityResidentElement(label, x=0.0, contacts=0.0)

Bases: Element

Resident of a city, who stays at a position.

Parameters:

Name Type Description Default
label str

The label of the resident or the group.

required
x Annotated[ArrayLike, StateSpec(vector=True, role=position)]

The position.

0.0
contacts Annotated[float, StateSpec()]

The initial guess of the sum of the interactions with the others, which the equation replaces.

0.0

Examples:

>>> from skcomplex.systems.social.organizations import CityResidentElement
>>> CityResidentElement('resident', x=(1.0, 2.0)).x
(1.0, 2.0)
Source code in src/skcomplex/systems/social/organizations/_urban_scaling.py
45
46
47
48
49
50
51
52
53
def __init__(
    self,
    label: str,
    x: Annotated[npt.ArrayLike, StateSpec(vector=True, role='position')] = 0.0,
    contacts: Annotated[float, StateSpec()] = 0.0,
) -> None:
    super().__init__(label)
    self.x = x
    self.contacts = contacts

CommuteInteraction(label, *, target)

Bases: MovementInteraction

Commute of every resident in a cycle of three steps: to work, to its favourite place, and back home.

The work and the favourite place are regions of the layers facilities and places, so a resident stands at a new uniform point of the region at every visit, and at the same point of its home.

Parameters:

Name Type Description Default
label str

The label of the interaction, unique among the parts of its system.

required
target Annotated[str, RoleSpec()]

The path of the group of residents.

required

Examples:

>>> from skcomplex.systems.social.organizations import CommuteInteraction
>>> CommuteInteraction('commute', target='residents').schedule
'member'
Source code in src/skcomplex/systems/social/organizations/_kauai.py
185
186
def __init__(self, label: str, *, target: Annotated[str, RoleSpec()]) -> None:
    super().__init__(label, target=target, step=self._commute, schedule='member')

ContainerMobilityInteraction(label, *, target, levels=3, side=4, travel=(0.6, 0.3, 0.1), exponent=1.5)

Bases: Interaction

Container model of Alessandretti and coauthors, displacements between locations nested in containers.

Space is a nested grid of levels levels, every container of a level split into side by side containers of the level below, and the containers of the lowest level are the locations. The children of every container have attractivenesses drawn from a power law of the exponent exponent and normalized, and home is the location of the largest attractiveness at every level. Every step every individual draws the level-distance d of its displacement from travel, chooses a container of level d other than its own among the children of its container of level d + 1, by attractiveness, and then descends to a location by the attractivenesses of the children at every level below. Every individual has its own hierarchy, drawn at its first step, and starts at home.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the group of individuals.

required
levels Annotated[int, ParameterSpec(domain=NN)]

The number L of levels.

3
side Annotated[int, ParameterSpec(domain=NN)]

The number of children of a container along every axis.

4
travel Sequence[float]

The probabilities of displacements at the level-distances 1 to L, the same from any distance from home.

(0.6, 0.3, 0.1)
exponent Annotated[float, ParameterSpec(domain=open(0, oo))]

The exponent of the power law of the attractivenesses.

1.5

Raises:

Type Description
ValidationError

The probabilities of travel do not match the levels, raised by prepare.

Examples:

>>> from skcomplex.systems.social.organizations import ContainerMobilityInteraction
>>> ContainerMobilityInteraction('displacement', target='individuals').levels
3
Source code in src/skcomplex/systems/social/organizations/_mobility.py
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
    levels: Annotated[int, ParameterSpec(domain=NN)] = 3,
    side: Annotated[int, ParameterSpec(domain=NN)] = 4,
    travel: Sequence[float] = (0.6, 0.3, 0.1),
    exponent: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.5,
) -> None:
    super().__init__(label)
    self.target = target
    self.levels = levels
    self.side = side
    self.travel = travel
    self.exponent = exponent

move(system)

Displace every individual once, at a drawn level-distance, to a location chosen by attractiveness.

Parameters:

Name Type Description Default
system SystemView

The system.

required
Source code in src/skcomplex/systems/social/organizations/_mobility.py
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
def move(self, system: SystemView) -> None:
    """Displace every individual once, at a drawn level-distance, to a location chosen by attractiveness.

    Args:
        system:
            The system.
    """
    rng = system.rng
    memory = system.storage
    children, levels = int(self.side) ** 2, int(self.levels)
    travel = np.asarray(self.travel, dtype=float)
    for individual in system.agents(self.target, ExplorerView):
        if f'location {individual.id}' not in memory:
            memory[f'attractiveness {individual.id}'] = self._get_attractiveness(rng)
            memory[f'location {individual.id}'], memory[f'visited {individual.id}'] = 0, {0}
            individual.x, individual.y = self._get_position(0)
        attractiveness = memory[f'attractiveness {individual.id}']
        location = memory[f'location {individual.id}']
        visited = memory[f'visited {individual.id}']
        if not isinstance(attractiveness, list) or not isinstance(location, int) or not isinstance(visited, set):
            continue
        level = int(rng.choice(levels, p=travel)) + 1
        # The location is its digits of base `children`, the lowest level first, and the container of a level its
        # leading digits, so a container of level `level` is the location divided by `children^(level - 1)`.
        own = location // children ** (level - 1)
        siblings = (own // children) * children + np.arange(children)
        weights = attractiveness[level - 1][siblings].copy()
        weights[own % children] = 0.0
        chosen = int(siblings[rng.choice(children, p=weights / weights.sum())])
        location = self._descend(chosen, level, attractiveness, rng)
        visited.add(location)
        memory[f'location {individual.id}'] = location
        individual.x, individual.y = self._get_position(location)
        individual.level = float(level)
        individual.distinct = float(len(visited))

ContenderElement(label, visual=VisualSpec('agents', {'color_by': 'strength'}), *, strength=0.0, eta=1.0, relaxation=0.1)

Bases: Element

Agent of the Bonabeau system, who wanders on a grid and fights the agents it runs into.

Every step an agent's strength relaxes by the factor 1 - relaxation, and the agent tries to step into a random neighbouring cell. An attacker beats the occupant with probability 1 / (1 + exp(eta * (h_occupant - h_attacker))), the winner gains 1 and the loser loses 1, and a winning attacker swaps places with the occupant. Agents differ only by their strength, so the swap exchanges their strengths.

Parameters:

Name Type Description Default
label str

The label of the agent or group.

required
visual VisualSpec | None

How the agent is drawn, or None for default_visual_.

VisualSpec('agents', {'color_by': 'strength'})
strength Annotated[float, StateSpec()]

The initial strength h.

0.0
eta Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The sensitivity of the outcome of a fight to the strength difference.

1.0
relaxation Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The fraction of its strength an agent loses every step.

0.1

Examples:

>>> from skcomplex.systems.social.organizations import ContenderElement
>>> group = GroupElement('contenders', element=ContenderElement, size=10)
>>> group.label, group.element.__name__, group.size
('contenders', 'ContenderElement', 10)
Source code in src/skcomplex/systems/social/organizations/_hierarchies.py
61
62
63
64
65
66
67
68
69
70
71
72
73
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'strength'}),
    *,
    strength: Annotated[float, StateSpec()] = 0.0,
    eta: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
    relaxation: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.1,
) -> None:
    super().__init__(label, visual=visual)
    self.strength = strength
    self.eta = eta
    self.relaxation = relaxation

wander(agent, system)

Relax the strength, then step into a random neighbouring cell or fight its occupant.

Parameters:

Name Type Description Default
agent ContenderView

The contender.

required
system SystemView

The system.

required
Source code in src/skcomplex/systems/social/organizations/_hierarchies.py
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
def wander(self, agent: ContenderView, system: SystemView) -> None:
    """Relax the strength, then step into a random neighbouring cell or fight its occupant.

    Args:
        agent:
            The contender.

        system:
            The system.
    """
    agent.strength *= 1 - agent.relaxation
    cells = agent.neighbor_cells()
    cell = cells[int(system.rng.integers(len(cells)))]
    occupants = [
        other
        for other in agent.neighbors()
        if isinstance(other, ContenderView)
        and tuple(np.ravel(np.asarray(other.location))) == tuple(np.ravel(np.asarray(cell)))
    ]
    if occupants:
        self._fight(agent, occupants[0], system)
        return
    agent.move_to(cell)

ContenderView(agent, stepper)

Bases: AgentView

View of an agent of the Bonabeau hierarchy system, with its states and parameters as attributes.

Source code in src/skcomplex/core/engine/solvers/_mesa.py
559
560
561
def __init__(self, agent: mesa.Agent, stepper: 'MesaSolver') -> None:
    self._agent = agent
    self._stepper = stepper

ContestElement(label, visual=None, *, a=55.0, b=45.0, budget=10.0, lifetime=10.0)

Bases: Element

Contest element.

Source code in src/skcomplex/systems/social/organizations/_traps.py
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    a: Annotated[object, StateSpec(domain=RealSet.closed_open(0, oo), quantity='resources')] = 55.0,
    b: Annotated[object, StateSpec(domain=RealSet.closed_open(0, oo), quantity='resources')] = 45.0,
    budget: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 10.0,
    lifetime: Annotated[object, ParameterSpec(domain=RealSet.open(0, oo))] = 10.0,
) -> None:
    super().__init__(label, visual=visual)
    self.a = a
    self.b = b
    self.budget = budget
    self.lifetime = lifetime

DailyMovementsInteraction(label, *, women, premises, n_cities=3, phase=None, victims_phase=None)

Bases: Interaction

Movements of the women of the brothel movement system.

Every day one random free woman moves to a random cell of a random city, and in the steps of the phase victims_phase the traffickers move a random number of the victims of every brothel to random brothels. A move between cities counts as a train trip. The cities are vertical bands of equal width of the grid.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
women Annotated[str, RoleSpec()]

The path of the group of travellers.

required
premises Annotated[str, RoleSpec()]

The path of the group of premises, whose brothels the victims move between.

required
n_cities int

The number of cities, vertical bands of equal width.

3
phase str | None

The phase of the step the free woman moves in, or None for the default phase.

None
victims_phase str | None

The phase of the step the victims move in, or None for the default phase.

None

Examples:

>>> from skcomplex.systems.social.organizations import DailyMovementsInteraction
>>> DailyMovementsInteraction('day', women='women', premises='premises').n_cities
3
Source code in src/skcomplex/systems/social/organizations/_trafficking.py
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
def __init__(
    self,
    label: str,
    *,
    women: Annotated[str, RoleSpec()],
    premises: Annotated[str, RoleSpec()],
    n_cities: int = 3,
    phase: str | None = None,
    victims_phase: str | None = None,
) -> None:
    super().__init__(label)
    self.women = women
    self.premises = premises
    self.n_cities = n_cities
    self.phase = phase
    self.victims_phase = victims_phase

move_free(system)

Move a random free woman to a random cell of a random city.

Parameters:

Name Type Description Default
system SystemView

The system.

required
Source code in src/skcomplex/systems/social/organizations/_trafficking.py
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
def move_free(self, system: SystemView) -> None:
    """Move a random free woman to a random cell of a random city.

    Args:
        system:
            The system.
    """
    free = [woman for woman in system.agents('women', TravellerView) if not woman.victim]
    if not free:
        return
    width, height = self._shape(system)
    city = int(system.rng.integers(self.n_cities))
    start, stop = city * width // self.n_cities, (city + 1) * width // self.n_cities
    cell = (int(system.rng.integers(start, stop)), int(system.rng.integers(height)))
    self._travel(free[int(system.rng.integers(len(free)))], cell, width)

move_victims(system)

Move a random number of the victims of every brothel to random brothels.

Parameters:

Name Type Description Default
system SystemView

The system.

required
Source code in src/skcomplex/systems/social/organizations/_trafficking.py
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
def move_victims(self, system: SystemView) -> None:
    """Move a random number of the victims of every brothel to random brothels.

    Args:
        system:
            The system.
    """
    women = system.agents('women', TravellerView)
    brothels = [
        DailyMovementsInteraction._cell(place.location)
        for place in system.agents('premises', PremisesView)
        if place.brothel
    ]
    width = self._shape(system)[0]
    for brothel in brothels:
        victims = [
            woman for woman in women if woman.victim and DailyMovementsInteraction._cell(woman.location) == brothel
        ]
        moved = system.rng.permutation(len(victims))[: int(system.rng.integers(len(victims) + 1))]
        for index in moved:
            self._travel(victims[index], brothels[int(system.rng.integers(len(brothels)))], width)

EaterElement(label, visual=VisualSpec('agents', {'color_by': 'bmi', 'color_range': (18.5, 35.0)}), *, home=0.0, work=0.0, weight=70.0, height=170.0, age=40.0, male=1.0, habit=1.0, calories=0.0, bmi=24.0, day=0.0, reach=30.0, activity=1.55)

Bases: Element

Person of the obesity system, who goes from home to work and back every day and eats near each place.

The moves and the meals of the day are movements and transfers of the system, in the phases home, work, and evening. The calories of a meal are drawn from the bottom, middle, or top half of the restaurant's range by the eating habit. In the phase week, every seventh step, the weight changes by the calories eaten beyond seven days of the expenditure, the basal metabolic rate times the activity factor of the Harris-Benedict equation, at 7700 kilocalories per kilogram.

Parameters:

Name Type Description Default
label str

The label of the agent or group.

required
visual VisualSpec | None

How the agent is drawn, or None for default_visual_.

VisualSpec('agents', {'color_by': 'bmi', 'color_range': (18.5, 35.0)})
home Annotated[ArrayLike, StateSpec(vector=True)]

The position of the person's home.

0.0
work Annotated[ArrayLike, StateSpec(vector=True)]

The position of the person's workplace.

0.0
weight Annotated[float, StateSpec(domain=open(0, oo))]

The weight in kilograms.

70.0
height Annotated[float, StateSpec(domain=open(0, oo))]

The height in centimetres.

170.0
age Annotated[float, StateSpec(domain=open(0, oo))]

The age in years.

40.0
male Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))]

1 for a man and 0 for a woman.

1.0
habit Annotated[float, StateSpec(domain=NN)]

The eating habit: low at 0, medium at 1, or high at 2.

1.0
calories Annotated[float, StateSpec(domain=closed_open(0, oo))]

The calories eaten so far this week.

0.0
bmi Annotated[float, StateSpec(domain=open(0, oo))]

The body mass index.

24.0
day Annotated[float, StateSpec(domain=NN)]

The number of days lived in the system.

0.0
reach Annotated[float, ParameterSpec(domain=open(0, oo))]

The distance within which a person looks for a restaurant.

30.0
activity Annotated[float, ParameterSpec(domain=open(0, oo))]

The factor of the basal metabolic rate a person spends a day, 1.2 when sedentary and 1.9 when very active.

1.55

Examples:

>>> from skcomplex.core.components.elements import GroupElement
>>> from skcomplex.systems.social.organizations import EaterElement
>>> group = GroupElement(
...     'people', element=EaterElement, size=5, values={'home': [1.0, 2.0], 'work': [3.0, 4.0]}
... )
>>> group.label, group.element.__name__, group.size
('people', 'EaterElement', 5)
Source code in src/skcomplex/systems/social/organizations/_obesity.py
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'bmi', 'color_range': (18.5, 35.0)}),
    *,
    home: Annotated[npt.ArrayLike, StateSpec(vector=True)] = 0.0,
    work: Annotated[npt.ArrayLike, StateSpec(vector=True)] = 0.0,
    weight: Annotated[float, StateSpec(domain=RealSet.open(0, oo))] = 70.0,
    height: Annotated[float, StateSpec(domain=RealSet.open(0, oo))] = 170.0,
    age: Annotated[float, StateSpec(domain=RealSet.open(0, oo))] = 40.0,
    male: Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))] = 1.0,
    habit: Annotated[float, StateSpec(domain=NN)] = 1.0,
    calories: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
    bmi: Annotated[float, StateSpec(domain=RealSet.open(0, oo))] = 24.0,
    day: Annotated[float, StateSpec(domain=NN)] = 0.0,
    reach: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 30.0,
    activity: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.55,
) -> None:
    super().__init__(label, visual=visual)
    self.home = home
    self.work = work
    self.weight = weight
    self.height = height
    self.age = age
    self.male = male
    self.habit = habit
    self.calories = calories
    self.bmi = bmi
    self.day = day
    self.reach = reach
    self.activity = activity

count_day(agent, system)

Count one more day lived in the system.

Parameters:

Name Type Description Default
agent EaterView

The person.

required
system SystemView

The system.

required
Source code in src/skcomplex/systems/social/organizations/_obesity.py
206
207
208
209
210
211
212
213
214
215
216
def count_day(self, agent: EaterView, system: SystemView) -> None:
    """Count one more day lived in the system.

    Args:
        agent:
            The person.

        system:
            The system.
    """
    agent.day += 1

draw_meal(agent, restaurant, system) staticmethod

Return the calories of a meal, drawn from the part of the range of a restaurant that a habit eats.

Parameters:

Name Type Description Default
agent EaterView

The person.

required
restaurant RestaurantView

The restaurant.

required
system SystemView

The system.

required

Returns:

Name Type Description
calories float

The calories of the meal.

Source code in src/skcomplex/systems/social/organizations/_obesity.py
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
@staticmethod
def draw_meal(agent: EaterView, restaurant: RestaurantView, system: SystemView) -> float:
    """Return the calories of a meal, drawn from the part of the range of a restaurant that a habit eats.

    Args:
        agent:
            The person.

        restaurant:
            The restaurant.

        system:
            The system.

    Returns:
        calories:
            The calories of the meal.
    """
    low, high = MEAL_RANGES[int(restaurant.kind)]
    start, stop = HABIT_RANGES[int(agent.habit)]
    return float(system.rng.uniform(low + start * (high - low), low + stop * (high - low)))

gain_weight(agent, system)

Change the weight by the calories of the week beyond the basal needs, and start a new week.

Parameters:

Name Type Description Default
agent EaterView

The person.

required
system SystemView

The system.

required
Source code in src/skcomplex/systems/social/organizations/_obesity.py
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
def gain_weight(self, agent: EaterView, system: SystemView) -> None:
    """Change the weight by the calories of the week beyond the basal needs, and start a new week.

    Args:
        agent:
            The person.

        system:
            The system.
    """
    rate = basal_metabolic_rate(agent.weight, agent.height, agent.age, agent.male)
    needs = DAYS_PER_WEEK * agent.activity * rate
    agent.weight += (agent.calories - needs) / KCAL_PER_KG
    agent.calories = 0.0
    agent.bmi = agent.weight / (agent.height / 100) ** 2

go_home(agent, system) staticmethod

Return the position of the home of a person.

Parameters:

Name Type Description Default
agent EaterView

The person.

required
system SystemView

The system.

required

Returns:

Name Type Description
position NDArray[float64]

The position of the home.

Source code in src/skcomplex/systems/social/organizations/_obesity.py
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
@staticmethod
def go_home(agent: EaterView, system: SystemView) -> npt.NDArray[np.float64]:
    """Return the position of the home of a person.

    Args:
        agent:
            The person.

        system:
            The system.

    Returns:
        position:
            The position of the home.
    """
    return np.asarray(agent.home, dtype=float)

go_to_work(agent, system) staticmethod

Return the position of the workplace of a person.

Parameters:

Name Type Description Default
agent EaterView

The person.

required
system SystemView

The system.

required

Returns:

Name Type Description
position NDArray[float64]

The position of the workplace.

Source code in src/skcomplex/systems/social/organizations/_obesity.py
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
@staticmethod
def go_to_work(agent: EaterView, system: SystemView) -> npt.NDArray[np.float64]:
    """Return the position of the workplace of a person.

    Args:
        agent:
            The person.

        system:
            The system.

    Returns:
        position:
            The position of the workplace.
    """
    return np.asarray(agent.work, dtype=float)

settle_meal(agent, restaurant, amounts, system) staticmethod

Credit a person with the calories of a meal, and count the sale on the restaurant.

Parameters:

Name Type Description Default
agent EaterView

The person.

required
restaurant RestaurantView

The restaurant.

required
amounts Sequence[float]

The calories of the meal, the only amount.

required
system SystemView

The system.

required
Source code in src/skcomplex/systems/social/organizations/_obesity.py
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
@staticmethod
def settle_meal(agent: EaterView, restaurant: RestaurantView, amounts: Sequence[float], system: SystemView) -> None:
    """Credit a person with the calories of a meal, and count the sale on the restaurant.

    Args:
        agent:
            The person.

        restaurant:
            The restaurant.

        amounts:
            The calories of the meal, the only amount.

        system:
            The system.
    """
    calories = amounts[0]
    low, high = MEAL_RANGES[int(restaurant.kind)]
    agent.calories += calories
    restaurant.sold += 1
    restaurant.sold_high += float(calories > (low + high) / 2)

EaterView(agent, stepper)

Bases: AgentView

View of a person of the obesity system, with its states and parameters as attributes.

Source code in src/skcomplex/core/engine/solvers/_mesa.py
559
560
561
def __init__(self, agent: mesa.Agent, stepper: 'MesaSolver') -> None:
    self._agent = agent
    self._stepper = stepper

EmployeeElement(label, visual=VisualSpec('agents', {'color_by': 'risk', 'color_range': (0.0, 5.0)}), *, predisposed=0.0, disgruntlement=0.0, stress=0.0, skill=0.0, affect=0.5, risk=0.5, days=0.0, first=0.0, mean_feeling=0.0, expectation=0.0, feeling=0.0, turnover=0.15, culture=0.8, change=0.05, primacy=1.0, consistency=1.0, recency=1.0, affect_weight=1.0, alpha_seen=0.5, lambda_seen=1.0, alpha_unseen=0.25, lambda_unseen=0.0, skill_growth=0.001, skill_variance=0.0005)

Bases: Element

Employee of the insider sabotage system, whose risk is the sum of five factors.

The risk is R = P + D + S + K + A: the predisposition P, the disgruntlement D of predisposed employees whose daily feeling about the organization falls short of their expectation, the stress S raised by seeing a supervisor, the skill K that grows every day, and the fixed affect A. The expectation weighs the first, the mean, and the latest feeling. Every day an employee wanders to an adjacent cell and leaves with probability turnover / 365, replaced by a new hire. In insider_sabotage, a movement, a map of every state, and a life cycle that renews the employees who leave run these steps in turn in the activation of every employee.

Parameters:

Name Type Description Default
label str

The label of the agent or group.

required
visual VisualSpec | None

How the agent is drawn, or None for default_visual_.

VisualSpec('agents', {'color_by': 'risk', 'color_range': (0.0, 5.0)})
predisposed Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))]

Whether the employee is predisposed to sabotage, 1, or not, 0.

0.0
disgruntlement Annotated[float, StateSpec()]

The initial disgruntlement D.

0.0
stress Annotated[float, StateSpec(domain=closed_open(0, oo))]

The initial stress S.

0.0
skill Annotated[float, StateSpec()]

The initial skill K.

0.0
affect Annotated[float, StateSpec(domain=closed_open(0, oo))]

The affect A.

0.5
risk Annotated[float, StateSpec()]

The initial risk R.

0.5
days Annotated[float, StateSpec(domain=NN)]

The number of days the employee has felt about the organization.

0.0
first Annotated[float, StateSpec()]

The first feeling.

0.0
mean_feeling Annotated[float, StateSpec()]

The mean feeling.

0.0
expectation Annotated[float, StateSpec()]

The expected feeling.

0.0
feeling Annotated[float, StateSpec()]

The latest feeling.

0.0
turnover Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The yearly share of employees who leave.

0.15
culture Annotated[float, ParameterSpec()]

The mean daily feeling about the organization.

0.8
change Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The variance of the daily feeling.

0.05
primacy Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The weight of the first feeling in the expectation.

1.0
consistency Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The weight of the mean feeling in the expectation.

1.0
recency Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The weight of the latest feeling in the expectation.

1.0
affect_weight Annotated[float, ParameterSpec()]

The factor of the shortfall of a feeling in the disgruntlement.

1.0
alpha_seen Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The learning rate of stress when a supervisor is in sight.

0.5
lambda_seen Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The level stress approaches when a supervisor is in sight.

1.0
alpha_unseen Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The learning rate of stress otherwise.

0.25
lambda_unseen Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The level stress approaches otherwise.

0.0
skill_growth Annotated[float, ParameterSpec()]

The mean daily growth of skill.

0.001
skill_variance Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The variance of the daily growth of skill.

0.0005

Examples:

>>> from skcomplex.core.components.elements import GroupElement
>>> from skcomplex.systems.social.organizations import EmployeeElement
>>> group = GroupElement('employees', element=EmployeeElement, size=10, values={'affect': 0.5})
>>> group.label, group.element.__name__, group.size
('employees', 'EmployeeElement', 10)
Source code in src/skcomplex/systems/social/organizations/_sabotage.py
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'risk', 'color_range': (0.0, 5.0)}),
    *,
    predisposed: Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))] = 0.0,
    disgruntlement: Annotated[float, StateSpec()] = 0.0,
    stress: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
    skill: Annotated[float, StateSpec()] = 0.0,
    affect: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.5,
    risk: Annotated[float, StateSpec()] = 0.5,
    days: Annotated[float, StateSpec(domain=NN)] = 0.0,
    first: Annotated[float, StateSpec()] = 0.0,
    mean_feeling: Annotated[float, StateSpec()] = 0.0,
    expectation: Annotated[float, StateSpec()] = 0.0,
    feeling: Annotated[float, StateSpec()] = 0.0,
    turnover: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.15,
    culture: Annotated[float, ParameterSpec()] = 0.8,
    change: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.05,
    primacy: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
    consistency: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
    recency: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
    affect_weight: Annotated[float, ParameterSpec()] = 1.0,
    alpha_seen: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.5,
    lambda_seen: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
    alpha_unseen: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.25,
    lambda_unseen: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
    skill_growth: Annotated[float, ParameterSpec()] = 0.001,
    skill_variance: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.0005,
) -> None:
    super().__init__(label, visual=visual)
    self.predisposed = predisposed
    self.disgruntlement = disgruntlement
    self.stress = stress
    self.skill = skill
    self.affect = affect
    self.risk = risk
    self.days = days
    self.first = first
    self.mean_feeling = mean_feeling
    self.expectation = expectation
    self.feeling = feeling
    self.turnover = turnover
    self.culture = culture
    self.change = change
    self.primacy = primacy
    self.consistency = consistency
    self.recency = recency
    self.affect_weight = affect_weight
    self.alpha_seen = alpha_seen
    self.lambda_seen = lambda_seen
    self.alpha_unseen = alpha_unseen
    self.lambda_unseen = lambda_unseen
    self.skill_growth = skill_growth
    self.skill_variance = skill_variance

EmployeeView(agent, stepper)

Bases: AgentView

View of an employee of the insider sabotage system, with its states and parameters as attributes.

Source code in src/skcomplex/core/engine/solvers/_mesa.py
559
560
561
def __init__(self, agent: mesa.Agent, stepper: 'MesaSolver') -> None:
    self._agent = agent
    self._stepper = stepper

ExecutionInteraction(label, *, target)

Bases: Interaction

Execution of one task per step from the list of the Barabási system.

The highest priority task is executed with probability greed and a random task otherwise. The executed task is replaced by a new task with a uniform random priority, and every other task ages by one step. The waiting time of the executed task is reported as wait, so the run records one waiting time per step.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the group of tasks.

required

Examples:

>>> from skcomplex.systems.social.organizations import ExecutionInteraction
>>> ExecutionInteraction('execution', target='tasks').target
'tasks'
Source code in src/skcomplex/systems/social/organizations/_queues.py
105
106
107
108
109
110
111
112
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
) -> None:
    super().__init__(label)
    self.target = target

execute(system)

Execute one task and replace it with a new one.

Parameters:

Name Type Description Default
system SystemView

The system.

required
Source code in src/skcomplex/systems/social/organizations/_queues.py
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
def execute(self, system: SystemView) -> None:
    """Execute one task and replace it with a new one.

    Args:
        system:
            The system.
    """
    tasks = system.agents(self.target, TaskView)
    if not tasks:
        return
    priorities = [task.priority for task in tasks]
    greedy = system.rng.random() < tasks[0].greed
    chosen = int(np.argmax(priorities)) if greedy else int(system.rng.integers(len(tasks)))
    for index, task in enumerate(tasks):
        task.age += 1
        if index == chosen:
            task.wait, task.age, task.priority = task.age, 0.0, float(system.rng.random())
            system.report('wait', task.wait)

ExplorationInteraction(label, *, target, rho=0.6, gamma=0.21, beta=1.55, least_jump=1.0)

Bases: 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.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the group of individuals.

required
rho Annotated[float, ParameterSpec(domain=closed(0, 1))]

The factor rho of the probability of exploring.

0.6
gamma Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The exponent gamma of the decay of the probability of exploring.

0.21
beta Annotated[float, ParameterSpec(domain=open(1, oo))]

The exponent beta of the power law of the jump lengths, above 1.

1.55
least_jump Annotated[float, ParameterSpec(domain=open(0, oo))]

The least jump length.

1.0

Examples:

>>> from skcomplex.systems.social.organizations import ExplorationInteraction
>>> ExplorationInteraction('displacement', target='individuals').rho
0.6
Source code in src/skcomplex/systems/social/organizations/_mobility.py
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
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

move(system)

Displace every individual once, to a new location or back to a visited one.

Parameters:

Name Type Description Default
system SystemView

The system.

required
Source code in src/skcomplex/systems/social/organizations/_mobility.py
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
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))

ExplorerElement(label, visual=None, *, x=0.0, y=0.0, distinct=1.0, level=0.0)

Bases: Element

Individual of the mobility systems, with its position, its number of distinct locations, and its last level.

Parameters:

Name Type Description Default
label str

The label of the individual or the group.

required
visual VisualSpec | None

How the agent is drawn, or None for default_visual_.

None
x Annotated[float, StateSpec()]

The first coordinate of the current location.

0.0
y Annotated[float, StateSpec()]

The second coordinate of the current location.

0.0
distinct Annotated[float, StateSpec(domain=NN)]

The number of distinct locations visited.

1.0
level Annotated[float, StateSpec(domain=NN)]

The level-distance of the last displacement in a hierarchy of containers, 0 without one.

0.0

Examples:

>>> from skcomplex.systems.social.organizations import ExplorerElement
>>> ExplorerElement('individual').distinct
1.0
Source code in src/skcomplex/systems/social/organizations/_mobility.py
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
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

ExplorerView(agent, stepper)

Bases: AgentView

View of an individual of the mobility systems, with its states as attributes.

Source code in src/skcomplex/core/engine/solvers/_mesa.py
559
560
561
def __init__(self, agent: mesa.Agent, stepper: 'MesaSolver') -> None:
    self._agent = agent
    self._stepper = stepper

FarmerElement(label, visual=VisualSpec('agents', {'color_by': 'wage', 'color_range': (0.0, 2.0)}), *, wage=1.0, workforce=50.0, employed=0.0, area=0.0)

Bases: Element

Employer of Ajani's Choice, who offers a daily wage until the workforce it needs is filled.

Parameters:

Name Type Description Default
label str

The label of the employer or the group.

required
visual VisualSpec | None

How the agent is drawn, or None for default_visual_.

VisualSpec('agents', {'color_by': 'wage', 'color_range': (0.0, 2.0)})
wage Annotated[float, StateSpec(domain=closed(0, 2))]

The daily wage the employer offers, the minimum wage being 1.

1.0
workforce Annotated[float, StateSpec(domain=NN)]

The number of workers the employer needs every day.

50.0
employed Annotated[float, StateSpec(domain=NN)]

The number of workers the employer has hired today.

0.0
area Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))]

The area of the farm, 0 near the first slum and 1 near the second.

0.0

Examples:

>>> from skcomplex.systems.social.organizations import FarmerElement
>>> FarmerElement('farmer', wage=1.5).wage
1.5
Source code in src/skcomplex/systems/social/organizations/_wages.py
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'wage', 'color_range': (0.0, 2.0)}),
    *,
    wage: Annotated[float, StateSpec(domain=RealSet.closed(0, 2))] = 1.0,
    workforce: Annotated[float, StateSpec(domain=NN)] = 50.0,
    employed: Annotated[float, StateSpec(domain=NN)] = 0.0,
    area: Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.wage = wage
    self.workforce = workforce
    self.employed = employed
    self.area = area

FarmerView(agent, stepper)

Bases: AgentView

View of an employer of Ajani's Choice, with its states as attributes.

Source code in src/skcomplex/core/engine/solvers/_mesa.py
559
560
561
def __init__(self, agent: mesa.Agent, stepper: 'MesaSolver') -> None:
    self._agent = agent
    self._stepper = stepper

GravityContactInteraction(label, *, target, reach, gamma=1.0)

Bases: Interaction

Contacts of every resident, the sum of r^-gamma over the others within a reach.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the group of residents.

required
reach float

The largest distance of a contact, which the diameter of a city covers.

required
gamma Annotated[float, ParameterSpec(domain=open(0, oo))]

The decay exponent of an interaction with the distance.

1.0

Examples:

>>> from skcomplex.systems.social.organizations import GravityContactInteraction
>>> GravityContactInteraction('contact', target='residents', reach=30.0, gamma=1.5).gamma
1.5
Source code in src/skcomplex/systems/social/organizations/_urban_scaling.py
78
79
80
81
82
83
84
85
86
87
88
89
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
    reach: float,
    gamma: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
) -> None:
    super().__init__(label)
    self.target = target
    self.reach = reach
    self.gamma = gamma

HarvestInteraction(label, *, employers, workers, asks=1, chat=3, adjustment=0.2)

Bases: Interaction

Working day of Ajani's Choice: hiring in the morning, wage adjustment and chat among employers in the evening.

Every worker asks the employers of its local area, the area of its slum, and moves to the other slum when its area has none. In each of asks rounds every worker without work, in random order, asks a random employer it has not asked yet, and an employer whose workforce is not filled offers its wage. The worker accepts if the wage is at least the minimum wage 1, if it did not work yesterday for at least the minimum wage, or if no employer is left to ask. In the evening every employer lowers its wage by adjustment if it filled its workforce and raises it otherwise, within [0.1, 2], and then, in random order, sets its wage to the mean wage of chat random employers. The interaction reports the number of workers paid below the minimum wage, slaves, and of workers paid at least the minimum wage, in_work.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
employers Annotated[str, RoleSpec()]

The path of the group of employers.

required
workers Annotated[str, RoleSpec()]

The path of the group of workers.

required
asks Annotated[int, ParameterSpec(domain=NN)]

The number of employers a worker asks for work in a day.

1
chat Annotated[int, ParameterSpec(domain=NN)]

The number of employers whose wages an employer averages in the evening.

3
adjustment Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The change of the wage of an employer after a day.

0.2

Examples:

>>> from skcomplex.systems.social.organizations import HarvestInteraction
>>> HarvestInteraction('day', employers='employers', workers='workers').chat
3
Source code in src/skcomplex/systems/social/organizations/_wages.py
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
def __init__(
    self,
    label: str,
    *,
    employers: Annotated[str, RoleSpec()],
    workers: Annotated[str, RoleSpec()],
    asks: Annotated[int, ParameterSpec(domain=NN)] = 1,
    chat: Annotated[int, ParameterSpec(domain=NN)] = 3,
    adjustment: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.2,
) -> None:
    super().__init__(label)
    self.employers = employers
    self.workers = workers
    self.asks = asks
    self.chat = chat
    self.adjustment = adjustment

work(system)

Run one working day.

Parameters:

Name Type Description Default
system SystemView

The system.

required
Source code in src/skcomplex/systems/social/organizations/_wages.py
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
def work(self, system: SystemView) -> None:
    """Run one working day.

    Args:
        system:
            The system.
    """
    farmers = system.agents(self.employers, FarmerView)
    harvesters = system.agents(self.workers, HarvesterView)
    rng = system.rng
    for farmer in farmers:
        farmer.employed = 0.0
    self._hire(farmers, harvesters, rng)
    for harvester in harvesters:
        harvester.total_pay += harvester.pay
    for farmer in farmers:
        step = -self.adjustment if farmer.employed >= farmer.workforce else self.adjustment
        farmer.wage = float(np.clip(farmer.wage + step, 0.1, 2.0))
    chat = min(int(self.chat), len(farmers))
    for position in rng.permutation(len(farmers)).tolist():
        partners = rng.choice(len(farmers), chat, replace=False).tolist()
        farmers[position].wage = float(np.mean([farmers[partner].wage for partner in partners]))
    system.report('slaves', float(sum(item.working and item.pay < 1 for item in harvesters)))
    system.report('in_work', float(sum(item.working and item.pay >= 1 for item in harvesters)))

HarvesterElement(label, visual=VisualSpec('agents', {'color_by': 'pay', 'color_range': (0.0, 2.0)}), *, slum=0.0, working=0.0, pay=0.0, total_pay=0.0)

Bases: Element

Migrant worker of Ajani's Choice, who asks the employers near its slum for a day of work.

Parameters:

Name Type Description Default
label str

The label of the worker or the group.

required
visual VisualSpec | None

How the agent is drawn, or None for default_visual_.

VisualSpec('agents', {'color_by': 'pay', 'color_range': (0.0, 2.0)})
slum Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))]

The slum the worker lives in, 0 or 1.

0.0
working Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))]

Whether the worker works today.

0.0
pay Annotated[float, StateSpec(domain=closed_open(0, oo))]

The pay of the worker today.

0.0
total_pay Annotated[float, StateSpec(domain=closed_open(0, oo))]

The pay the worker has earned so far.

0.0

Examples:

>>> from skcomplex.systems.social.organizations import HarvesterElement
>>> HarvesterElement('worker', slum=1.0).slum
1.0
Source code in src/skcomplex/systems/social/organizations/_wages.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'pay', 'color_range': (0.0, 2.0)}),
    *,
    slum: Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))] = 0.0,
    working: Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))] = 0.0,
    pay: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
    total_pay: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.slum = slum
    self.working = working
    self.pay = pay
    self.total_pay = total_pay

HarvesterView(agent, stepper)

Bases: AgentView

View of a worker of Ajani's Choice, with its states as attributes.

Source code in src/skcomplex/core/engine/solvers/_mesa.py
559
560
561
def __init__(self, agent: mesa.Agent, stepper: 'MesaSolver') -> None:
    self._agent = agent
    self._stepper = stepper

PerformanceElement(label, visual=None, *, performance=1.0, perceived=1.0, desired=1.0, bias=0.1, perception_time=2.0, correction_time=2.0, goal_time=5.0, erosion=1.0)

Bases: Element

Performance element.

Source code in src/skcomplex/systems/social/organizations/_traps.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    performance: Annotated[object, StateSpec(quantity='performance')] = 1.0,
    perceived: Annotated[object, StateSpec(quantity='performance')] = 1.0,
    desired: Annotated[object, StateSpec(quantity='performance')] = 1.0,
    bias: Annotated[object, ParameterSpec(domain=RealSet.open(-oo, 1))] = 0.1,
    perception_time: Annotated[object, ParameterSpec(domain=RealSet.open(0, oo))] = 2.0,
    correction_time: Annotated[object, ParameterSpec(domain=RealSet.open(0, oo))] = 2.0,
    goal_time: Annotated[object, ParameterSpec(domain=RealSet.open(0, oo))] = 5.0,
    erosion: Annotated[object, ParameterSpec(domain=RealSet.closed(0, 1))] = 1.0,
) -> None:
    super().__init__(label, visual=visual)
    self.performance = performance
    self.perceived = perceived
    self.desired = desired
    self.bias = bias
    self.perception_time = perception_time
    self.correction_time = correction_time
    self.goal_time = goal_time
    self.erosion = erosion

PlaceVisitsInteraction(label, *, target)

Bases: MapInteraction

Count of the steps every resident stands in its favourite place, read from the regions of the layer places.

Parameters:

Name Type Description Default
label str

The label of the interaction, unique among the parts of its system.

required
target Annotated[str, RoleSpec()]

The path of the group of residents.

required

Examples:

>>> from skcomplex.systems.social.organizations import PlaceVisitsInteraction
>>> PlaceVisitsInteraction('visits', target='residents').mode
'increment'
Source code in src/skcomplex/systems/social/organizations/_kauai.py
218
219
220
221
222
223
224
225
226
227
def __init__(self, label: str, *, target: Annotated[str, RoleSpec()]) -> None:
    super().__init__(
        label,
        target=target,
        state='visits',
        function=self._count,
        mode='increment',
        schedule='member',
        form='agent',
    )

PremisesElement(label, visual=VisualSpec('agents', {'color_by': 'brothel', 'color_range': (0.0, 1.0)}), *, brothel=0.0, bills=0.0, last_bills=0.0, efficiency=1.0, day=0.0)

Bases: Element

House or brothel of the brothel movement system, whose energy bills grow with the people inside.

Every day the bills grow by 1 / (1 + exp(x0 - n)) for the n people in the premises' cell, and every quarter they are recorded and reset. In trafficking, maps of the bills read the counts of the women by cell every day, and maps in a phase of a period of 90 days record and reset them.

Parameters:

Name Type Description Default
label str

The label of the agent or group.

required
visual VisualSpec | None

How the agent is drawn, or None for default_visual_.

VisualSpec('agents', {'color_by': 'brothel', 'color_range': (0.0, 1.0)})
brothel Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))]

Whether the premises are a brothel, 1, or a house, 0.

0.0
bills Annotated[float, StateSpec(domain=closed_open(0, oo))]

The bills of the current quarter.

0.0
last_bills Annotated[float, StateSpec(domain=closed_open(0, oo))]

The bills of the last full quarter.

0.0
efficiency Annotated[float, StateSpec()]

The midpoint x0 of the sigmoid of the bills.

1.0
day Annotated[float, StateSpec(domain=NN)]

The number of days run.

0.0

Examples:

>>> from skcomplex.core.components.elements import GroupElement
>>> from skcomplex.systems.social.organizations import PremisesElement
>>> group = GroupElement('premises', element=PremisesElement, size=3, values={'brothel': 1.0})
>>> group.label, group.element.__name__, group.size
('premises', 'PremisesElement', 3)
Source code in src/skcomplex/systems/social/organizations/_trafficking.py
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'brothel', 'color_range': (0.0, 1.0)}),
    *,
    brothel: Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))] = 0.0,
    bills: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
    last_bills: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
    efficiency: Annotated[float, StateSpec()] = 1.0,
    day: Annotated[float, StateSpec(domain=NN)] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.brothel = brothel
    self.bills = bills
    self.last_bills = last_bills
    self.efficiency = efficiency
    self.day = day

PremisesView(agent, stepper)

Bases: AgentView

View of a premises of the trafficking system, with its states and parameters as attributes.

Source code in src/skcomplex/core/engine/solvers/_mesa.py
559
560
561
def __init__(self, agent: mesa.Agent, stepper: 'MesaSolver') -> None:
    self._agent = agent
    self._stepper = stepper

ResidentElement(label, visual=VisualSpec('agents', {'color_by': 'sentiment', 'color_range': (-1.0, 1.0)}), *, sentiment=0.0, weights=0.0, home=(math.nan, math.nan), census=0.0, work=0.0, favourite=0.0, visits=0.0, race=0.0, gender=0.0, occupation=0.0, age=0.0, convergence=1.0, event_rate=0.05, cell_size=1000.0, partners=15.0)

Bases: Element

Resident of the Kauai population system, who lives, works, and spends time at points of a map.

A resident starts at its home, a point of its census region. It holds a sentiment between -1 and 1, one weight per topic of the events that move the sentiment, and the demographic scalars of Carmichael et al. (2019 ch. 3 §4.2).

Parameters:

Name Type Description Default
label str

The label of the agent or group.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

VisualSpec('agents', {'color_by': 'sentiment', 'color_range': (-1.0, 1.0)})
sentiment Annotated[float, StateSpec(domain=closed(-1, 1))]

The initial sentiment.

0.0
weights Annotated[ArrayLike, StateSpec(vector=True)]

The weight of every topic.

0.0
home Annotated[ArrayLike, StateSpec(vector=True)]

The point of the home, which the first commute sets to the initial location.

(nan, nan)
census Annotated[float, StateSpec()]

The index of the census region of the home.

0.0
work Annotated[float, StateSpec()]

The index of the facility of the work.

0.0
favourite Annotated[float, StateSpec()]

The index of the favourite place.

0.0
visits Annotated[float, StateSpec()]

The number of steps the resident stood in its favourite place.

0.0
race Annotated[float, StateSpec()]

The index of the race.

0.0
gender Annotated[float, StateSpec()]

The index of the gender.

0.0
occupation Annotated[float, StateSpec()]

The index of the occupation.

0.0
age Annotated[float, StateSpec()]

The age in years.

0.0
convergence Annotated[float, ParameterSpec(domain=closed(0, 1))]

The immutable convergence parameter CR of the interactions between agents.

1.0
event_rate Annotated[float, ParameterSpec(domain=closed(0, 1))]

The convergence rate mu of the response to an event.

0.05
cell_size Annotated[float, ParameterSpec(domain=open(0, oo))]

The side of the grid cells of the space within which residents meet.

1000.0
partners Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The largest number of residents drawn from the grid cell as possible partners.

15.0

Examples:

>>> from skcomplex.core.components.elements import GroupElement
>>> from skcomplex.systems.social.organizations import ResidentElement
>>> GroupElement('residents', element=ResidentElement, size=10).size
10
Source code in src/skcomplex/systems/social/organizations/_kauai.py
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'sentiment', 'color_range': (-1.0, 1.0)}),
    *,
    sentiment: Annotated[float, StateSpec(domain=RealSet.closed(-1, 1))] = 0.0,
    weights: Annotated[npt.ArrayLike, StateSpec(vector=True)] = 0.0,
    home: Annotated[npt.ArrayLike, StateSpec(vector=True)] = (math.nan, math.nan),
    census: Annotated[float, StateSpec()] = 0.0,
    work: Annotated[float, StateSpec()] = 0.0,
    favourite: Annotated[float, StateSpec()] = 0.0,
    visits: Annotated[float, StateSpec()] = 0.0,
    race: Annotated[float, StateSpec()] = 0.0,
    gender: Annotated[float, StateSpec()] = 0.0,
    occupation: Annotated[float, StateSpec()] = 0.0,
    age: Annotated[float, StateSpec()] = 0.0,
    convergence: Annotated[float, ParameterSpec(domain=RealSet.closed(0, 1))] = 1.0,
    event_rate: Annotated[float, ParameterSpec(domain=RealSet.closed(0, 1))] = 0.05,
    cell_size: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1000.0,
    partners: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 15.0,
) -> None:
    super().__init__(label, visual=visual)
    self.sentiment = sentiment
    self.weights = weights
    self.home = home
    self.census = census
    self.work = work
    self.favourite = favourite
    self.visits = visits
    self.race = race
    self.gender = gender
    self.occupation = occupation
    self.age = age
    self.convergence = convergence
    self.event_rate = event_rate
    self.cell_size = cell_size
    self.partners = partners

ResidentView(agent, stepper)

Bases: AgentView

View of a resident of the Kauai population system, with its states and parameters as attributes.

Source code in src/skcomplex/core/engine/solvers/_mesa.py
559
560
561
def __init__(self, agent: mesa.Agent, stepper: 'MesaSolver') -> None:
    self._agent = agent
    self._stepper = stepper

RestaurantElement(label, visual=None, *, kind=0.0, sold=0.0, sold_high=0.0)

Bases: Element

Restaurant of the obesity system: a market at 0, a restaurant at 1, or a fast food outlet at 2.

Parameters:

Name Type Description Default
label str

The label of the agent or group.

required
visual VisualSpec | None

How the agent is drawn, or None for default_visual_.

None
kind Annotated[float, StateSpec(domain=NN)]

The kind of restaurant.

0.0
sold Annotated[float, StateSpec(domain=NN)]

The number of meals sold.

0.0
sold_high Annotated[float, StateSpec(domain=NN)]

The number of meals sold from the top half of the restaurant's calorie range.

0.0

Examples:

>>> from skcomplex.core.components.elements import GroupElement
>>> from skcomplex.systems.social.organizations import RestaurantElement
>>> group = GroupElement('restaurants', element=RestaurantElement, size=3, values={'kind': 2.0})
>>> group.label, group.element.__name__, group.size
('restaurants', 'RestaurantElement', 3)
Source code in src/skcomplex/systems/social/organizations/_obesity.py
341
342
343
344
345
346
347
348
349
350
351
352
353
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    kind: Annotated[float, StateSpec(domain=NN)] = 0.0,
    sold: Annotated[float, StateSpec(domain=NN)] = 0.0,
    sold_high: Annotated[float, StateSpec(domain=NN)] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.kind = kind
    self.sold = sold
    self.sold_high = sold_high

RestaurantView(agent, stepper)

Bases: AgentView

View of a restaurant of the obesity system, with its states and parameters as attributes.

Source code in src/skcomplex/core/engine/solvers/_mesa.py
559
560
561
def __init__(self, agent: mesa.Agent, stepper: 'MesaSolver') -> None:
    self._agent = agent
    self._stepper = stepper

SentimentInteraction(label, *, target, events=())

Bases: MapInteraction

Sentiment of every resident, which responds to events and to the partner the resident chooses at each step.

The rules are those of Zanbaka et al. in Carmichael et al. (2019 ch. 3 §4.3). An event is one or more entries (time, topic, weight), and it changes the sentiment S to S + sum(mu A_T (E_T - S)) over its topics, with mu the parameter event_rate of the resident, A_T its weight of the topic, and E_T the weight of the event (Eq. 5). The partner is the most likely of at most partners residents drawn from the grid cell of the resident, by P = DSS / 2 + STI / 2, with DSS the mean of five similarities and STI the share of topics both weigh (Eqs. 1 and 2). The two sentiments then change at once to a1 + mu1 (a2 - a1) and a2 + mu2 (a1 - a2), with mu = 1 - |a| CR of each and CR its parameter convergence (Eq. 4). The chapter prints no definition of the terms of DSS, so here race, gender, and occupation count 1 when equal and 0 otherwise, and age and sentiment count one minus their distance over their range. A sentiment stays between -1 and 1.

Parameters:

Name Type Description Default
label str

The label of the interaction, unique among the parts of its system.

required
target Annotated[str, RoleSpec()]

The path of the group of residents.

required
events Sequence[tuple[int, int, float]]

The entries of the events as (time, topic, weight).

()

Examples:

>>> from skcomplex.systems.social.organizations import SentimentInteraction
>>> SentimentInteraction('sentiment', target='residents', events=[(1, 0, 0.5)]).events
[(1, 0, 0.5)]
Source code in src/skcomplex/systems/social/organizations/_kauai.py
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
    events: Sequence[tuple[int, int, float]] = (),
) -> None:
    super().__init__(
        label,
        target=target,
        state='sentiment',
        function=self._update,
        schedule='member',
        form='agent',
    )
    self.events = events

StandoffElement(label, visual=None, *, stock=50.0, effort_a=0.0, effort_b=0.0, effort_c=0.0, goal_a=80.0, goal_b=20.0, goal_c=50.0, strength_a=0.1, strength_b=0.1, strength_c=0.1, intensification_time=10.0)

Bases: Element

Standoff element.

Source code in src/skcomplex/systems/social/organizations/_traps.py
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    stock: Annotated[object, StateSpec(quantity='stock')] = 50.0,
    effort_a: Annotated[object, StateSpec(quantity='effort')] = 0.0,
    effort_b: Annotated[object, StateSpec(quantity='effort')] = 0.0,
    effort_c: Annotated[object, StateSpec(quantity='effort')] = 0.0,
    goal_a: Annotated[object, ParameterSpec()] = 80.0,
    goal_b: Annotated[object, ParameterSpec()] = 20.0,
    goal_c: Annotated[object, ParameterSpec()] = 50.0,
    strength_a: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.1,
    strength_b: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.1,
    strength_c: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.1,
    intensification_time: Annotated[object, ParameterSpec(domain=RealSet.open(0, oo))] = 10.0,
) -> None:
    super().__init__(label, visual=visual)
    self.stock = stock
    self.effort_a = effort_a
    self.effort_b = effort_b
    self.effort_c = effort_c
    self.goal_a = goal_a
    self.goal_b = goal_b
    self.goal_c = goal_c
    self.strength_a = strength_a
    self.strength_b = strength_b
    self.strength_c = strength_c
    self.intensification_time = intensification_time

SupplierElement(label, visual=VisualSpec('agents', {'color_by': 'adopted', 'color_range': (0.0, 1.0)}), *, adopted=0.0, inflow=0.0, outflow=0.0)

Bases: Element

Firm of a supply chain, a buyer or a factory, with the value of the goods that flow into and out of it.

Parameters:

Name Type Description Default
label str

The label of the firm or the group.

required
visual VisualSpec | None

How the agent is drawn, or None for default_visual_.

VisualSpec('agents', {'color_by': 'adopted', 'color_range': (0.0, 1.0)})
adopted Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))]

Whether the firm has adopted good working conditions.

0.0
inflow Annotated[float, StateSpec(domain=closed_open(0, oo))]

The total value w_->i of the goods that flow into the firm from its suppliers.

0.0
outflow Annotated[float, StateSpec(domain=closed_open(0, oo))]

The total value w_j-> of the goods that flow out of the firm to its buyers.

0.0

Examples:

>>> from skcomplex.systems.social.organizations import SupplierElement
>>> SupplierElement('factory', outflow=6.0).outflow
6.0
Source code in src/skcomplex/systems/social/organizations/_supply_chains.py
57
58
59
60
61
62
63
64
65
66
67
68
69
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'adopted', 'color_range': (0.0, 1.0)}),
    *,
    adopted: Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))] = 0.0,
    inflow: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
    outflow: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.adopted = adopted
    self.inflow = inflow
    self.outflow = outflow

SupplierView(agent, stepper)

Bases: AgentView

View of a firm of the supply chain diffusion system, with its states as attributes.

Source code in src/skcomplex/core/engine/solvers/_mesa.py
559
560
561
def __init__(self, agent: mesa.Agent, stepper: 'MesaSolver') -> None:
    self._agent = agent
    self._stepper = stepper

TaskElement(label, visual=VisualSpec('histograms', {'color_by': 'age', 'shown': ('agents',)}), *, priority=0.5, age=0.0, wait=0.0, greed=0.99999)

Bases: Element

Task of the Barabási priority queue, with a priority, an age, and the waiting time when it was last executed.

Parameters:

Name Type Description Default
label str

The label of the agent or group.

required
visual VisualSpec | None

How the agent is drawn, or None for default_visual_.

VisualSpec('histograms', {'color_by': 'age', 'shown': ('agents',)})
priority Annotated[float, StateSpec(domain=closed_open(0, oo))]

The priority, uniform on 0 to 1 for a new task.

0.5
age Annotated[float, StateSpec(domain=NN)]

The number of steps the task has waited.

0.0
wait Annotated[float, StateSpec(domain=NN)]

The waiting time of the last task executed from this slot of the list.

0.0
greed Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The probability that the highest priority task is executed rather than a random one.

0.99999

Examples:

>>> from skcomplex.core.components.elements import GroupElement
>>> from skcomplex.systems.social.organizations import TaskElement
>>> group = GroupElement('tasks', element=TaskElement, size=2, values={'priority': [0.2, 0.7]})
>>> group.label, group.element.__name__, group.size
('tasks', 'TaskElement', 2)
Source code in src/skcomplex/systems/social/organizations/_queues.py
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('histograms', {'color_by': 'age', 'shown': ('agents',)}),
    *,
    priority: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.5,
    age: Annotated[float, StateSpec(domain=NN)] = 0.0,
    wait: Annotated[float, StateSpec(domain=NN)] = 0.0,
    greed: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.99999,
) -> None:
    super().__init__(label, visual=visual)
    self.priority = priority
    self.age = age
    self.wait = wait
    self.greed = greed

TaskView(agent, stepper)

Bases: AgentView

View of a task of the priority queue system, with its states and parameters as attributes.

Source code in src/skcomplex/core/engine/solvers/_mesa.py
559
560
561
def __init__(self, agent: mesa.Agent, stepper: 'MesaSolver') -> None:
    self._agent = agent
    self._stepper = stepper

TravellerElement(label, visual=VisualSpec('agents', {'color_by': 'victim', 'color_range': (0.0, 1.0)}), *, victim=0.0, trips=0.0)

Bases: Element

Woman of the brothel movement system: a trafficking victim, or a woman who travels freely.

Parameters:

Name Type Description Default
label str

The label of the agent or group.

required
visual VisualSpec | None

How the agent is drawn, or None for default_visual_.

VisualSpec('agents', {'color_by': 'victim', 'color_range': (0.0, 1.0)})
victim Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))]

Whether the woman is a trafficking victim, 1, or not, 0.

0.0
trips Annotated[float, StateSpec(domain=NN)]

The number of train trips between cities.

0.0

Examples:

>>> from skcomplex.core.components.elements import GroupElement
>>> from skcomplex.systems.social.organizations import TravellerElement
>>> group = GroupElement('women', element=TravellerElement, size=4, values={'victim': 1.0})
>>> group.label, group.element.__name__, group.size
('women', 'TravellerElement', 4)
Source code in src/skcomplex/systems/social/organizations/_trafficking.py
128
129
130
131
132
133
134
135
136
137
138
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'victim', 'color_range': (0.0, 1.0)}),
    *,
    victim: Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))] = 0.0,
    trips: Annotated[float, StateSpec(domain=NN)] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.victim = victim
    self.trips = trips

TravellerView(agent, stepper)

Bases: AgentView

View of a woman of the trafficking system, with its states and parameters as attributes.

Source code in src/skcomplex/core/engine/solvers/_mesa.py
559
560
561
def __init__(self, agent: mesa.Agent, stepper: 'MesaSolver') -> None:
    self._agent = agent
    self._stepper = stepper

WatchmakerElement(label, visual=None, *, progress=0.0, done=0.0, watches=0.0, task_size=10.0, tasks=111.0)

Bases: Element

Watchmaker who assembles a watch as a number of tasks of a number of actions each.

Parameters:

Name Type Description Default
label str

The label of the watchmaker or the group.

required
visual VisualSpec | None

How the agent is drawn, or None for default_visual_.

None
progress Annotated[float, StateSpec(domain=NN)]

The number of actions done on the task in hand, which an interruption undoes.

0.0
done Annotated[float, StateSpec(domain=NN)]

The number of stable tasks done on the watch in hand.

0.0
watches Annotated[float, StateSpec(domain=NN)]

The number of finished watches.

0.0
task_size Annotated[float, StateSpec(domain=NN)]

The number of actions of a task, the parts or subassemblies put together into a stable assembly.

10.0
tasks Annotated[float, StateSpec(domain=NN)]

The number of tasks of a watch.

111.0

Examples:

>>> from skcomplex.systems.social.organizations import WatchmakerElement
>>> WatchmakerElement('tempus', task_size=1000.0, tasks=1.0).task_size
1000.0
Source code in src/skcomplex/systems/social/organizations/_watchmakers.py
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    progress: Annotated[float, StateSpec(domain=NN)] = 0.0,
    done: Annotated[float, StateSpec(domain=NN)] = 0.0,
    watches: Annotated[float, StateSpec(domain=NN)] = 0.0,
    task_size: Annotated[float, StateSpec(domain=NN)] = 10.0,
    tasks: Annotated[float, StateSpec(domain=NN)] = 111.0,
) -> None:
    super().__init__(label, visual=visual)
    self.progress = progress
    self.done = done
    self.watches = watches
    self.task_size = task_size
    self.tasks = tasks

WatchmakerView(agent, stepper)

Bases: AgentView

View of a watchmaker of the Hora and Tempus fable, with its states and parameters as attributes.

Source code in src/skcomplex/core/engine/solvers/_mesa.py
559
560
561
def __init__(self, agent: mesa.Agent, stepper: 'MesaSolver') -> None:
    self._agent = agent
    self._stepper = stepper

WorkplaceElement(label, visual=None, *, colour=0, employer=0)

Bases: Element

Cell of the employment takeover system: an employer, white 0 or black 1, or a worker, red 0 or blue 1.

Employers and workers alternate like the squares of a checkerboard. Every step each worker first takes the colour of the employer to its right, at x + 1. Then each employer turns black when at least three of its four workers are blue, stays as it is when two are, and turns white otherwise. The two phases are local rule interactions of the lattice, which employment_takeover adds.

Parameters:

Name Type Description Default
label str

The label of the cell or the lattice group.

required
visual VisualSpec | None

How the agent is drawn, or None for default_visual_.

None
colour Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))]

The initial colour, 0 or 1.

0
employer Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))]

Whether the cell is an employer, 1, or a worker, 0.

0

Examples:

>>> from skcomplex.systems.social.organizations import WorkplaceElement
>>> WorkplaceElement('cell', employer=1).employer
1
Source code in src/skcomplex/systems/social/organizations/_employment.py
65
66
67
68
69
70
71
72
73
74
75
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    colour: Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))] = 0,
    employer: Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))] = 0,
) -> None:
    super().__init__(label, visual=visual)
    self.colour = colour
    self.employer = employer

ajanis_choice(n_employers=10, *, n_workers=500, asks=1, chat=3, adjustment=0.2, random_state=0)

Return Chesney's Ajani's Choice, migrant farm workers and employers among whom underpayment emerges.

Employers need between 50 and 99 workers a day each and start with a random wage between 1 and 2, the minimum wage being 1. Workers live in two slums, n_workers in each, and ask the employers of their local area for work every morning, accepting an offer below the minimum wage only when desperate, after a day without work at the minimum wage, or when no employer is left to ask. Every evening employers lower their wage if they filled their workforce and raise it otherwise, by adjustment, within [0.1, 2], and then average the wages of chat random employers (Chesney 2021 sections 3.3 and 3.8). A worker paid below the minimum wage is a slave, and a run lasts 60 days, two summer months. When workers outnumber the jobs, desperation drags the wages below the minimum, as with the default 10 employers and 1000 workers. The NetLogo listing of section 3.8 accepts a low offer when yesterday's pay was above the minimum, against rule 3 of the text, and the system follows the text.

Parameters:

Name Type Description Default
n_employers int

The number of employers.

10
n_workers int

The number of workers in each slum.

500
asks int

The number of employers a worker asks for work in a day.

1
chat int

The number of employers whose wages an employer averages in the evening.

3
adjustment float

The change of the wage of an employer after a day.

0.2
random_state int | Generator | None

The seed or generator of the employers.

0

Returns:

Name Type Description
system System

The system, with the groups employers and workers, and the reporters day.slaves and day.in_work.

Examples:

>>> from skcomplex.systems.social.organizations import ajanis_choice
>>> ajanis_choice(5, n_workers=10).elements[1].prepare().size_
20
Source code in src/skcomplex/systems/social/organizations/_wages.py
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
@register_system(
    field='social/organizations',
    tags=('agents', 'labour exploitation', 'wages', 'emergence'),
    references=('Chesney 2021', 'Chesney, Evans, Gold, and Trautrims 2018'),
    defaults={'until': 60},
)
def ajanis_choice(
    n_employers: int = 10,
    *,
    n_workers: int = 500,
    asks: int = 1,
    chat: int = 3,
    adjustment: float = 0.2,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return Chesney's Ajani's Choice, migrant farm workers and employers among whom underpayment emerges.

    Employers need between 50 and 99 workers a day each and start with a random wage between 1 and 2, the minimum
    wage being 1. Workers live in two slums, `n_workers` in each, and ask the employers of their local area for work
    every morning, accepting an offer below the minimum wage only when desperate, after a day without work at the
    minimum wage, or when no employer is left to ask. Every evening employers lower their wage if they filled their
    workforce and raise it otherwise, by `adjustment`, within `[0.1, 2]`, and then average the wages of `chat`
    random employers (Chesney 2021 sections 3.3 and 3.8). A worker paid below the minimum wage is a slave, and a run
    lasts 60 days, two summer months. When workers outnumber the jobs, desperation drags the wages below the
    minimum, as with the default 10 employers and 1000 workers. The NetLogo listing of section 3.8 accepts a low offer
    when yesterday's pay was above the minimum, against rule 3 of the text, and the system follows the text.

    Args:
        n_employers:
            The number of employers.

        n_workers:
            The number of workers in each slum.

        asks:
            The number of employers a worker asks for work in a day.

        chat:
            The number of employers whose wages an employer averages in the evening.

        adjustment:
            The change of the wage of an employer after a day.

        random_state:
            The seed or generator of the employers.

    Returns:
        system:
            The system, with the groups `employers` and `workers`, and the reporters `day.slaves` and `day.in_work`.

    Examples:
        >>> from skcomplex.systems.social.organizations import ajanis_choice
        >>> ajanis_choice(5, n_workers=10).elements[1].prepare().size_
        20
    """
    rng = np.random.default_rng(random_state)
    employers = GroupElement(
        'employers',
        element=FarmerElement,
        size=n_employers,
        values={
            'wage': 1.0 + rng.random(n_employers),
            'workforce': rng.integers(50, 100, size=n_employers).astype(float),
            'area': rng.integers(2, size=n_employers).astype(float),
        },
    )
    workers = GroupElement(
        'workers',
        element=HarvesterElement,
        size=2 * n_workers,
        values={'slum': np.repeat([0.0, 1.0], n_workers)},
    )
    day = HarvestInteraction(
        'day',
        employers='employers',
        workers='workers',
        asks=asks,
        chat=chat,
        adjustment=adjustment,
    )
    return System(elements=[employers, workers], interactions=[day])

basal_metabolic_rate(weight, height, age, male)

Return the daily basal metabolic rate of the revised Harris-Benedict equation, in kilocalories.

Parameters:

Name Type Description Default
weight float

The weight in kilograms.

required
height float

The height in centimetres.

required
age float

The age in years.

required
male float

1 for a man and 0 for a woman.

required

Returns:

Name Type Description
rate float

The basal metabolic rate per day.

Examples:

>>> from skcomplex.systems.social.organizations import basal_metabolic_rate
>>> round(basal_metabolic_rate(70.0, 175.0, 30.0, 1.0))
1696
Source code in src/skcomplex/systems/social/organizations/_obesity.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
def basal_metabolic_rate(weight: float, height: float, age: float, male: float) -> float:
    """Return the daily basal metabolic rate of the revised Harris-Benedict equation, in kilocalories.

    Args:
        weight:
            The weight in kilograms.

        height:
            The height in centimetres.

        age:
            The age in years.

        male:
            1 for a man and 0 for a woman.

    Returns:
        rate:
            The basal metabolic rate per day.

    Examples:
        >>> from skcomplex.systems.social.organizations import basal_metabolic_rate
        >>> round(basal_metabolic_rate(70.0, 175.0, 30.0, 1.0))
        1696
    """
    if male:
        return 88.362 + 13.397 * weight + 4.799 * height - 5.677 * age
    return 447.593 + 9.247 * weight + 3.098 * height - 4.330 * age

bonabeau_hierarchies(width=20, height=20, *, density=0.5, eta=1.0, relaxation=0.1, random_state=0)

Return the Bonabeau system, where a hierarchy of strengths emerges above a critical density of agents.

Parameters:

Name Type Description Default
width int

The number of columns of the torus.

20
height int

The number of rows of the torus.

20
density float

The share of cells with an agent.

0.5
eta float

The sensitivity of the outcome of a fight to the strength difference.

1.0
relaxation float

The fraction of its strength an agent loses every step.

0.1
random_state int | Generator | None

The seed or generator of the initial cells.

0

Returns:

Name Type Description
system System

The system, with the group contenders.

Examples:

>>> from skcomplex.systems.social.organizations import bonabeau_hierarchies
>>> bonabeau_hierarchies(10, 10, density=0.2).elements[0].prepare().size_
20
Source code in src/skcomplex/systems/social/organizations/_hierarchies.py
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
@register_system(
    field='social/organizations',
    tags=('hierarchy', 'agents', 'grid', 'phase transition'),
    references=('Bonabeau, Theraulaz, and Deneubourg 1995', 'Castellano, Fortunato, and Loreto 2009'),
    defaults={'until': 50},
)
def bonabeau_hierarchies(
    width: int = 20,
    height: int = 20,
    *,
    density: float = 0.5,
    eta: float = 1.0,
    relaxation: float = 0.1,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the Bonabeau system, where a hierarchy of strengths emerges above a critical density of agents.

    Args:
        width:
            The number of columns of the torus.

        height:
            The number of rows of the torus.

        density:
            The share of cells with an agent.

        eta:
            The sensitivity of the outcome of a fight to the strength difference.

        relaxation:
            The fraction of its strength an agent loses every step.

        random_state:
            The seed or generator of the initial cells.

    Returns:
        system:
            The system, with the group `contenders`.

    Examples:
        >>> from skcomplex.systems.social.organizations import bonabeau_hierarchies
        >>> bonabeau_hierarchies(10, 10, density=0.2).elements[0].prepare().size_
        20
    """
    size = int(width * height * density)
    cells = np.random.default_rng(random_state).permutation(width * height)[:size]
    contenders = GroupElement(
        'contenders',
        element=ContenderElement,
        size=size,
        values={'eta': eta, 'relaxation': relaxation},
        location=[(int(cell % width), int(cell // width)) for cell in cells],
    )
    return System(elements=[contenders], space=LatticeSpace((width, height), boundary='periodic', capacity=1))

container_mobility(n_individuals=10, *, levels=3, side=4, travel=(0.6, 0.3, 0.1), exponent=1.5)

Return the container model of human mobility of Alessandretti, Aslak, and Lehmann, trajectories across scales.

Space is a hierarchy of L levels of containers, a nested grid where every container holds side by side containers of the level below, down to the locations, and every container has an attractiveness among its siblings, drawn from a power law. An individual travels at the level-distance d, the highest level at which its origin and destination differ, with the probability p_d, chooses the destination container of level d among the other children of its container of level d + 1 by attractiveness, and descends by attractiveness to a location (arXiv 2109.07381 Methods, Generating traces). The nested grid, the power law of the attractivenesses, and the probabilities of travel that do not depend on the distance from home follow Supplementary Note 3. Displacements are a mixture of the scales of the containers, from one cell to side^(L - 1) cells.

Parameters:

Name Type Description Default
n_individuals int

The number of individuals.

10
levels int

The number L of levels.

3
side int

The number of children of a container along every axis.

4
travel Sequence[float]

The probabilities p_d of displacements at the level-distances 1 to L.

(0.6, 0.3, 0.1)
exponent float

The exponent of the power law of the attractivenesses.

1.5

Returns:

Name Type Description
system System

The system, with the group individuals.

Examples:

>>> from skcomplex.systems.social.organizations import container_mobility
>>> container_mobility(3).elements[0].prepare().size_
3
Source code in src/skcomplex/systems/social/organizations/_mobility.py
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
@register_system(
    field='social/organizations',
    tags=('agents', 'human mobility', 'hierarchy', 'containers', 'scales'),
    references=('Alessandretti, Aslak, and Lehmann 2020',),
    defaults={'until': 500},
)
def container_mobility(
    n_individuals: int = 10,
    *,
    levels: int = 3,
    side: int = 4,
    travel: Sequence[float] = (0.6, 0.3, 0.1),
    exponent: float = 1.5,
) -> System:
    """Return the container model of human mobility of Alessandretti, Aslak, and Lehmann, trajectories across scales.

    Space is a hierarchy of `L` levels of containers, a nested grid where every container holds `side` by `side`
    containers of the level below, down to the locations, and every container has an attractiveness among its siblings,
    drawn from a power law. An individual travels at the level-distance `d`, the highest level at which its origin and
    destination differ, with the probability `p_d`, chooses the destination container of level `d` among the other
    children of its container of level `d + 1` by attractiveness, and descends by attractiveness to a location
    (arXiv 2109.07381 Methods, Generating traces). The nested grid, the power law of the attractivenesses, and the
    probabilities of travel that do not depend on the distance from home follow Supplementary Note 3. Displacements are
    a mixture of the scales of the containers, from one cell to `side^(L - 1)` cells.

    Args:
        n_individuals:
            The number of individuals.

        levels:
            The number `L` of levels.

        side:
            The number of children of a container along every axis.

        travel:
            The probabilities `p_d` of displacements at the level-distances 1 to `L`.

        exponent:
            The exponent of the power law of the attractivenesses.

    Returns:
        system:
            The system, with the group `individuals`.

    Examples:
        >>> from skcomplex.systems.social.organizations import container_mobility
        >>> container_mobility(3).elements[0].prepare().size_
        3
    """
    individuals = GroupElement('individuals', element=ExplorerElement, size=n_individuals)
    displacement = ContainerMobilityInteraction(
        'displacement',
        target='individuals',
        levels=levels,
        side=side,
        travel=travel,
        exponent=exponent,
    )
    return System(elements=[individuals], interactions=[displacement])

drift_to_low_performance(performance=1.0, *, bias=0.1, perception_time=2.0, correction_time=2.0, goal_time=5.0, absolute=False)

Return Meadows' drift to low performance trap, eroding goals that follow a pessimistic view of the performance.

The actor compares the desired state with the actual performance and corrects the gap over the correction time, an ordinary balancing loop. The actor perceives the performance worse than it is, smoothed over the perception time and lowered by the negative bias, and the desired state slips toward the perceived state over the goal time. The lower the perceived state, the lower the goal, the less corrective action, and the lower the performance, a reinforcing loop that drags the performance down exponentially. With absolute standards, absolute=True, the desired state does not slip and the performance holds, and with an upbeat bias, a negative bias, the same structure drifts toward high performance, the two ways out of the book. The book gives the structure only, as a narrative, so the parameters are a documented choice.

Parameters:

Name Type Description Default
performance float

The initial performance, also the initial perceived and desired states.

1.0
bias float

The fraction by which the actor underrates the performance, negative for an upbeat bias.

0.1
perception_time float

The time over which the actor perceives the performance.

2.0
correction_time float

The time over which the corrective action closes the gap to the desired state.

2.0
goal_time float

The time over which the desired state follows the perceived state.

5.0
absolute bool

Whether the desired state is kept absolute, regardless of the performance.

False

Returns:

Name Type Description
system System

The system, with the performance performance.performance, the perceived state performance.perceived, and the desired state performance.desired, and the flows performance.correction, performance.perception, and performance.goal_erosion. The observable of the system is performance.discrepancy.

Examples:

>>> from skcomplex.systems.social.organizations import drift_to_low_performance
>>> drift_to_low_performance(absolute=True).elements[0].erosion
0.0
Source code in src/skcomplex/systems/social/organizations/_traps.py
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
@register_system(
    field='social/organizations',
    tags=('system traps', 'eroding goals', 'stocks and flows', 'system dynamics', 'ode'),
    references=('Meadows 2008',),
    defaults={'until': 100.0, 'dt': 0.5},
    observables=lambda system: [ObservableSpec('discrepancy', _discrepancy, element='performance')],
)
def drift_to_low_performance(
    performance: float = 1.0,
    *,
    bias: float = 0.1,
    perception_time: float = 2.0,
    correction_time: float = 2.0,
    goal_time: float = 5.0,
    absolute: bool = False,
) -> System:
    """Return Meadows' drift to low performance trap, eroding goals that follow a pessimistic view of the performance.

    The actor compares the desired state with the actual performance and corrects the gap over the correction time,
    an ordinary balancing loop. The actor perceives the performance worse than it is, smoothed over the perception
    time and lowered by the negative bias, and the desired state slips toward the perceived state over the goal time.
    The lower the perceived state, the lower the goal, the less corrective action, and the lower the performance, a
    reinforcing loop that drags the performance down exponentially. With absolute standards, `absolute=True`, the
    desired state does not slip and the performance holds, and with an upbeat bias, a negative `bias`, the same
    structure drifts toward high performance, the two ways out of the book. The book gives the structure only, as a
    narrative, so the parameters are a documented choice.

    Args:
        performance:
            The initial performance, also the initial perceived and desired states.

        bias:
            The fraction by which the actor underrates the performance, negative for an upbeat bias.

        perception_time:
            The time over which the actor perceives the performance.

        correction_time:
            The time over which the corrective action closes the gap to the desired state.

        goal_time:
            The time over which the desired state follows the perceived state.

        absolute:
            Whether the desired state is kept absolute, regardless of the performance.

    Returns:
        system:
            The system, with the performance `performance.performance`, the perceived state `performance.perceived`,
            and the desired state `performance.desired`, and the flows `performance.correction`,
            `performance.perception`, and `performance.goal_erosion`. The observable of the system is
            `performance.discrepancy`.

    Examples:
        >>> from skcomplex.systems.social.organizations import drift_to_low_performance
        >>> drift_to_low_performance(absolute=True).elements[0].erosion
        0.0
    """
    element = PerformanceElement(
        'performance',
        performance=performance,
        perceived=performance,
        desired=performance,
        bias=bias,
        perception_time=perception_time,
        correction_time=correction_time,
        goal_time=goal_time,
        erosion=0.0 if absolute else 1.0,
    )
    flows = TransferInteraction(
        'flows',
        source='performance',
        target='performance',
        state=Source('effort'),
        into='performance',
        amount=_correction,
        flow='correction',
        legs=[
            (Source('information'), 'perceived', _perception, 'perception'),
            (Source('standards'), 'desired', _goal_erosion, 'goal_erosion'),
        ],
    )
    return System(elements=[element], interactions=[flows])

employment_takeover(size=32, *, random_state=0)

Return the employment takeover system on a periodic checkerboard, with half of each kind coloured at random.

Parameters:

Name Type Description Default
size int

The even number of rows and columns.

32
random_state int | Generator | None

The seed or generator of the cells coloured 1.

0

Returns:

Name Type Description
system System

The system, with the colours cells.colour and the employers cells.employer.

Examples:

>>> from skcomplex.systems.social.organizations import employment_takeover
>>> employment_takeover(4).elements[0].prepare().size_
16
Source code in src/skcomplex/systems/social/organizations/_employment.py
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
@register_system(
    field='social/organizations',
    tags=('lattice', 'employment', 'takeover', 'deterministic'),
    references=('Chesney 2021',),
    defaults={'until': 50, 'dt': 1},
)
def employment_takeover(size: int = 32, *, random_state: int | np.random.Generator | None = 0) -> System:
    """Return the employment takeover system on a periodic checkerboard, with half of each kind coloured at random.

    Args:
        size:
            The even number of rows and columns.

        random_state:
            The seed or generator of the cells coloured 1.

    Returns:
        system:
            The system, with the colours `cells.colour` and the employers `cells.employer`.

    Examples:
        >>> from skcomplex.systems.social.organizations import employment_takeover
        >>> employment_takeover(4).elements[0].prepare().size_
        16
    """
    rng = np.random.default_rng(random_state)
    rows, columns = np.indices((size, size))
    employers = ((rows + columns) % 2 == 0).astype(float)
    colours = np.zeros((size, size))
    for kind in (1.0, 0.0):
        cells = np.flatnonzero(employers.ravel() == kind)
        colours.ravel()[rng.choice(cells, len(cells) // 2, replace=False)] = 1.0
    cells = LatticeElement(
        'cells',
        element=WorkplaceElement,
        space=LatticeSpace((size, size)),
        values={'colour': colours, 'employer': employers},
    )
    phases = [
        LocalRuleInteraction(
            label,
            target='cells',
            state='colour',
            rule=rule,
            neighborhood=NEIGHBORS,
            reads=('colour', 'employer'),
        )
        for label, rule in (('workers', _follow_employer), ('employers', _follow_workers))
    ]
    return System(elements=[cells], interactions=phases)

escalation(a=10.0, b=10.5, *, margin=0.1, adjustment_time=2.0, perception_delay=1.0)

Return Meadows' escalation trap, two parties that each want to exceed the other's stock by a margin.

Each party sees the stock of the other through an information smoothing over the perception delay, wants to hold 1 + margin times that stock, and builds up the gap over the adjustment time. The loop through both stocks is reinforcing, so both stocks grow exponentially at the rate r that solves r * adjustment_time = (1 + margin) / (1 + r * perception_delay) - 1. The book gives the structure only, so the parameters are a documented choice.

Parameters:

Name Type Description Default
a float

The initial stock of the first party.

10.0
b float

The initial stock of the second party.

10.5
margin float

The fraction by which each party wants to exceed the other.

0.1
adjustment_time float

The time over which a party closes the gap to its desired stock.

2.0
perception_delay float

The time over which a party perceives the stock of the other.

1.0

Returns:

Name Type Description
system System

The system, with the stocks race.a and race.b in years, and the flows race.build_a and race.build_b. The observables of the system are race.desired_a and race.desired_b.

Examples:

>>> from skcomplex.systems.social.organizations import escalation
>>> escalation(margin=0.2).elements[0].margin
0.2
Source code in src/skcomplex/systems/social/organizations/_traps.py
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
@register_system(
    field='social/organizations',
    tags=('system traps', 'arms race', 'stocks and flows', 'system dynamics', 'ode'),
    references=('Meadows 2008',),
    defaults={'until': 100.0, 'dt': 0.5},
    observables=lambda system: [
        ObservableSpec('desired_a', _desired_a, element='race'),
        ObservableSpec('desired_b', _desired_b, element='race'),
    ],
)
def escalation(
    a: float = 10.0,
    b: float = 10.5,
    *,
    margin: float = 0.1,
    adjustment_time: float = 2.0,
    perception_delay: float = 1.0,
) -> System:
    """Return Meadows' escalation trap, two parties that each want to exceed the other's stock by a margin.

    Each party sees the stock of the other through an information smoothing over the perception delay, wants to hold
    `1 + margin` times that stock, and builds up the gap over the adjustment time. The loop through both stocks is
    reinforcing, so both stocks grow exponentially at the rate `r` that solves
    `r * adjustment_time = (1 + margin) / (1 + r * perception_delay) - 1`. The book gives the structure only, so the
    parameters are a documented choice.

    Args:
        a:
            The initial stock of the first party.

        b:
            The initial stock of the second party.

        margin:
            The fraction by which each party wants to exceed the other.

        adjustment_time:
            The time over which a party closes the gap to its desired stock.

        perception_delay:
            The time over which a party perceives the stock of the other.

    Returns:
        system:
            The system, with the stocks `race.a` and `race.b` in years, and the flows `race.build_a` and
            `race.build_b`. The observables of the system are `race.desired_a` and `race.desired_b`.

    Examples:
        >>> from skcomplex.systems.social.organizations import escalation
        >>> escalation(margin=0.2).elements[0].margin
        0.2
    """
    race = ArmsRaceElement(
        'race',
        a=a,
        b=b,
        margin=margin,
        adjustment_time=adjustment_time,
        perception_delay=perception_delay,
    )
    build = TransferInteraction(
        'build',
        source='race',
        target='race',
        state=Source('production'),
        into='a',
        amount=_build_a,
        flow='build_a',
        legs=[(Source('production'), 'b', _build_b, 'build_b')],
    )
    return System(elements=[race], interactions=[build])

exploration_preferential_return(n_individuals=10, *, rho=0.6, gamma=0.21, beta=1.55, least_jump=1.0)

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.

Parameters:

Name Type Description Default
n_individuals int

The number of individuals.

10
rho float

The factor rho of the probability of exploring.

0.6
gamma float

The exponent gamma of the decay of the probability of exploring.

0.21
beta float

The exponent beta of the power law of the jump lengths, above 1.

1.55
least_jump float

The least jump length.

1.0

Returns:

Name Type Description
system 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
Source code in src/skcomplex/systems/social/organizations/_mobility.py
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
@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])

gravity_urban_scaling(population=400, *, alpha=1.0, gamma=1.0, random_state=0)

Return the residents of a city at uniform positions in a disc, each interacting with the others as r^-gamma.

The disc has the area N^alpha, so the total interaction of a city grows as N^(2 - alpha gamma / 2) for gamma < 2, which with the population dimension D_P = 2 / alpha is the exponent 2 - gamma / D_P of Ribeiro and Netto (2024 §4). An Experiment sweeps population and fits the exponent with ScalingAnalysis(kind='power').

Parameters:

Name Type Description Default
population int

The number of residents N.

400
alpha float

The exponent of the area of the city with its population.

1.0
gamma float

The decay exponent of an interaction with the distance.

1.0
random_state int | Generator | None

The seed or generator of the positions.

0

Returns:

Name Type Description
system System

The system, with the states residents.x and residents.contacts and the observable total_contacts of the catalog, which a run passes in observables.

Examples:

>>> from skcomplex.systems.social.organizations import gravity_urban_scaling
>>> gravity_urban_scaling(100).elements[0].size
100
Source code in src/skcomplex/systems/social/organizations/_urban_scaling.py
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
@register_system(
    field='social/organizations',
    tags=('urban scaling', 'gravity system', 'static', 'allometry'),
    references=('Ribeiro and Netto 2024, arXiv 2404.02642', 'Ribeiro et al. 2017'),
    observables=_total_contacts,
)
def gravity_urban_scaling(
    population: int = 400,
    *,
    alpha: float = 1.0,
    gamma: float = 1.0,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the residents of a city at uniform positions in a disc, each interacting with the others as `r^-gamma`.

    The disc has the area `N^alpha`, so the total interaction of a city grows as `N^(2 - alpha gamma / 2)` for
    `gamma < 2`, which with the population dimension `D_P = 2 / alpha` is the exponent `2 - gamma / D_P` of Ribeiro and
    Netto (2024 §4). An `Experiment` sweeps `population` and fits the exponent with `ScalingAnalysis(kind='power')`.

    Args:
        population:
            The number of residents `N`.

        alpha:
            The exponent of the area of the city with its population.

        gamma:
            The decay exponent of an interaction with the distance.

        random_state:
            The seed or generator of the positions.

    Returns:
        system:
            The system, with the states `residents.x` and `residents.contacts` and the observable `total_contacts` of
            the catalog, which a run passes in `observables`.

    Examples:
        >>> from skcomplex.systems.social.organizations import gravity_urban_scaling
        >>> gravity_urban_scaling(100).elements[0].size
        100
    """
    radius = np.sqrt(population**alpha / np.pi)
    rng = np.random.default_rng(random_state)
    distances = radius * np.sqrt(rng.uniform(size=population))
    angles = rng.uniform(0.0, 2 * np.pi, population)
    positions = radius + distances[:, None] * np.column_stack([np.cos(angles), np.sin(angles)])
    residents = GroupElement('residents', element=CityResidentElement, size=population, values={'x': positions})
    contact = GravityContactInteraction('contact', target='residents', reach=2.0 * radius, gamma=gamma)
    space = ContinuousSpace(extent=[2.0 * radius, 2.0 * radius])
    return System(elements=[residents], interactions=[contact], space=space, time=StaticTime())

insider_sabotage(size=20, *, n_employees=130, n_supervisors=3, turnover=0.15, random_state=0)

Return the insider sabotage system of employees whose risk of sabotage builds up over the days of a year.

Parameters:

Name Type Description Default
size int

The number of rows and columns of the torus.

20
n_employees int

The number of employees.

130
n_supervisors int

The number of supervisors.

3
turnover float

The yearly share of employees who leave.

0.15
random_state int | Generator | None

The seed or generator of the predispositions and affects.

0

Returns:

Name Type Description
system System

The system, with the groups employees and supervisors.

Examples:

>>> from skcomplex.systems.social.organizations import insider_sabotage
>>> [group.size for group in insider_sabotage(n_employees=20, n_supervisors=2).elements]
[20, 2]
Source code in src/skcomplex/systems/social/organizations/_sabotage.py
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
@register_system(
    field='social/organizations',
    tags=('organizations', 'agents', 'grid', 'insider threat'),
    references=('Ardiles Cruz et al. 2019', 'Carmichael et al. 2019'),
    defaults={'until': 365},
)
def insider_sabotage(
    size: int = 20,
    *,
    n_employees: int = 130,
    n_supervisors: int = 3,
    turnover: float = 0.15,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the insider sabotage system of employees whose risk of sabotage builds up over the days of a year.

    Args:
        size:
            The number of rows and columns of the torus.

        n_employees:
            The number of employees.

        n_supervisors:
            The number of supervisors.

        turnover:
            The yearly share of employees who leave.

        random_state:
            The seed or generator of the predispositions and affects.

    Returns:
        system:
            The system, with the groups `employees` and `supervisors`.

    Examples:
        >>> from skcomplex.systems.social.organizations import insider_sabotage
        >>> [group.size for group in insider_sabotage(n_employees=20, n_supervisors=2).elements]
        [20, 2]
    """
    rng = np.random.default_rng(random_state)
    predisposed = (rng.random(n_employees) < PREDISPOSED).astype(float)
    affect = rng.choice(AFFECTS, n_employees, p=AFFECT_SHARES)
    employees = GroupElement(
        'employees',
        element=EmployeeElement,
        size=n_employees,
        values={'predisposed': predisposed, 'affect': affect, 'risk': predisposed + affect, 'turnover': turnover},
    )
    supervisors = GroupElement('supervisors', element=Element, size=n_supervisors)
    wandering = MovementInteraction('wandering', target='supervisors', schedule='member')
    maps = [
        ('feeling', _feel, 'replace'),
        ('first', _remember_first, 'replace'),
        ('disgruntlement', _disgruntle, 'replace'),
        ('days', _count_day, 'increment'),
        ('mean_feeling', _average, 'increment'),
        ('expectation', _expect, 'replace'),
        ('stress', _stress, 'increment'),
        ('skill', _learn, 'increment'),
    ]
    work = [
        MovementInteraction('working', target='employees', schedule='member'),
        *(
            MapInteraction(
                state,
                target='employees',
                state=state,
                function=function,
                mode=mode,
                schedule='member',
                form='agent',
            )
            for state, function, mode in maps
        ),
        LifeCycleInteraction('turnover', target='employees', death=_leave, renew=True, values=_hire, schedule='member'),
        MapInteraction('risk', target='employees', state='risk', function=_sum_risk, schedule='member', form='agent'),
    ]
    return System(
        elements=[employees, supervisors],
        interactions=[wandering, *work],
        space=LatticeSpace((size, size), boundary='periodic', neighborhood='von_neumann'),
    )

kauai_population(populations=None, *, layers=None, n_topics=2, weighted_share=0.5, events=((1, 0, 0.8),), convergence=1.0, event_rate=0.05, cell_size=1000.0, partners=15, random_state=0)

Return one agent per resident of the census regions of a map, moving between home, work, and favourite places.

Every resident starts at a uniform point of its census region, its home, and has a facility of work and a favourite place drawn uniformly. It commutes between the three in a cycle of three steps. Its sentiment follows the rules of Zanbaka et al. in Carmichael et al. (2019 ch. 3 §4.3): the response to events (Eq. 5) and the interaction with a partner it chooses from its grid cell (Eqs. 1, 2, and 4), through SentimentInteraction. The default map is a synthetic island, and the occupations follow Table 1 of the chapter. This system leaves out the base security interactions (Eq. 7), physical security information, adverse agents, and the many favourite places and the weekly cycle of the chapter. The chapter gives the 15 partners, the grid of 1 km, and the event rate 0.05 of its example, and it gives no value of CR, the initial sentiments, the topic weights, or the demographics, which are choices of this system.

Parameters:

Name Type Description Default
populations Sequence[int] | None

The number of residents of every census region, or None for the populations of the synthetic island.

None
layers Mapping[str, Sequence[Polygon]] | None

The polygons of the layers boundary, census, places, and facilities in non-negative metric coordinates, or None for the synthetic island.

None
n_topics int

The number of topics.

2
weighted_share float

The share of residents that weight a topic above zero.

0.5
events Sequence[tuple[int, int, float]]

The entries of the events as (time, topic, weight), with the weight in [-1, 1]. Entries of one time form one event.

((1, 0, 0.8),)
convergence float

The convergence parameter CR of every resident, in mu = 1 - |a| CR.

1.0
event_rate float

The convergence rate mu of the response to an event.

0.05
cell_size float

The side of the grid cells within which residents meet, in the units of the coordinates.

1000.0
partners int

The largest number of residents drawn from the grid cell as possible partners, where 0 turns the interaction off.

15
random_state int | Generator | None

The seed or generator of the initial sentiments, the weights, the demographics, the work, and the favourite places.

0

Returns:

Name Type Description
system System

The system, with the group residents, its states sentiment, weights, home, census, work, favourite, visits, race, gender, occupation, and age, and the region layers in its space.

Raises:

Type Description
ValidationError

The populations do not name one number per census region.

Examples:

>>> from skcomplex.systems.social.organizations import kauai_population
>>> kauai_population((3, 2, 0)).elements[0].size
5
Source code in src/skcomplex/systems/social/organizations/_kauai.py
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
@register_system(
    field='social/organizations',
    tags=('agents', 'continuous space', 'region layers', 'census', 'sentiment'),
    references=('Carmichael et al. 2019',),
    defaults={'until': 30},
    observables=_mean_sentiment,
)
def kauai_population(
    populations: Sequence[int] | None = None,
    *,
    layers: Mapping[str, Sequence[Polygon]] | None = None,
    n_topics: int = 2,
    weighted_share: float = 0.5,
    events: Sequence[tuple[int, int, float]] = ((1, 0, 0.8),),
    convergence: float = 1.0,
    event_rate: float = 0.05,
    cell_size: float = 1000.0,
    partners: int = 15,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return one agent per resident of the census regions of a map, moving between home, work, and favourite places.

    Every resident starts at a uniform point of its census region, its home, and has a facility of work and a favourite
    place drawn uniformly. It commutes between the three in a cycle of three steps. Its sentiment follows the rules of
    Zanbaka et al. in Carmichael et al. (2019 ch. 3 §4.3): the response to events (Eq. 5) and the interaction with a
    partner it chooses from its grid cell (Eqs. 1, 2, and 4), through `SentimentInteraction`. The default map is a
    synthetic island, and the occupations follow Table 1 of the chapter. This system leaves out the base security
    interactions (Eq. 7), physical security information, adverse agents, and the many favourite places and the weekly
    cycle of the chapter. The chapter gives the 15 partners, the grid of 1 km, and the event rate 0.05 of its example,
    and it gives no value of `CR`, the initial sentiments, the topic weights, or the demographics, which are choices of
    this system.

    Args:
        populations:
            The number of residents of every census region, or `None` for the populations of the synthetic island.

        layers:
            The polygons of the layers `boundary`, `census`, `places`, and `facilities` in non-negative metric
            coordinates, or `None` for the synthetic island.

        n_topics:
            The number of topics.

        weighted_share:
            The share of residents that weight a topic above zero.

        events:
            The entries of the events as `(time, topic, weight)`, with the weight in `[-1, 1]`. Entries of one time
            form one event.

        convergence:
            The convergence parameter `CR` of every resident, in `mu = 1 - |a| CR`.

        event_rate:
            The convergence rate `mu` of the response to an event.

        cell_size:
            The side of the grid cells within which residents meet, in the units of the coordinates.

        partners:
            The largest number of residents drawn from the grid cell as possible partners, where 0 turns the interaction
            off.

        random_state:
            The seed or generator of the initial sentiments, the weights, the demographics, the work, and the favourite
            places.

    Returns:
        system:
            The system, with the group `residents`, its states `sentiment`, `weights`, `home`, `census`, `work`,
            `favourite`, `visits`, `race`, `gender`, `occupation`, and `age`, and the region layers in its space.

    Raises:
        ValidationError:
            The populations do not name one number per census region.

    Examples:
        >>> from skcomplex.systems.social.organizations import kauai_population
        >>> kauai_population((3, 2, 0)).elements[0].size
        5
    """
    regions = load_island_layers() if layers is None else layers
    counts = load_island_populations() if populations is None else populations
    if len(counts) != len(regions['census']):
        error_msg = f'The populations {counts} need one number for each of the {len(regions["census"])} census regions.'
        raise ValidationError(error_msg)
    rng = np.random.default_rng(random_state)
    census = np.repeat(np.arange(len(counts)), counts)
    size = len(census)
    shares = np.array(OCCUPATION_SHARES) / sum(OCCUPATION_SHARES)
    residents = GroupElement(
        'residents',
        element=ResidentElement,
        size=size,
        values={
            'sentiment': rng.uniform(-0.2, 0.2, size),
            'weights': _get_weights(size, n_topics, weighted_share, rng),
            'home': np.full((size, 2), np.nan),
            'census': census.astype(float),
            'work': rng.integers(len(regions['facilities']), size=size).astype(float),
            'favourite': rng.integers(len(regions['places']), size=size).astype(float),
            'race': rng.integers(RACES, size=size).astype(float),
            'gender': rng.integers(GENDERS, size=size).astype(float),
            'occupation': rng.choice(len(shares), size=size, p=shares).astype(float),
            'age': rng.uniform(0.0, OLDEST_AGE, size),
            'convergence': convergence,
            'event_rate': event_rate,
            'cell_size': cell_size,
            'partners': float(partners),
        },
        location=[regions['census'][region] for region in census.tolist()],
    )
    bounds = shapely.total_bounds([polygon for polygons in regions.values() for polygon in polygons])
    interactions = [
        CommuteInteraction('commute', target='residents'),
        PlaceVisitsInteraction('visits', target='residents'),
        SentimentInteraction('sentiment', target='residents', events=events),
    ]
    space = ContinuousSpace(extent=(float(bounds[2]), float(bounds[3])), regions=regions)
    return System(elements=[residents], interactions=interactions, space=space)

load_island_layers()

Return the polygons of every layer of the synthetic island, in a metric coordinate system.

Returns:

Name Type Description
layers dict[str, list[Polygon]]

The polygons of the layers boundary, census, places, and facilities, by the name of the layer.

Examples:

>>> from skcomplex.systems.social.organizations import load_island_layers
>>> {layer: len(polygons) for layer, polygons in load_island_layers().items()}
{'boundary': 1, 'census': 3, 'places': 3, 'facilities': 2}
Source code in src/skcomplex/systems/social/organizations/_kauai.py
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
def load_island_layers() -> dict[str, list[Polygon]]:
    """Return the polygons of every layer of the synthetic island, in a metric coordinate system.

    Returns:
        layers:
            The polygons of the layers `boundary`, `census`, `places`, and `facilities`, by the name of the layer.

    Examples:
        >>> from skcomplex.systems.social.organizations import load_island_layers
        >>> {layer: len(polygons) for layer, polygons in load_island_layers().items()}
        {'boundary': 1, 'census': 3, 'places': 3, 'facilities': 2}
    """
    return {
        layer: [shape(item['geometry']) for item in json.loads(_read_layer(layer))['features']]
        for layer in ISLAND_LAYERS
    }

load_island_populations()

Return the number of residents of every region of the census layer of the synthetic island.

Returns:

Name Type Description
populations list[int]

The population of every census region, in the order of the regions of the layer.

Examples:

>>> from skcomplex.systems.social.organizations import load_island_populations
>>> load_island_populations()
[100, 50, 0]
Source code in src/skcomplex/systems/social/organizations/_kauai.py
373
374
375
376
377
378
379
380
381
382
383
384
385
def load_island_populations() -> list[int]:
    """Return the number of residents of every region of the census layer of the synthetic island.

    Returns:
        populations:
            The population of every census region, in the order of the regions of the layer.

    Examples:
        >>> from skcomplex.systems.social.organizations import load_island_populations
        >>> load_island_populations()
        [100, 50, 0]
    """
    return [int(item['properties']['population']) for item in json.loads(_read_layer('census'))['features']]

obesity(n_people=100, *, n_places=20, size=280.0, reach=30.0, activity=1.55, random_state=0)

Return the obesity system of adults who eat near home and work, one step per day.

Homes, workplaces, and restaurants of three kinds lie at random, and the eating habits are split equally. At home, at work, and at home again a person eats one meal at a random restaurant within reach, or at the nearest restaurant when none is within reach, so that access changes what is eaten but never skips a meal.

Parameters:

Name Type Description Default
n_people int

The number of people.

100
n_places int

The number of homes, of workplaces, and of restaurants.

20
size float

The side of the square the places lie in.

280.0
reach float

The distance within which a person looks for a restaurant.

30.0
activity float

The factor of the basal metabolic rate a person spends a day, 1.55 for moderate activity.

1.55
random_state int | Generator | None

The seed or generator of the places and the people.

0

Returns:

Name Type Description
system System

The system, with the groups people and restaurants.

Examples:

>>> from skcomplex.systems.social.organizations import obesity
>>> [group.size for group in obesity(10, n_places=4, random_state=0).elements]
[10, 4]
Source code in src/skcomplex/systems/social/organizations/_obesity.py
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
@register_system(
    field='social/organizations',
    tags=('health', 'agents', 'continuous space', 'obesity'),
    references=('Lynch et al. 2019', 'Carmichael et al. 2019'),
    defaults={'until': 70},
)
def obesity(
    n_people: int = 100,
    *,
    n_places: int = 20,
    size: float = 280.0,
    reach: float = 30.0,
    activity: float = 1.55,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the obesity system of adults who eat near home and work, one step per day.

    Homes, workplaces, and restaurants of three kinds lie at random, and the eating habits are split equally. At home,
    at work, and at home again a person eats one meal at a random restaurant within `reach`, or at the nearest
    restaurant when none is within reach, so that access changes what is eaten but never skips a meal.

    Args:
        n_people:
            The number of people.

        n_places:
            The number of homes, of workplaces, and of restaurants.

        size:
            The side of the square the places lie in.

        reach:
            The distance within which a person looks for a restaurant.

        activity:
            The factor of the basal metabolic rate a person spends a day, 1.55 for moderate activity.

        random_state:
            The seed or generator of the places and the people.

    Returns:
        system:
            The system, with the groups `people` and `restaurants`.

    Examples:
        >>> from skcomplex.systems.social.organizations import obesity
        >>> [group.size for group in obesity(10, n_places=4, random_state=0).elements]
        [10, 4]
    """
    rng = np.random.default_rng(random_state)
    homes, works = rng.uniform(0, size, (2, n_places, 2))
    home, work = homes[rng.integers(n_places, size=n_people)], works[rng.integers(n_places, size=n_people)]
    male = (rng.random(n_people) < MALE_SHARE).astype(float)
    height = np.where(male > 0, rng.normal(176, 7, n_people), rng.normal(163, 7, n_people))
    weight = rng.normal(24, 3, n_people) * (height / 100) ** 2
    people = GroupElement(
        'people',
        element=EaterElement,
        size=n_people,
        values={
            'home': home,
            'work': work,
            'weight': weight,
            'height': height,
            'age': rng.uniform(18, 65, n_people),
            'male': male,
            'habit': np.arange(n_people) % 3,
            'bmi': weight / (height / 100) ** 2,
            'reach': reach,
            'activity': activity,
        },
        location=home,
    )
    restaurants = GroupElement(
        'restaurants',
        element=RestaurantElement,
        size=n_places,
        values={'kind': rng.integers(0, 3, n_places).astype(float)},
        location=rng.uniform(0, size, (n_places, 2)),
    )
    interactions: list[BaseInteraction] = []
    for phase, step in (
        ('home', EaterElement.go_home),
        ('work', EaterElement.go_to_work),
        ('evening', EaterElement.go_home),
    ):
        interactions += [
            MovementInteraction(f'go_{phase}', target='people', step=step, phase=phase),
            TransferInteraction(
                f'meal_{phase}',
                source='people',
                target='restaurants',
                state='calories',
                into='sold',
                amount=EaterElement.draw_meal,
                pairing=PartnerSpec('reach', fallback='nearest'),
                form='agent',
                phase=phase,
                settlement=EaterElement.settle_meal,
            ),
        ]
    phases = (PhaseSpec('home'), PhaseSpec('work'), PhaseSpec('evening'), PhaseSpec('week', period=DAYS_PER_WEEK))
    return System(
        elements=[people, restaurants],
        interactions=interactions,
        space=ContinuousSpace(extent=(size, size)),
        time=DiscreteTime(schedule=PhasesScheduleSpec(phases)),
    )

policy_resistance(stock=50.0, *, goals=(80.0, 20.0, 50.0), strengths=(0.1, 0.1, 0.1), intensification_time=10.0)

Return Meadows' policy resistance trap, one stock pulled by three actors toward inconsistent goals.

Every actor compares the stock with its own goal and pulls the stock toward it, in proportion to the discrepancy and to its strength, and intensifies its effort while the discrepancy persists, the ratchet of the book. The book's example is the drug supply on the streets, which addicts want high, enforcement agencies want low, and pushers want in the middle. The countermoves produce a standoff: the stock settles at the mean of the goals weighted by the strengths, which none of the actors wants, while the efforts of the actors above and below it grow without bound. When one actor lets up, its strength set to zero, the others drag the stock toward their own goals. The book gives the structure only, as a narrative, so the corrective rules and the parameters are a documented choice.

Parameters:

Name Type Description Default
stock float

The initial stock.

50.0
goals tuple[float, float, float]

The goals of the three actors.

(80.0, 20.0, 50.0)
strengths tuple[float, float, float]

The rates at which the three actors pull the stock toward their goals, per unit of discrepancy.

(0.1, 0.1, 0.1)
intensification_time float

The time over which an actor doubles its effort against a persistent discrepancy.

10.0

Returns:

Name Type Description
system System

The system, with the stock standoff.stock and the efforts standoff.effort_a, standoff.effort_b, and standoff.effort_c, and the flows standoff.push, standoff.intensify_a, standoff.intensify_b, and standoff.intensify_c. The observable of the system is standoff.compromise.

Examples:

>>> from skcomplex.systems.social.organizations import policy_resistance
>>> policy_resistance(strengths=(0.1, 0.0, 0.1)).elements[0].strength_b
0.0
Source code in src/skcomplex/systems/social/organizations/_traps.py
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
@register_system(
    field='social/organizations',
    tags=('system traps', 'policy resistance', 'stocks and flows', 'system dynamics', 'ode'),
    references=('Meadows 2008',),
    defaults={'until': 100.0, 'dt': 0.5},
    observables=lambda system: [ObservableSpec('compromise', _compromise, element='standoff')],
)
def policy_resistance(
    stock: float = 50.0,
    *,
    goals: tuple[float, float, float] = (80.0, 20.0, 50.0),
    strengths: tuple[float, float, float] = (0.1, 0.1, 0.1),
    intensification_time: float = 10.0,
) -> System:
    """Return Meadows' policy resistance trap, one stock pulled by three actors toward inconsistent goals.

    Every actor compares the stock with its own goal and pulls the stock toward it, in proportion to the discrepancy
    and to its strength, and intensifies its effort while the discrepancy persists, the ratchet of the book. The book's
    example is the drug supply on the streets, which addicts want high, enforcement agencies want low, and pushers want
    in the middle. The countermoves produce a standoff: the stock settles at the mean of the goals weighted by the
    strengths, which none of the actors wants, while the efforts of the actors above and below it grow without bound.
    When one actor lets up, its strength set to zero, the others drag the stock toward their own goals. The book gives
    the structure only, as a narrative, so the corrective rules and the parameters are a documented choice.

    Args:
        stock:
            The initial stock.

        goals:
            The goals of the three actors.

        strengths:
            The rates at which the three actors pull the stock toward their goals, per unit of discrepancy.

        intensification_time:
            The time over which an actor doubles its effort against a persistent discrepancy.

    Returns:
        system:
            The system, with the stock `standoff.stock` and the efforts `standoff.effort_a`, `standoff.effort_b`, and
            `standoff.effort_c`, and the flows `standoff.push`, `standoff.intensify_a`, `standoff.intensify_b`, and
            `standoff.intensify_c`. The observable of the system is `standoff.compromise`.

    Examples:
        >>> from skcomplex.systems.social.organizations import policy_resistance
        >>> policy_resistance(strengths=(0.1, 0.0, 0.1)).elements[0].strength_b
        0.0
    """
    standoff = StandoffElement(
        'standoff',
        stock=stock,
        goal_a=goals[0],
        goal_b=goals[1],
        goal_c=goals[2],
        strength_a=strengths[0],
        strength_b=strengths[1],
        strength_c=strengths[2],
        intensification_time=intensification_time,
    )
    actions = TransferInteraction(
        'actions',
        source='standoff',
        target='standoff',
        state=Source('actors'),
        into='stock',
        amount=_push,
        flow='push',
        legs=[
            (Source('actors'), 'effort_a', _intensify_a, 'intensify_a'),
            (Source('actors'), 'effort_b', _intensify_b, 'intensify_b'),
            (Source('actors'), 'effort_c', _intensify_c, 'intensify_c'),
        ],
    )
    return System(elements=[standoff], interactions=[actions])

priority_queues(n_tasks=2, *, greed=0.99999, random_state=0)

Return the Barabási priority queue, whose waiting times follow a power law as the choice becomes greedy.

Parameters:

Name Type Description Default
n_tasks int

The length of the list of tasks.

2
greed float

The probability that the highest priority task is executed rather than a random one.

0.99999
random_state int | Generator | None

The seed or generator of the initial priorities.

0

Returns:

Name Type Description
system System

The system, with the group tasks and the execution execution, which reports the waiting time of the task it executes at every step.

Examples:

>>> from skcomplex.systems.social.organizations import priority_queues
>>> priority_queues(3, random_state=0).elements[0].prepare().size_
3
Source code in src/skcomplex/systems/social/organizations/_queues.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
@register_system(
    field='social/organizations',
    tags=('queues', 'agents', 'bursts', 'power law'),
    references=('Barabási 2005', 'Castellano, Fortunato, and Loreto 2009'),
    defaults={'until': 200},
)
def priority_queues(
    n_tasks: int = 2,
    *,
    greed: float = 0.99999,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the Barabási priority queue, whose waiting times follow a power law as the choice becomes greedy.

    Args:
        n_tasks:
            The length of the list of tasks.

        greed:
            The probability that the highest priority task is executed rather than a random one.

        random_state:
            The seed or generator of the initial priorities.

    Returns:
        system:
            The system, with the group `tasks` and the execution `execution`, which reports the waiting time of the
            task it executes at every step.

    Examples:
        >>> from skcomplex.systems.social.organizations import priority_queues
        >>> priority_queues(3, random_state=0).elements[0].prepare().size_
        3
    """
    priorities = np.random.default_rng(random_state).random(n_tasks)
    tasks = GroupElement('tasks', element=TaskElement, size=n_tasks, values={'priority': priorities, 'greed': greed})
    return System(elements=[tasks], interactions=[ExecutionInteraction('execution', target='tasks')])

shifting_the_burden(capability=0.5, intervention=0.0, *, desired=1.0, fix_rate=1.0, build_time=5.0, solution_delay=2.0, erosion=0.5)

Return Meadows' shifting the burden trap, a quick fix that relieves a symptom and erodes the system's capability.

The symptom is the gap between the desired state and the state that the capability of the system and an outside intervention deliver together. The intervention grows quickly with the symptom. The capability grows slowly with the symptom, perceived over the solution delay, and the intervention erodes it as a side effect. With a quick fix the intervention takes over and the capability withers away, an addiction. Without one, fix_rate = 0, the capability grows to the desired state. The book gives the structure only, so the parameters are a documented choice.

Parameters:

Name Type Description Default
capability float

The initial capability of the system to deliver the desired state on its own.

0.5
intervention float

The initial level of the intervention.

0.0
desired float

The desired state.

1.0
fix_rate float

The rate at which the intervention grows per unit of symptom, 0 for no quick fix.

1.0
build_time float

The time the fundamental solution needs to close a perceived symptom.

5.0
solution_delay float

The time over which the fundamental solution perceives the symptom.

2.0
erosion float

The rate at which a unit of intervention erodes the capability.

0.5

Returns:

Name Type Description
system System

The system, with the capability burden.capability and the intervention burden.intervention in years, the flows burden.fixing, burden.building, and burden.erosion. The observable of the system is burden.symptom.

Examples:

>>> from skcomplex.systems.social.organizations import shifting_the_burden
>>> shifting_the_burden(fix_rate=0.0).elements[0].fix_rate
0.0
Source code in src/skcomplex/systems/social/organizations/_traps.py
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
@register_system(
    field='social/organizations',
    tags=('system traps', 'addiction', 'stocks and flows', 'system dynamics', 'ode'),
    references=('Meadows 2008',),
    defaults={'until': 60.0, 'dt': 0.5},
    observables=lambda system: [ObservableSpec('symptom', _symptom, element='burden')],
)
def shifting_the_burden(
    capability: float = 0.5,
    intervention: float = 0.0,
    *,
    desired: float = 1.0,
    fix_rate: float = 1.0,
    build_time: float = 5.0,
    solution_delay: float = 2.0,
    erosion: float = 0.5,
) -> System:
    """Return Meadows' shifting the burden trap, a quick fix that relieves a symptom and erodes the system's capability.

    The symptom is the gap between the desired state and the state that the capability of the system and an outside
    intervention deliver together. The intervention grows quickly with the symptom. The capability grows slowly with
    the symptom, perceived over the solution delay, and the intervention erodes it as a side effect. With a quick fix
    the intervention takes over and the capability withers away, an addiction. Without one, `fix_rate = 0`, the
    capability grows to the desired state. The book gives the structure only, so the parameters are a documented
    choice.

    Args:
        capability:
            The initial capability of the system to deliver the desired state on its own.

        intervention:
            The initial level of the intervention.

        desired:
            The desired state.

        fix_rate:
            The rate at which the intervention grows per unit of symptom, 0 for no quick fix.

        build_time:
            The time the fundamental solution needs to close a perceived symptom.

        solution_delay:
            The time over which the fundamental solution perceives the symptom.

        erosion:
            The rate at which a unit of intervention erodes the capability.

    Returns:
        system:
            The system, with the capability `burden.capability` and the intervention `burden.intervention` in years,
            the flows `burden.fixing`, `burden.building`, and `burden.erosion`. The observable of the system is
            `burden.symptom`.

    Examples:
        >>> from skcomplex.systems.social.organizations import shifting_the_burden
        >>> shifting_the_burden(fix_rate=0.0).elements[0].fix_rate
        0.0
    """
    burden = BurdenElement(
        'burden',
        capability=capability,
        intervention=intervention,
        desired=desired,
        fix_rate=fix_rate,
        build_time=build_time,
        solution_delay=solution_delay,
        erosion=erosion,
    )
    flows = TransferInteraction(
        'flows',
        source='burden',
        target='burden',
        state=Source('intervener'),
        into='intervention',
        amount=_fixing,
        flow='fixing',
        legs=[
            (Source('learning'), 'capability', _building, 'building'),
            ('capability', Sink('atrophy'), _erosion, 'erosion'),
        ],
    )
    return System(elements=[burden], interactions=[flows])

success_to_the_successful(a=55.0, b=45.0, *, budget=10.0, lifetime=10.0)

Return Meadows' success to the successful trap, two competitors that share a budget by their past success.

Each competitor holds a stock of resources that its share of the budget fills and that depreciates over its lifetime. The share of the first competitor is a table function of its success, its fraction of all resources, that gives more than a proportional share to the more successful one. The two reinforcing loops make the symmetric state unstable, so the competitor that starts larger ends with the whole budget and the other one disappears. The book gives the structure only, so the table and the parameters are a documented choice.

Parameters:

Name Type Description Default
a float

The initial resources of the first competitor.

55.0
b float

The initial resources of the second competitor.

45.0
budget float

The resources allocated per year.

10.0
lifetime float

The average lifetime of resources, in years.

10.0

Returns:

Name Type Description
system System

The system, with the resources contest.a and contest.b in years, the flows contest.allocation_a, contest.allocation_b, contest.depreciation_a, and contest.depreciation_b. The observables of the system are contest.success and contest.allocation.

Examples:

>>> from skcomplex.systems.social.organizations import success_to_the_successful
>>> success_to_the_successful(a=60.0).elements[0].a
60.0
Source code in src/skcomplex/systems/social/organizations/_traps.py
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
@register_system(
    field='social/organizations',
    tags=('system traps', 'competitive exclusion', 'stocks and flows', 'system dynamics', 'ode'),
    references=('Meadows 2008',),
    defaults={'until': 100.0, 'dt': 0.5},
    observables=lambda system: [
        ObservableSpec('success', _success, element='contest'),
        ObservableSpec('allocation', _allocation, element='contest'),
    ],
)
def success_to_the_successful(
    a: float = 55.0,
    b: float = 45.0,
    *,
    budget: float = 10.0,
    lifetime: float = 10.0,
) -> System:
    """Return Meadows' success to the successful trap, two competitors that share a budget by their past success.

    Each competitor holds a stock of resources that its share of the budget fills and that depreciates over its
    lifetime. The share of the first competitor is a table function of its success, its fraction of all resources,
    that gives more than a proportional share to the more successful one. The two reinforcing loops make the symmetric
    state unstable, so the competitor that starts larger ends with the whole budget and the other one disappears. The
    book gives the structure only, so the table and the parameters are a documented choice.

    Args:
        a:
            The initial resources of the first competitor.

        b:
            The initial resources of the second competitor.

        budget:
            The resources allocated per year.

        lifetime:
            The average lifetime of resources, in years.

    Returns:
        system:
            The system, with the resources `contest.a` and `contest.b` in years, the flows `contest.allocation_a`,
            `contest.allocation_b`, `contest.depreciation_a`, and `contest.depreciation_b`. The observables of the
            system are `contest.success` and `contest.allocation`.

    Examples:
        >>> from skcomplex.systems.social.organizations import success_to_the_successful
        >>> success_to_the_successful(a=60.0).elements[0].a
        60.0
    """
    allocation = TransferInteraction(
        'allocation',
        source='contest',
        target='contest',
        state=Source('budget'),
        into='a',
        amount=_allocation_a,
        flow='allocation_a',
        legs=[
            (Source('budget'), 'b', _allocation_b, 'allocation_b'),
            ('a', Sink('depreciation'), _depreciation_a, 'depreciation_a'),
            ('b', Sink('depreciation'), _depreciation_b, 'depreciation_b'),
        ],
    )
    return System(
        elements=[ContestElement('contest', a=a, b=b, budget=budget, lifetime=lifetime)],
        interactions=[allocation],
    )

supply_chain_diffusion(graph=None, *, buyers=3, factories=40, seeds=2, random_state=0)

Return Chesney's Namrata's Family, buyers who push good working conditions down their supply network.

Goods flow from suppliers to buyers along the weighted edges of a directed network, w_ji from j to i. A buyer i that has adopted forces a supplier j to adopt when it is less beholden to the supplier than the supplier is to it, V_ij = w_ji / w_->i < V_ji = w_ji / w_j->, where w_->i is the total value that flows into i and w_j-> the total value that flows out of j, with a strict inequality since factories resist by default (Chesney 2021 sections 4.9 and 4.10). A seed group of buyers adopts at the start, and in every time period the firms that adopted in the previous one push their suppliers. Since the condition reads w_->i > w_j->, a buyer with a single supplier never forces it, so diffusion needs buyers with two or more suppliers (section 4.11). The default network has buyers buyers and factories factories, every factory supplying one to three random firms listed before it with values from 1 to 10.

Parameters:

Name Type Description Default
graph object

A NetworkX directed graph of the supply network with the values of the goods as the edge attribute weight, or None for a random network.

None
buyers int

The number of buyers, the first nodes, of the default network.

3
factories int

The number of factories of the default network.

40
seeds int

The number of the first nodes that adopt at the start.

2
random_state int | Generator | None

The seed or generator of the default network.

0

Returns:

Name Type Description
system System

The system, with the group firms on the nodes of the network.

Examples:

>>> from skcomplex.systems.social.organizations import supply_chain_diffusion
>>> supply_chain_diffusion(factories=10).elements[0].prepare().size_
13
Source code in src/skcomplex/systems/social/organizations/_supply_chains.py
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
@register_system(
    field='social/organizations',
    tags=('agents', 'network', 'supply chains', 'diffusion', 'labour standards'),
    references=('Chesney 2021', 'Gold, Chesney, Gruchmann, and Trautrims 2020'),
    defaults={'until': 10},
)
def supply_chain_diffusion(
    graph: object = None,
    *,
    buyers: int = 3,
    factories: int = 40,
    seeds: int = 2,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return Chesney's Namrata's Family, buyers who push good working conditions down their supply network.

    Goods flow from suppliers to buyers along the weighted edges of a directed network, `w_ji` from `j` to `i`. A buyer
    `i` that has adopted forces a supplier `j` to adopt when it is less beholden to the supplier than the supplier is
    to it, `V_ij = w_ji / w_->i < V_ji = w_ji / w_j->`, where `w_->i` is the total value that flows into `i` and
    `w_j->` the total value that flows out of `j`, with a strict inequality since factories resist by default
    (Chesney 2021 sections 4.9 and 4.10). A seed group of buyers adopts at the start, and in every time period the
    firms that adopted in the previous one push their suppliers. Since the condition reads `w_->i > w_j->`, a buyer
    with a single supplier never forces it, so diffusion needs buyers with two or more suppliers (section 4.11). The
    default network has `buyers` buyers and `factories` factories, every factory supplying one to three random firms
    listed before it with values from 1 to 10.

    Args:
        graph:
            A NetworkX directed graph of the supply network with the values of the goods as the edge attribute
            `weight`, or `None` for a random network.

        buyers:
            The number of buyers, the first nodes, of the default network.

        factories:
            The number of factories of the default network.

        seeds:
            The number of the first nodes that adopt at the start.

        random_state:
            The seed or generator of the default network.

    Returns:
        system:
            The system, with the group `firms` on the nodes of the network.

    Examples:
        >>> from skcomplex.systems.social.organizations import supply_chain_diffusion
        >>> supply_chain_diffusion(factories=10).elements[0].prepare().size_
        13
    """
    rng = np.random.default_rng(random_state)
    network = _supply_network(buyers, factories, rng) if graph is None else graph
    if not isinstance(network, nx.DiGraph):
        network = nx.DiGraph(network)
    nodes = list(network.nodes)
    adopted = np.zeros(len(nodes))
    adopted[:seeds] = 1.0
    firms = GroupElement(
        'firms',
        element=SupplierElement,
        size=len(nodes),
        values={
            'adopted': adopted,
            'inflow': [float(network.in_degree(node, weight='weight')) for node in nodes],
            'outflow': [float(network.out_degree(node, weight='weight')) for node in nodes],
        },
    )
    adoption = MapInteraction('adoption', target='firms', state='adopted', function=_adopt, form='agent')
    return System(elements=[firms], interactions=[adoption], space=NetworkSpace(network))

trafficking(width=30, height=30, *, n_cities=3, n_houses=40, n_brothels=3, n_victims=12, n_free=30, random_state=0)

Return the brothel movement system, where victims moved between brothels leave traces in bills and train trips.

Parameters:

Name Type Description Default
width int

The number of columns of the grid.

30
height int

The number of rows of the grid.

30
n_cities int

The number of cities, vertical bands of equal width.

3
n_houses int

The number of houses.

40
n_brothels int

The number of brothels.

3
n_victims int

The number of victims, spread over the brothels.

12
n_free int

The number of women who travel freely.

30
random_state int | Generator | None

The seed or generator of the places, the women, and the efficiencies.

0

Returns:

Name Type Description
system System

The system, with the groups premises and women.

Examples:

>>> from skcomplex.systems.social.organizations import trafficking
>>> [group.size for group in trafficking(n_houses=5, n_brothels=2, n_victims=4, n_free=3).elements]
[7, 7]
Source code in src/skcomplex/systems/social/organizations/_trafficking.py
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
@register_system(
    field='social/organizations',
    tags=('trafficking', 'agents', 'grid', 'crime'),
    references=('Chesney 2021',),
    defaults={'until': 90},
)
def trafficking(
    width: int = 30,
    height: int = 30,
    *,
    n_cities: int = 3,
    n_houses: int = 40,
    n_brothels: int = 3,
    n_victims: int = 12,
    n_free: int = 30,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the brothel movement system, where victims moved between brothels leave traces in bills and train trips.

    Args:
        width:
            The number of columns of the grid.

        height:
            The number of rows of the grid.

        n_cities:
            The number of cities, vertical bands of equal width.

        n_houses:
            The number of houses.

        n_brothels:
            The number of brothels.

        n_victims:
            The number of victims, spread over the brothels.

        n_free:
            The number of women who travel freely.

        random_state:
            The seed or generator of the places, the women, and the efficiencies.

    Returns:
        system:
            The system, with the groups `premises` and `women`.

    Examples:
        >>> from skcomplex.systems.social.organizations import trafficking
        >>> [group.size for group in trafficking(n_houses=5, n_brothels=2, n_victims=4, n_free=3).elements]
        [7, 7]
    """
    rng = np.random.default_rng(random_state)
    n_places = n_houses + n_brothels
    cells = rng.permutation(width * height)[:n_places]
    places = [(int(cell % width), int(cell // width)) for cell in cells]
    brothel = (np.arange(n_places) >= n_houses).astype(float)
    premises = GroupElement(
        'premises',
        element=PremisesElement,
        size=n_places,
        values={'brothel': brothel, 'efficiency': rng.uniform(0, 3, n_places)},
        location=places,
    )
    homes = [places[n_houses + index % n_brothels] for index in range(n_victims)]
    homes += [(int(rng.integers(width)), int(rng.integers(height))) for _ in range(n_free)]
    victim = (np.arange(n_victims + n_free) < n_victims).astype(float)
    women = GroupElement(
        'women',
        element=TravellerElement,
        size=n_victims + n_free,
        values={'victim': victim},
        location=homes,
    )
    day = DailyMovementsInteraction(
        'day',
        women='women',
        premises='premises',
        n_cities=n_cities,
        phase=DAILY,
        victims_phase=MONTHLY,
    )
    bills = [
        MapInteraction(
            'days',
            target='premises',
            state='day',
            function=_count_day,
            mode='increment',
            form='agent',
            phase=BILLING,
        ),
        MapInteraction(
            'bills',
            target='premises',
            state='bills',
            function=_heat,
            sources=['women'],
            mode='increment',
            form='agent',
            phase=BILLING,
        ),
        MapInteraction(
            'record',
            target='premises',
            state='last_bills',
            function=_record,
            form='agent',
            phase=QUARTERLY,
        ),
        MapInteraction('reset', target='premises', state='bills', function=_reset, form='agent', phase=QUARTERLY),
    ]
    phases = [
        PhaseSpec(DAILY, activation='sequential'),
        PhaseSpec(MONTHLY, activation='sequential', period=MONTH),
        PhaseSpec(BILLING, activation='sequential'),
        PhaseSpec(QUARTERLY, activation='sequential', period=QUARTER),
    ]
    return System(
        elements=[premises, women],
        interactions=[day, *bills],
        space=LatticeSpace((width, height), boundary='fixed'),
        time=DiscreteTime(schedule=PhasesScheduleSpec(phases)),
    )

urban_scaling_interactions(population=300, *, edges=2, internal=0.002, meeting=0.5, exponent=2.5, temperature=0.2, random_state=0)

Return a city that grows by residents and internal links, so its meetings grow faster than its population.

Every step a resident joins at a uniform angle and adds edges links to residents drawn in proportion to the Fermi-Dirac probability of their hyperbolic distance, as in popularity-similarity growth (Papadopoulos et al. 2012). Every resident also adds an internal link with the probability internal per step, toward the same weight, so the links per step and the mean degree grow with the size. This rate per resident is the assumption of this system, since Papadopoulos et al. (2015) add a fixed number of internal links per step and keep the mean degree constant. Every resident meets each neighbour with the probability meeting per step. The growth and the internal links stop when the city holds population residents, so one run time reaches every size. An Experiment sweeps population, and ScalingAnalysis(kind='allometric') fits the exponent of the meetings, which Ribeiro and Netto (2024) review near 1.15 for socioeconomic quantities, and which the rate internal sets here.

Parameters:

Name Type Description Default
population int

The number of residents the city holds room for.

300
edges int

The number of links of every new resident, and one less than the residents of the complete seed city.

2
internal float

The probability per step that a resident adds an internal link.

0.002
meeting float

The probability per step that a resident meets one neighbour.

0.5
exponent float

The exponent of the distribution of the links of the growth without internal links, above 2.

2.5
temperature float

The temperature of the network, between 0 and 1.

0.2
random_state int | Generator | None

The seed or generator of the angles of the seed city.

0

Returns:

Name Type Description
system System

The system, with the groups residents and links, the states residents.angle, residents.age, residents.degree, and residents.meetings, and the observable total_meetings of the catalog, which a run passes in observables.

Raises:

Type Description
ValidationError

The exponent is not above 2, or the temperature is not between 0 and 1.

Examples:

>>> from skcomplex.core.engine.runtime import Simulation
>>> from skcomplex.systems.social.organizations import urban_scaling_interactions
>>> system = urban_scaling_interactions(13, internal=0.0)
>>> trajectory = Simulation(system, random_state=0).prepare().run(until=10).trajectory_
>>> int(trajectory.network(-1).sources.size)
23
Source code in src/skcomplex/systems/social/organizations/_urban_interactions.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
@register_system(
    field='social/organizations',
    tags=('urban scaling', 'network', 'growth', 'hyperbolic', 'allometry'),
    references=(
        'Ribeiro and Netto 2024, arXiv 2404.02642',
        'Papadopoulos et al. 2012, arXiv 1106.0286',
        'Papadopoulos et al. 2015',
    ),
    observables=_total_meetings,
    defaults={'until': 297},
)
def urban_scaling_interactions(
    population: int = 300,
    *,
    edges: int = 2,
    internal: float = 0.002,
    meeting: float = 0.5,
    exponent: float = 2.5,
    temperature: float = 0.2,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return a city that grows by residents and internal links, so its meetings grow faster than its population.

    Every step a resident joins at a uniform angle and adds `edges` links to residents drawn in proportion to the
    Fermi-Dirac probability of their hyperbolic distance, as in popularity-similarity growth (Papadopoulos et al.
    2012). Every resident also adds an internal link with the probability `internal` per step, toward the same weight,
    so the links per step and the mean degree grow with the size. This rate per resident is the assumption of this
    system, since Papadopoulos et al. (2015) add a fixed number of internal links per step and keep the mean degree
    constant.
    Every resident meets each neighbour with the probability `meeting` per step. The growth and the internal links stop
    when the city holds `population` residents, so one run time reaches every size. An `Experiment` sweeps `population`,
    and `ScalingAnalysis(kind='allometric')` fits the exponent of the meetings, which Ribeiro and Netto (2024) review
    near 1.15 for socioeconomic quantities, and which the rate `internal` sets here.

    Args:
        population:
            The number of residents the city holds room for.

        edges:
            The number of links of every new resident, and one less than the residents of the complete seed city.

        internal:
            The probability per step that a resident adds an internal link.

        meeting:
            The probability per step that a resident meets one neighbour.

        exponent:
            The exponent of the distribution of the links of the growth without internal links, above 2.

        temperature:
            The temperature of the network, between 0 and 1.

        random_state:
            The seed or generator of the angles of the seed city.

    Returns:
        system:
            The system, with the groups `residents` and `links`, the states `residents.angle`, `residents.age`,
            `residents.degree`, and `residents.meetings`, and the observable `total_meetings` of the catalog, which a
            run passes in `observables`.

    Raises:
        ValidationError:
            The exponent is not above 2, or the temperature is not between 0 and 1.

    Examples:
        >>> from skcomplex.core.engine.runtime import Simulation
        >>> from skcomplex.systems.social.organizations import urban_scaling_interactions
        >>> system = urban_scaling_interactions(13, internal=0.0)
        >>> trajectory = Simulation(system, random_state=0).prepare().run(until=10).trajectory_
        >>> int(trajectory.network(-1).sources.size)
        23
    """
    if exponent <= MIN_EXPONENT:
        error_msg = f'The degree exponent of the city is {exponent!r}. Give an exponent above {MIN_EXPONENT}.'
        raise ValidationError(error_msg)
    if not 0 < temperature < 1:
        error_msg = f'The temperature of the city is {temperature!r}. Give a temperature between 0 and 1.'
        raise ValidationError(error_msg)
    seed_nodes = edges + 1
    fading = 1 / (exponent - 1)
    arrivals = seed_nodes + 1

    def closeness(node: SimpleNamespace) -> object:
        return _get_closeness(node, arrivals, edges, fading, temperature)

    angles = np.random.default_rng(random_state).uniform(0.0, 2 * np.pi, seed_nodes)
    ages = np.arange(seed_nodes - 1, -1, -1, dtype=float)
    residents = GroupElement(
        'residents',
        element=CityDwellerElement,
        on='sites',
        variable=True,
        capacity=population,
        values={'age': ages.tolist(), 'angle': angles.tolist()},
    )
    capacity = edges * population + seed_nodes**2 + int(np.ceil(internal * population**2))
    links = GroupElement('links', element=Element, on='edges', variable=True, capacity=capacity)
    growth = TopologyInteraction(
        'growth',
        action='add-node',
        edges=edges,
        condition=lambda s: s.t < population - seed_nodes,
        values={'angle': lambda rng: rng.uniform(0.0, 2 * np.pi)},
        toward=closeness,
    )
    bridging = TopologyInteraction(
        'bridging',
        target='residents',
        action='add-edge',
        probability=internal,
        condition=lambda s: s.t < population - seed_nodes,
        toward=closeness,
    )
    ageing = MapInteraction('ageing', target='residents', state='age', function=lambda s: 1, mode='increment')
    degree = MapInteraction('degree', target='residents', state='degree', function=lambda s: neighbors(1))
    meetings = MapInteraction(
        'meetings',
        target='residents',
        state='meetings',
        function=lambda s: draw('meet', 'binom', n=s.target.degree, p=meeting),
    )
    return System(
        elements=[residents, links],
        interactions=[growth, bridging, ageing, degree, meetings],
        space=NetworkSpace(nx.complete_graph(seed_nodes)),
        time=DiscreteTime(),
    )

watchmakers(parts=1000, *, subassembly=10, interruption=0.01)

Return Simon's fable of the watchmakers Hora and Tempus, told by Meadows, with and without stable subassemblies.

Both watchmakers build watches of parts parts. Tempus puts all of them together at once, so an interruption, a customer's call, makes the unfinished watch fall to pieces. Hora puts together stable subassemblies of subassembly elements, then assemblies of those, up to the whole watch, so an interruption loses only the assembly in hand. Hora has more actions to take, parts - 1 divided by subassembly - 1 tasks of subassembly actions each, 111 tasks of 10 actions for 1000 parts. A task of k actions interrupted with the probability p at every action takes ((1 - p)^-k - 1) / p actions on average, so at p = 0.01 Hora finishes a watch every 1173 steps and Tempus one every 2.3 million steps. The book tells the fable as a narrative, so the step of one action and the interruption at every action follow Simon's original telling.

Parameters:

Name Type Description Default
parts int

The number of parts of a watch, a power of subassembly.

1000
subassembly int

The number of elements of a stable assembly of Hora.

10
interruption float

The probability that a watchmaker is interrupted at an action.

0.01

Returns:

Name Type Description
system System

The system, with the group watchmakers, Hora first and Tempus second.

Examples:

>>> from skcomplex.systems.social.organizations import watchmakers
>>> watchmakers().elements[0].prepare().size_
2
Source code in src/skcomplex/systems/social/organizations/_watchmakers.py
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
@register_system(
    field='social/organizations',
    tags=('hierarchy', 'agents', 'stable intermediate forms', 'interruptions'),
    references=('Simon 1962', 'Meadows 2008'),
    defaults={'until': 5000},
)
def watchmakers(
    parts: int = 1000,
    *,
    subassembly: int = 10,
    interruption: float = 0.01,
) -> System:
    """Return Simon's fable of the watchmakers Hora and Tempus, told by Meadows, with and without stable subassemblies.

    Both watchmakers build watches of `parts` parts. Tempus puts all of them together at once, so an interruption,
    a customer's call, makes the unfinished watch fall to pieces. Hora puts together stable subassemblies of
    `subassembly` elements, then assemblies of those, up to the whole watch, so an interruption loses only the
    assembly in hand. Hora has more actions to take, `parts - 1` divided by `subassembly - 1` tasks of `subassembly`
    actions each, 111 tasks of 10 actions for 1000 parts. A task of `k` actions interrupted with the probability `p` at
    every action takes `((1 - p)^-k - 1) / p` actions on average, so at `p = 0.01` Hora finishes a watch every 1173
    steps and Tempus one every 2.3 million steps. The book tells the fable as a narrative, so the step of one action
    and the interruption at every action follow Simon's original telling.

    Args:
        parts:
            The number of parts of a watch, a power of `subassembly`.

        subassembly:
            The number of elements of a stable assembly of Hora.

        interruption:
            The probability that a watchmaker is interrupted at an action.

    Returns:
        system:
            The system, with the group `watchmakers`, Hora first and Tempus second.

    Examples:
        >>> from skcomplex.systems.social.organizations import watchmakers
        >>> watchmakers().elements[0].prepare().size_
        2
    """
    hora_tasks = (parts - 1) // (subassembly - 1)
    group = GroupElement(
        'watchmakers',
        element=WatchmakerElement,
        size=2,
        values={'task_size': [float(subassembly), float(parts)], 'tasks': [float(hora_tasks), 1.0]},
    )
    return System(
        elements=[group],
        interactions=[AssemblyInteraction('assembly', target='watchmakers', interruption=interruption)],
    )