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.

Documentation

Libraries reference

Six optional libraries: sound, point-mass mechanics, genomes, genetic programming, Box2D physics, and X account data. A model enables a library by listing it under imports.

Reference

Libraries

Architecture

Imports & namespaces

Library commands are available only to a model that lists the library in an imports: section.

The imports: section is a declaration section, so it comes before setup:. The libraries are sound, mechanics, genome, gp, physics, and x. Each model gets its own instance of every library it imports, and commands are called with the library name as a prefix (sound.*, physics.*, x.*).

multi-library model imports

imports:  sound  physics world:  x: -20..20  y: -20..20  topology: box setup:  sound.load(clap)  physics.gravity(0, -9.81) step:  physics.step(1 / 30)

Calling convention

Symbols, context, & URLs

Library commands take ordinary expressions as arguments, plus three special forms:

  1. Library symbols: constants defined by the library, such as clap or profile. If a param, field, or local has the same name, the model's name wins; write the symbol with the library prefix (sound.clap) to get the constant. An unknown bare symbol is an error.
  2. Pet fields: inside a pet block, pet.field is shorthand for self.field, the calling pet's declared field.
  3. Strings: asset paths ("/audio/chime.wav"), HTTP(S) URLs, and, for x.bind, a param name and a data field path ("activity.postsPerDay").

argument resolution examples

sound.load(clap)                        # Built-in library symbolsound.load(sound.clap)                  # Qualified symbol (avoids shadows)sound.load(pet.sound)                   # Property of calling pet agentsound.load("/audio/clap.wav")           # Relative asset pathsound.load("https://example.com/a.wav") # Remote CORS URL

Some commands are restricted to a context. Commands that act on a pet's body (such as physics.body and physics.impulse) must be called inside a pet block. Observer commands (such as physics.gravity and physics.step) cannot be called inside an agent block. The compiler reports a violation as an error.

Determinism

Runtime isolation

A library affects a run only through the commands the model calls:

  • Random stream: sound playback and audio downloads draw no random numbers and change no agent state, so two runs with the same seed are identical whether sound is on, off, or unavailable. The genome and gp libraries do draw random numbers, keyed to the seed like the language's own random calls.
  • Clocks: sound delays are measured in real time on the Web Audio clock. Physics and mechanics advance only when the model calls their step(...) command, by the duration it is given.
  • Headless runs: in tests, server rendering, and command-line runs, no audio context is created and no audio is downloaded. Sound commands still check their arguments and which clips are loaded, and X models use their default params.

Library · sound

Audio clock & synthesis

The soundlibrary plays short clips when model code calls it. In the Studio every model starts muted, and the browser's audio context is created only when you turn sound on with the model's sound button (labeled Sound off while muted).

Delays are in milliseconds, written with the ms suffix (for example, 120 ms or index * 25 ms, where the suffix applies to the whole expression). They are real time on the Web Audio clock, not simulation ticks: running the model faster makes events more frequent but does not shorten a delay.

scheduled rhythmic audio

imports:  sound setup:  sound.load(clap) step:  repeat index in 0..5:    sound.playDelayed(index * 20 ms)

Reference

Sound command reference

Sound commands are statements, usable in setup, in steps, and inside agent blocks:

sound signatures

sound.load(source)                  # Load clip and set as defaultsound.pan(position)                 # -1 left, 0 center, +1 rightsound.play()                        # Play the default loaded clipsound.play(source)                  # Play a specific pre-loaded clipsound.playDelayed(delay ms)         # Schedule default clip at audioTime + delaysound.playDelayed(source, delay ms) # Schedule specific clip at audioTime + delaysound.stop()                        # Cancel all playing and scheduled voices

Pan must be a finite number from -1 (left) to +1 (right), with 0 centered. In a pet block it sets that pet's pan; outside a pet block it sets the default for observer sounds and for pets without their own setting. It applies to every loaded clip and is captured when play or playDelayed is called, so changing it affects later calls, not voices already scheduled or playing. Reset clears all pan settings.

Delays must be finite numbers from 0 to 60000 ms. At most 128 voices can be playing or scheduled at once; further plays are dropped until voices finish. Output passes through a compressor that limits the level when many clips play together.

Assets

Clips, URLs, & CORS

