Sign in to see your models

Save models to your account and pick up where you left off on any device.

Sign-in is temporarily unavailable. Please try again later.

MathPets language

Graph

Links connect pairs of pets. Like patches and pets they are agents, with their own breeds, fields, and update blocks.

Chapter

Graph

Open the matching course chapter

Links as agents

Links are agents in the same sense as patches and pets: they have fields, belong to named breeds, can be counted and filtered, and have their own update blocks. In addition, each link has two endpoint pets and a numeric weight.

A model without a links: section has no links. Library models use them for contact networks (Virus on a Network), directed flows (Diffusion on a Directed Network), and the rods and springs of the mechanics library (Double Pendulum).

pets:  people:    state: susceptible | infected | recovered = susceptible links:  link contacts undirected:    state: calm | exposed = calm monitors:  contact-count: number = count contacts  exposed-count: number = count contacts where exposed

Link breeds

Declare a link breed inside links: with link breed-name <direction>:. The direction is undirected (the default) or directed. An undirected link is symmetric; a directed link runs from a source endpoint to a target.

Link fields are declared like pet fields, with a type and an initial value. The link breed name is an agentset: you can count contacts, filter with contacts where (...), and update with contacts:. Every link also has a built-in numeric weight field, initially 1.

links:  link contacts undirected:    state: calm | exposed = calm   link follows directed:    strength: number = 0

Creating links

Inside a pet update, create-link-with(breed, partner) links the current pet and partner, and create-link-to(breed, target) creates a directed link from the current pet to target. Creating a link that already exists refreshes it instead of adding a duplicate, and a pet can't link to itself.

The first sample below is the usual pattern for a random graph: in a pet update, pick a partner with one-of other breed and link to it, and wrap the update in repeat n: to run the pass several times. Each pass draws a new partner, but the random-float test gives each pet the same result in every pass, because keyed random calls repeat within a tick (see Essentials, tick and randomness).

To add an exact number of edges, use the observer command add-random-edges(count, strategy). The compact form infers the breeds only when the model has exactly one pet breed and one link breed. In a model with several graphs, use add-random-edges(nodes, links, count, strategy) and name them explicitly.

uniform picks uniformly among the links that do not exist yet. preferentialgrows the graph outward: while isolated nodes remain, each new link joins a uniformly chosen isolated node to an attached parent chosen with probability proportional to the parent's degree. If the graph has no links yet, the first one is uniform. Once every node is attached, missing pairs are weighted by the product of their endpoint degrees. On a directed breed, each growth link points from the parent to the new node.

Fractional counts are floored, and a count larger than the remaining capacity fills every missing pair. Neither strategy creates self-links or duplicates. For directed breeds the two directions are separate pairs; for undirected breeds they are the same edge. The command is valid only at the top level of setup or a step, and its draws are deterministic for a given seed.

setup:  create people population  people:    set-random-position   repeat 3:    people:      where (random-float(100) < link-chance):        let partner = one-of other people        create-link-with(contacts, partner)
setup:  create people population   # Prefer popular parents while bringing isolated people into the graph.  add-random-edges(people, contacts, 120, preferential)

Force layout

force-layout(nodes, links) runs one relaxation pass over a graph: every pair of nodes repels with an inverse-square force, and each link acts as a spring with a preferred length of 5. Use a larger depth in setup to settle a static network, and depth 1 per step when links change while the model runs.

The optional third argument, depth, is the number of passes: an integer from 0 to 1000, default 1, where 0 does nothing. Each pass computes all forces from the current positions and then moves every node together, by at most one world unit. A link's built-in weight(default 1) multiplies its spring strength; negative or non-finite weights add no spring force. Directed links act as ordinary symmetric springs in the layout. Layout is deterministic, does not consume the model's random stream, and ignores walls. Weight does not change how thick a link is drawn.

Optional fourth to sixth arguments set the spring strength, the preferred spring length, and the repulsion strength. Their defaults are 0.04, 5, and 1.

setup:  # Build contacts first, then settle the graph.  force-layout(people, contacts, 30) step:  # Edge weights are graph data, updated separately from layout.  collaborations:    weight := sqrt(1 + times)  force-layout(people, collaborations, 1)

