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.
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:
- Library symbols: constants defined by the library, such as
claporprofile. 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. - Pet fields: inside a pet block,
pet.fieldis shorthand forself.field, the calling pet's declared field. - Strings: asset paths (
"/audio/chime.wav"), HTTP(S) URLs, and, forx.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 URLSome 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 voicesPan 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 aratepercent chance to be redrawn; bit genes flip instead.genome.nudge(rate, spread)gives each number gene aratepercent chance to shift by a Gaussian step with standard deviationspread, 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 atpointsdistinct 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) / 100Crossover 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 andgp.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 axisMass 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 theworld: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 := 0orset-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 0Built-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 / 24The 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.
Sound
Audience Applause
96 phase oscillators with mean-field coupling. Each clap is scheduled with sound.playDelayed at the moment within the tick when its phase wraps, and panned by seat.
Open model →Mechanics
Spring Chain
Eleven beads joined by spring links, with both ends pinned. A displaced center bead sends a disturbance along the chain.
Open model →Mechanics
Double Pendulum
Two rod links under gravity. The default preset starts two pendulums 0.1° apart and tracks how far their lower bobs separate.
Open model →X data
Starter — X
A minimal X template: you are one account agent, and a synthetic audience of lurkers and replyers notices your posts. x.load(profile) sets its params from your account.
Open model →X data
Your X Forest
One account agent among audience archetypes (lurkers, replyers, amplifiers, quoters) whose mix comes from your reply, repost, and quote shares. No longer listed in the library.
Open model →