Three built-in clips are synthesized in the browser, so they need no download:

  • clap: three filtered noise bursts 12 ms apart, then a decaying tail; 150 ms in all.
  • click: a 25 ms burst of filtered noise.
  • beep: a 120 ms, 660 Hz sine tone with a 5 ms attack and an exponential decay.

For other sounds, pass an HTTP(S) URL or a path on this site (such as "/audio/chime.wav"). Files are fetched only after sound is turned on, and other servers must allow cross-origin requests (CORS). Decoded audio is cached, so loading the same file again reuses it.

agent breed with dynamic sound

imports:  sound pets:  voices:    sound: string = "beep" setup:  create voices 1  voices:    sound.load(pet.sound) step:  voices:    sound.play(pet.sound)

Lifecycle

Playback lifecycle

Playback follows the simulation and the page:

  • Pause, reset, and finish: all playing and scheduled voices are cancelled. Missed sounds are not replayed when the model resumes.
  • Hidden tab: switching tabs or hiding the window stops playback, and nothing is scheduled while the page is hidden.
  • Pending downloads: a sound whose file has not finished downloading and decoding is skipped, not played late.

Genome: genes, variation and selection

A genome is a pet field holding a fixed number of genes. Declare genes: genome(8, 0..1) = 0 for number genes that stay inside a range, or bits: genome(24, bits) = 0 for genes that are only 0 or 1. Every gene starts at the value after the equals sign. Read one gene with genes[0] (counting from zero), write one with genes[0] := value, and copy a whole genome with genes := mate.genes. A written gene is clamped to its range. Hatched pets inherit a copy, so editing a child never changes its parent.

The library supplies the variation operators; the evolutionary loop stays in your model. Selection uses ordinary queries: max-one-of n-of(3, other solvers) report (score) is a tournament of three, and one-of other solvers weighted-by (score) picks a mate with chance proportional to its score (negative scores count as zero). Rates are percentages, like random-pc. All draws are keyed to the model seed, so a pinned seed replays the same evolution.

OneMax: evolve a string of ones

imports:  genome monitors:  best: number = max solvers report (score)  diversity: number = genome-diversity(solvers, bits) pets:  solvers:    bits: genome(24, bits) = 0    score: number = 0 setup:  create solvers 40  solvers:    genome.randomize()    score := genome-sum(bits) step:  solvers:    let mate = one-of other solvers weighted-by (score)    where (mate.score >= score):      genome.crossAt(mate.bits, 2)      genome.mutate(2)      score := genome-sum(bits)

These reporters need no import and work in any expression: genome-sum(genes) adds the genes (for bits, it counts the ones), genome-length(genes) is the number of genes, genome-distance(genes, mate.genes) is the Euclidean distance between two genomes, and genome-diversity(breed, field) is the per-gene standard deviation across a breed, averaged over genes and divided by the gene range. Diversity near zero means the population has converged. Genetic Algorithm uses number genes with tournament selection.

Genome command reference

Each command runs inside a pet block and changes that pet's genome. When a breed has several genome fields, name the one you mean first: genome.mutate(flags, 2).

  • genome.randomize() draws every gene uniformly from its range.
  • genome.mutate(rate) gives each gene a rate percent chance to be redrawn; bit genes flip instead.
  • genome.nudge(rate, spread) gives each number gene a rate percent chance to shift by a Gaussian step with standard deviation spread, clamped to the range. It does not apply to bit genes.
  • genome.cross(mate.genes) takes each gene from the mate with even odds (uniform crossover).
  • genome.crossAt(mate.genes, points) cuts at points distinct random positions (one if omitted) and alternates between its own genes and the mate's at each cut, so neighbouring genes stay together.

Genome commands change pets, so use them in a plain step:, not step staged:. Both genomes in a crossover must have the same length.

GP: evolving programs

Genetic programming evolves formulas instead of numbers. A tree field holds one program: program: tree(add sub mul | v 1 2, depth 4) = v. Before the bar come the operators the program may use; after it come its inputs (names) and constants (numbers); depth (1 to 12) caps how tall a program may grow. The value after the equals sign is the starting program, one input or number.