Querying links

The aggregates work on link breeds as they do on patches and pets: count, any, sum, mean, min, max. Use them in monitors for network-wide summaries.

A filter in a monitor can test link fields, as in count contacts where exposed. Endpoint fields such as end1.state can only be read inside a link update, so to count links by what their endpoints are doing, set a link field in a link update (next section) and count that.

monitors:  contact-count: number = count contacts  active-contacts: number = count contacts where exposed  average-weight: number = mean contacts report (weight) # mean degree of an undirected graph: 2 * links / nodesmonitors:  mean-degree: number = if (count people > 0) then (count contacts * 2 / count people) else 0

Graph operations

largest-component-size(pets, links) reports the size of the largest connected component of the selected pets. Links to pets outside the selection do not join components. Isolated pets count as one; an empty selection returns zero. Directed links are treated as undirected connections for this reporter.

redirect-links(links, reference-field, chance-percent) is an observer command that redirects each selected link endpoint through a reference stored on that pet. For example, an assistant reference lets people hand their contacts to their assistants. The chance is sampled independently for each eligible endpoint; omit it for 100%. Missing or dead references stay put.

Redirection preserves link direction. Self-links stay unchanged, and duplicate endpoint pairs merge into the existing link. A new link inherits the original's fields and starts a fresh decay window; an existing duplicate keeps its fields and is refreshed. Filter the link selection to protect owner attachments or other permanent connections, then update link states from their new endpoints.

pets:  people:    delegate: people = nobody links:  link contacts undirected:    permanent = false monitors:  biggest-group = largest-component-size(people, contacts) step:  redirect-links(contacts where (not permanent), delegate, 12)

Link updates

A link update runs the body once per link of the breed. Inside the block, the link's own fields are accessible as bare names; end1 and end2 are the two endpoint pets. On a directed link, end1 is the source and end2 is the target.

Endpoint fields are read with a dot: end1.state is the state of the first endpoint. The update below marks a contact exposed when one end is infected and the other susceptible.

step:  contacts:    state := calm     where ((end1.state = infected and end2.state = susceptible) or (end2.state = infected and end1.state = susceptible)):      state := exposed stop when (count people where infected = 0)

From a pet

Inside a pet update, link-neighbors(link-breed) returns the pets connected to the current pet by links of that breed, as an agentset you can filter and count like any other.

links-with(link-breed) returns the incident links themselves, for example to count the current pet's links in a given state: count links-with(contacts) where exposed. To query another pet's incident links, pass its reference as a second argument: links-with(link-breed, partner). These queries use the runtime's adjacency index, inspecting only incident links.

For directed breeds, in-links(breed) and out-links(breed) return the links pointing into and out of the current pet, and in-link-neighbors(breed) and out-link-neighbors(breed) the pets at their other ends. On an undirected breed the in- and out- forms return the same sets as links-with and link-neighbors.

diffuse(value, rate) over breed moves a numeric pet field along links instead of across the patch grid. With two arguments the rate is a fraction from 0 to 1. diffuse(value, 30, percent) over flows sends 30% of each node's current value, and diffuse(value, 5, total) over flows sends at most 5 units per node. The amount is split among the node's out-neighbors (all its neighbors, on an undirected breed); a node without neighbors keeps its value, and the total is conserved. Like patch diffusion, it is a statement at the top level of a step.

step:  people:    where susceptible:      let neighbors = link-neighbors(contacts)      let infected-neighbor-count = count neighbors where infected       where (infected-neighbor-count > 0 and random-float(100) < virus-spread-chance):        state := infected     where infected:      where (random-float(100) < recovery-chance):        state := recovered

Removing links

Inside a link update, die-link() removes the current link; its endpoint pets are unaffected. Unlike die, it must currently be written with empty parentheses: the compiler rejects the bare name.

When a pet is removed with die or kill, all of its links are removed with it.

# prune light edgesstep:  contacts where (weight < threshold):    die-link()

Models that use this

Library models that build, lay out, and run dynamics on graphs.