Programs are stored as text such as (sub (mul v v) 1), so they copy with program := mate.program, save with the run, and show up in the pet inspector. gp.eval(program, 3) runs a program, binding its inputs in declared order. Every operator is defined for every input: division by zero gives 1 (Koza's protected division), log (natural) and sqrt use the absolute value, exp is capped, and results are clamped to ±10⁹. Logic operators treat any value above zero as true and return 0 or 1.

Evolve a formula that turns 3 into 12

imports:  gp pets:  critters:    program: tree(add sub mul | v 1 2, depth 4) = v    error: number = 0 setup:  create critters 40  critters:    gp.randomize() step:  critters:    let mate = max-one-of n-of(3, other critters) report (0 - error)    where (mate.error < error):      gp.cross(mate.program)    where (random-pc <= 20):      gp.mutate()    error := abs(gp.eval(program, 3) - 12) + gp.size(program) / 100

Crossover and mutation never exceed the declared depth, which bounds bloat; charging a little for gp.size(program) in the score, as above, favors short formulas. Symbolic Regression and Boolean Multiplexer are complete, editable examples.

GP commands and reporters

Commands run inside a pet block and change that pet's program. As with genomes, name the field first only when the breed has several tree fields.

  • gp.randomize() writes a random program by ramped half-and-half: a depth from 1 to the limit, then a full or a grown tree with even odds.
  • gp.cross(mate.program) replaces a random branch with a random branch of the mate's program, which must use the same operators and inputs. A graft that would exceed the depth limit is skipped.
  • gp.mutate() replaces a random branch with a new random branch that fits in the remaining depth.

Reporters draw no random numbers and change nothing, so they work in any expression, including monitors.

  • gp.eval(program, inputs...) runs the program; give one number per declared input.
  • gp.size(program) counts its nodes and gp.depth(program) measures its height (a lone input or constant has depth 0).
  • gp.toInfix(program) formats it as ordinary math, such as (v * v) - 1, for labels.

Operators: add sub mul div min max, neg abs sqrt sin cos exp log, comparisons gt lt, logic and or not, and if, which takes a condition and two branches.

Mechanics: pets and distance connections

The mechanics library treats pets as point masses and links as rods or springs between them. There is no separate physics world: mass and velocity are pet fields, rest length and stiffness are link fields, and the library advances them in small numerical steps.

Inside a pet block, mechanics.body(mass) enrolls the pet as a point mass; mass 0 fixes it in place. Declare mass, velocity-x, and velocity-y as breed fields to read or set them from model code; declared velocities become initial conditions. Momentum is mass times velocity. mechanics.force(x, y) adds to the force applied during the next step; mechanics.impulse(x, y) changes velocity at once, by impulse divided by mass.

Connections are ordinary undirected links. Inside a link update, mechanics.rod(length) makes the link a rigid distance constraint and mechanics.spring(length, stiffness, damping) an elastic one. The link fields connection, rest-length, stiffness, and damping hold that state, and removing the link removes the interaction.

A spring pushes its endpoints apart or together along the link with force stiffness * (distance - rest-length) + damping * relative-speed, equal and opposite on the two ends. Stiffness is force per unit distance and damping is force per unit relative speed. Rods are not stiff springs: they are enforced by repeated, mass-weighted position and velocity corrections (SHAKE/RATTLE).

Gravity acts on pets; links exchange momentum

# After enrolling pets and configuring their links:step:  beads:    mechanics.force(0, -9.81 * mass)  mechanics.step(1 / 30)

mechanics.step(seconds), called from the observer, advances all enrolled pets and then clears the accumulated forces. It splits the step into substeps of at most 1/240 second, shorter for stiff or heavily damped springs. The step must be above 0 and at most 0.1 seconds. Stiffness that would need more than 4096 substeps, or rods that do not converge within 100 correction passes, raise an error instead of silently stretching. A fixed endpoint absorbs reaction momentum and a free pair conserves it; energy is conserved only approximately, and damping removes it on purpose.

The world must use box topology. Its bounds are only for viewing: these point masses have no collisions, radius, rotation, wrapping, or boundary walls. For collisions and sliding joints, use the physics library below, and don't enroll the same pet in both. Reset recreates the pets and links, so no mechanical state survives it. Double Pendulum and Spring Chain are complete, editable examples.

Library · physics

Planck.js (Box2D) solver

The physics library gives each model a 2D rigid-body solver from Planck.js (a JavaScript rewrite of Box2D). Pets become circular bodies with mass that collide with each other and with the world boundary, under gravity, forces, and impulses, and can be joined by springs, rods, and slides. Bodies use continuous collision detection and do not rotate.

The solver is synchronous and deterministic. It does not follow the browser's frame rate: it advances only when the model calls physics.step(dt), by exactly dt seconds.

minimal free-falling body

imports:  physics world:  x: -20..20  y: -15..15  topology: box pets:  balls:    state: falling = falling setup:  physics.gravity(0, -9.81)  create balls 1  balls:    set-position(0, 10)    physics.body(1, 0.5) step:  physics.step(1 / 30)

Pet context

Pet body commands

These commands must be called inside a pet block (such as balls:). A pet can be registered as a body only once:

body command signatures

physics.body(mass, radius)                     # Register circle: mass 0 = static, >0 = dynamicphysics.velocity(vx, vy)                       # Set linear velocity in world units per secondphysics.force(fx, fy)                          # Apply continuous force for the next physics stepphysics.impulse(ix, iy)                        # Apply instantaneous momentum (velocity += impulse / mass)physics.spring(other, length, freq, damping)   # Elastic distance jointphysics.rod(other, length)                     # Rigid distance between pet centersphysics.rod(other, length, offsetX, offsetY)    # Anchor offset from the other petphysics.slide(other, axisX, axisY)             # Relative motion along one axis

Mass and radius: mass ranges from 0 (a static, immovable anchor) to 1,000,000. Radius must be at least 0.01and at most half the world's smaller dimension. The collision radius is independent of how the pet is drawn, which is set in styles.petstyle.

Observer context

World commands

World commands configure the model's solver and must be called outside agent blocks (at the top level of setup: or step:):

world command signatures

physics.gravity(gx, gy)     # Set gravitational acceleration vector (negative y points down)physics.restitution(value)  # Global coefficient of restitution (elasticity: 0 = dead, 1 = elastic)physics.step(seconds)       # Advance simulation by constant timestep (e.g. 1 / 30 or 1 / 60)

Timestep: the duration passed to physics.step(seconds) must be from 0.000001 to 0.1 seconds. The solver splits it into equal substeps of at most 1/120 second and clears accumulated forces at the end of the call. Restitution defaults to 0.8 and must be between 0 and 1.

Coupled mechanics

Springs, rods & slides

physics.spring joins two pets with a Box2D distance joint that behaves as a damped spring:

  • other: the pet to connect to.
  • length: rest length in world units (0.01 to 1,000,000).
  • frequency: natural frequency in hertz (0.01 to 30). Higher values make a stiffer spring.
  • damping: damping ratio, from 0 (undamped) to 1 (critically damped).

At least one endpoint must be dynamic (mass above 0). Calling physics.spring again for the same pair replaces the existing joint instead of adding a second one.

A rod holds a fixed distance from the calling pet's center to the other pet's center, or to an anchor offset from it. Its length has the same limits as a spring's. A pair has one distance constraint, so calling rod or spring again replaces it. A slide restricts relative motion to one axis, given as a nonzero vector and normalized by the library; (1, 0) allows horizontal travel. Both pets need bodies, and at least one must be dynamic. Rods and slides turn off collisions between the two bodies they connect. None of these joints draws a link.

anchored elastic pendulum

imports:  physics world:  x: -20..20  y: -20..20  topology: box pets:  anchors:    state: pinned = pinned  weights:    state: dynamic = dynamic setup:  physics.gravity(0, -9.81)  create anchors 1  anchors:    set-position(0, 15)    physics.body(0, 0.5)  create weights 1  weights:    set-position(0, 5)    physics.body(2, 0.5)    physics.spring(one-of anchors, 8, 2, 0.1) step:  physics.step(1 / 30)

Constraints

World constraints & synchronization

The physics library handles circular bodies only, with these limits:

  • Box topology only: the world must declare topology: box. The solver adds four boundary walls along the world: bounds. Wrapping worlds are not supported, nor are patch walls or non-circular shapes.
  • Moving a body from code: if model code sets a pet's position directly (x := 0 or set-position(x, y)), the next physics step moves its body to that point and keeps its velocity.
  • Removal: when a pet dies, its body and every joint attached to it are destroyed at the next physics step.

Library · x

Connection & recipes

The xlibrary sets model params from aggregate statistics of the signed-in user's X (Twitter) account. The model declares which params map to which statistics; the Studio shows a panel for connecting an account, and when the data arrives it writes the mapped values into the params and restarts the model.

The shortest route is a built-in recipe, x.load(recipe), which maps the aggregates to a fixed set of params. Params the model doesn't declare are skipped, and a value whose type doesn't match its param is reported as an error.

loading the profile recipe

imports:  x params:  posts-per-day: number = slider(8, 1..40)  peak-hour: number = slider(14, 0..23) memory:  signal: number = 0 setup:  x.load(profile) step:  signal := if (tick % 24 = peak-hour) then posts-per-day else 0

Built-in mappings

Built-in model recipes

There are nine recipes, each written for one model: Starter — X and eight X experiments that are no longer listed in the library but still open from their URLs. Most recipes also set account-username, posts-per-day, and peak-hour. The other params each one sets:

  • profile (Starter — X): audience-size, reply-share, engagement-power, sense-radius.
  • forest (Your X Forest): audience-size, night-share, reply-share, repost-share, quote-share, engagement-power, sense-radius.
  • slime (Your X Attention Slime): population, night-share, engagement-power, deposit-amount, diffusion-rate (always 40), evaporation-rate, follow-strength.
  • village (Your X Ego Village): village-size, network-density, reply-share, repost-share, quote-share, engagement-power, status-decay.
  • fireflies (Your X Posting Fireflies): population, night-share, engagement-power, nudge, sense-radius, reply-share.
  • fatigue (Your X Meme Fatigue): audience-size, network-density, hub-fraction, reach-chance, recovery-chance, fatigue-chance, night-share, engagement-power.
  • timeline (Your X Timeline Schelling): conversational-share, similarity-threshold, engagement-power, and density fixed at 78.
  • clones (Your X Clone Swarm): clone-count, noise-count, night-share, reply-share, repost-share, engagement-power, attention-budget, decay-rate.
  • threads (Your X Termite Threads): population, chip-density, reply-share, quote-share, branching, decay-rate.

Custom mappings

Custom parameter bindings

To map a single aggregate field to a param without a recipe, use x.bind(parameter, fieldPath). The two X models still listed in the library (The Attention Lottery and Reach or Relationships?) use it. A minimal example:

direct parameter binding

imports:  x params:  activity-rate: number = slider(8, 0..100) memory:  total: number = 0 setup:  x.bind("activity-rate", "activity.postsPerDay") step:  total := total + activity-rate / 24

The first argument is the name of a param declared in params:; the second is one of the field paths listed below. When the data loads, the field's number or string is written into the param, with the same type check as recipes.

Schema

Available aggregate fields

These are the fields x.bindaccepts. All but the profile fields are computed from the account's most recent posts, up to 100; "sample.posts" is how many were used. Shares are percentages.

  • Profile and sample: "profile.username", "profile.name", "profile.publicMetrics.followersCount", "profile.publicMetrics.followingCount", "profile.publicMetrics.postCount", "sample.posts".
  • Activity: "activity.postsPerDay", "activity.peakHour" (0–23, local time), "activity.nightShare" (posts from midnight to 6 a.m.), "activity.weekendShare".
  • Composition (shares of the sampled posts): "composition.originalShare", "composition.replyShare", "composition.repostShare", "composition.quoteShare".
  • Engagement (averages per sampled post): "engagement.averageLikes", "engagement.averageReposts", "engagement.averageReplies", "engagement.averageQuotes", "engagement.interactionsPerPost", "engagement.interactionsPerThousandFollowers".

Security

Privacy & offline execution

What the Studio reads, keeps, and passes to the model:

  • Credentials: OAuth tokens are stored in encrypted, HTTP-only cookies that page scripts cannot read, and only the Studio server uses them. Model code never sees them.
  • Data: the server reads the public profile and the most recent posts (up to 100): their timestamps, whether each is a reply, repost, or quote, and their public counts. The browser receives the profile (name, username, avatar URL, public counts) and the aggregates listed above. Post text is not stored or passed on, and direct messages are never requested.
  • Caching: aggregates are cached for 15 minutes to stay within API rate limits. The panel's Refresh from X button fetches them again.
  • Read-only access: the OAuth scopes are tweet.read, users.read, and offline.access. A model cannot post, like, repost, or change the account.
  • Offline: without a connected account, and in headless runs, an X model runs with the param values written in its source.

Catalog

Models that use these libraries

Each of these models imports one of the libraries above; open it to read and edit the source.