pysims manual

Every call and every setting, each with what it takes, what it does, what it gives back, and a line you can paste. Everything is in meters and seconds, x runs right and y runs up, and angles are in degrees counterclockwise from the x direction.

How to read this page

Each entry starts with the call written out, then says what each argument is, what it does, and what it gives back. Under that is a line or two you can paste into an empty program.

Two kinds of thing are listed. An action has parentheses and does something: sim.step(), ball.push(0, 20). A setting has none and is a number or a word you read, and usually set: sim.gravity = 1.6 puts you on the Moon. Writing a setting with parentheses does nothing at all, and that is the commonest mistake on this page.

Name your arguments. Only the first few of any call can be given by position. Ball(0, 5, 0.3) is x, y and radius, and everything after that must be written out: Ball(0, 5, 0.3, mass=2). Writing Ball(0, 5, 0.3, 2) and meaning a mass is an error, and the page will say so rather than guess.

Defaults are shown in the call as name=value. Where an entry says a number is in meters, it is in meters even when the thing on the screen is small: the view is a window onto a world measured in real units, not a picture measured in pixels.

Starting

from pysims import sim, Ball, Box, Segment, Curve, Spring, Pin, Rope, Pulley, Pivot, Label, Agent, Grid

ball = Ball(x=0, y=5, radius=0.3)
floor = Segment(-10, 0, 10, 0)

while sim.running:
    sim.step()

That is a whole program. sim is the simulation, the capitalized names are the things you can make, and the loop is yours to write. Import only what you use; an unused name costs nothing but reads as a promise you did not keep.

Python's own modules are there too. import math for a sine, import random for a dice roll, and numpy and matplotlib load the first time you import them, which takes a few seconds and says so.

The page shows where a program starts before it runs, so you can see what you are about to set going. Change a number and press Reset the scene, the button beside Stop, to see the new setup; press Run to set it going.

Every name at a glance

The whole API, in the order it appears below.

The loop

sim.step()action

Moves the world on by sim.dt seconds, draws it, and records a row. This is the one line a loop must have. Everything else you write around it.

A step does the physics in a fixed order: the forces you pushed this step, then gravity, then velocities, then the contacts, then positions, then the rods. Call it once per pass through your loop. Calling it twice runs the world twice as fast and halves the number of rows in your data, which is almost never what you meant.

while sim.running:
    sim.step()

See also sim.dt for how big a step is, and sim.record for putting your own number in the row this step writes.

sim.runningsetting, read only

True until Stop is pressed on the page or the program calls sim.stop. Putting it in the while is what lets Stop actually stop you.

A loop written while True: ignores Stop. The page gives it half a second and then kills the worker, which loses the run's data, so write the loop the way below.

while sim.running:          # Stop ends this loop
    sim.step()

print("stopped after", sim.tick, "steps")

It is read, never set. To end a run from inside the program call sim.stop.

sim.stop()action

Ends the run from inside the program. sim.running goes False, so the loop finishes its pass and falls out. Anything after the loop still runs, which is where a sweep prints its answer or saves its file.

while sim.running:
    sim.step()
    if ball.y < 0:
        sim.stop()

print("it took", round(sim.time, 2), "seconds")

sim.dt = 1/60setting

How much time one step covers, in seconds. A sixtieth by default, which matches a screen and is small enough for almost everything.

A smaller step is more accurate and slower: halving it roughly halves the error an orbit or a pendulum builds up, and doubles the work. Set it once at the top, before anything moves. Changing it part way through rewrites the clock, because sim.time is worked out fresh each step from the step you are using now.

sim.dt = 0.001        # a thousandth: for an orbit that has to close
sim.dt = 0.1          # a tenth: for a model where a step is a day

See also sim.speed, which changes how fast it plays and not how accurate it is.

sim.ticksetting, read only

How many steps have run. A whole number, starting at 0 before the first step. Useful for doing something every so often.

if sim.tick % 60 == 0:
    print("a second has passed")

sim.timesetting, read only

Seconds since the run began, exactly tick * dt. The t column in your data is this number, so anything you record is already against the time and you never record the time yourself.

while sim.running:
    if sim.time > 5:
        sim.stop()
    sim.step()

print("ran for", round(sim.time, 2), "seconds")

sim.reset()action

Throws away every body, agent, grid, link and row, and puts the clock back to zero. Keeps the settings: gravity, dt, the view, the instruments, and the controls you made.

This is how a sweep runs the same model thirty times. Make the bodies again inside the loop, because reset removed the last run's.

import math

sim.realtime = False                      # no need to watch thirty runs

for angle in range(10, 81, 5):
    sim.reset()
    ball = Ball(x=0, y=0, radius=0.3)     # again, every run: reset took the last
    ball.vx = 20 * math.cos(math.radians(angle))
    ball.vy = 20 * math.sin(math.radians(angle))
    while ball.y >= 0 and sim.running:
        sim.step()
    print(angle, "degrees went", round(ball.x, 1), "meters")

See also sim.realtime, which a sweep turns off.

Settings

sim.gravity = 9.8setting

Meters per second squared, pulling down, applied to every body the engine moves. Set it to 0 for space, 1.6 for the Moon, 24.8 for Jupiter. It can be changed while the run goes, from a slider, and the change takes effect on the next step.

It does not touch a fixed body, a driven body, or an agent.

sim.gravity = 1.6                 # the Moon

g = sim.slider("gravity", 0, 30, start=9.8)
while sim.running:
    sim.gravity = g.value
    sim.step()

sim.G = 6.674e-11setting

The gravitational constant, for your own force law. The engine never reads it. An orbit is a push you wrote, and this is the number you write it with, kept here so you do not have to remember it.

Setting it to 1 makes a toy solar system whose numbers are small enough to read.

dx = sun.x - planet.x
dy = sun.y - planet.y
r = (dx * dx + dy * dy) ** 0.5
f = sim.G * sun.mass * planet.mass / (r * r)
planet.push(f * dx / r, f * dy / r)

See also body.push.

sim.speed = 1setting

How fast the run plays against the clock on the wall. 1 is real time, 0.1 is slow motion, 4 is fast forward. It changes nothing about the physics: the same steps happen, further apart or closer together.

The page has its own speed slider in the bar, which multiplies this one. The program cannot see the page's.

sim.speed = 0.2       # slow enough to watch a collision

sim.realtime = Truesetting

True paces the run against the clock. False runs as fast as the machine can, drawing only now and then, which is what a sweep wants: thirty runs in a second instead of thirty seconds.

sim.realtime = False

sim.wrap = Falsesetting

True makes the view a torus: anything that leaves one edge comes back at the opposite one. The rectangle it wraps on is the view, so sim.view decides the size of the world.

It moves bodies and agents alike. A body carried across the edge simply arrives at the other side, and whatever it is touching is found again from there.

sim.wrap = True
sim.view(width=40, height=30)

sim.digits = 4setting

How many significant figures the page shows a number with: in a graph's readout, in a monitor, and in the readout under the pointer. One to fifteen.

This is what is shown. The run keeps every figure it worked out, and so does sim.data. The clock is the exception, because sim.dt already says what a time is worth: a step of 0.1 makes tenths and they are shown as tenths whatever this is set to.

sim.digits = 6

See also sim.save, which rounds a file rather than a screen.

sim.view(width=, height=, center=, follow=)action

width
how many meters across the canvas shows. Give this one alone and the height follows the canvas's shape.
height
how many meters up and down.
center
a pair, (x, y) in meters, for the middle of the view.
follow
a body or an agent. The view keeps it in the middle as it moves.

What the canvas shows, in meters. Without a call the view is 16 meters across and 10 up, centered a little above the origin, so a floor at y = 0 sits near the bottom. A Grid or a Curve made before you call this claims the view for itself, which is usually what you wanted.

sim.view(width=40, center=(0, 12))
sim.view(follow=rocket)

See also sim.wrap, which turns this rectangle into the whole world.

sim.view_bounds()action, gives an answer

Gives back (xmin, ymin, xmax, ymax) in meters: what is on the canvas right now, following along if the view follows something. The way to ask whether a body has left the screen, or to scatter things where they can be seen.

import random

xmin, ymin, xmax, ymax = sim.view_bounds()
for i in range(20):
    Ball(x=random.uniform(xmin, xmax), y=random.uniform(ymin, ymax), radius=0.2)

Bodies

Four things the engine knows how to collide: a Ball, a Box, a Segment and a Curve. What this section says first is true of all four. Each kind's own entry comes after, with whatever is true of it alone.

x and y are always the center, never a corner. You may set any attribute of your own on a body: ball.team = "red" just works, and nothing in the engine will touch it.

Where an entry below writes ball. or curve., it means the thing you made, and the name is yours. Write hill = Curve(...) and every curve.height(x) on this page is hill.height(x) in your program.

what every body takessettings

These go in the call, and can be read or set afterwards. A Ball and a Box take all of them. A Segment and a Curve are fixed scenery with no mass, so they take only the five marked scenery too; handing one a mass, a velocity or fixed=False is an error rather than an argument quietly ignored.

mass=1
kilograms.
vx=0, vy=0
the velocity it starts with, meters per second.
bounce=0.5
scenery too. 0 sticks, 1 comes back as fast as it went in. Two bodies' values multiply, so a 0.8 ball on a 0.5 floor bounces at 0.4. Below a crawl the bounce is dropped altogether, so a ball comes to rest instead of buzzing.
friction=0.3
scenery too. 0 is ice. Of two bodies' values the smaller wins, so one slippery thing is enough.
fixed=False
scenery too. True means no force moves it, whatever hits it: a wall, the Sun. Give one a velocity as well and it is a wall that moves, which is the next entry.
driven=False
True means you move it. The engine draws it and reports what it touches but never moves it, so its position is whatever you last set.
layer=0
scenery too. Two bodies touch only on the same layer, and that is the whole rule.
color="white"
scenery too. Any CSS color, alpha included, so "rgba(255,0,0,0.3)" is a ghost. Scenery is gray by default.
name=None
scenery too. What its columns are called in the data. Worth setting when you are going to graph it, and not otherwise: see naming a column.
ball = Ball(x=0, y=8, radius=0.3, mass=2, bounce=0.7, friction=0.1,
            color="orange")

Layers have no exception, which is worth knowing before you use one. Two pendulums on layers 1 and 2 swing through each other; a floor on layer 0 stops layer-0 things and nothing else, so a floor on the wrong layer lets everything fall through it. A higher layer draws on top. Springs and pins have no layer, and sim.near ignores layers, because finding is not touching.

Three more settings go in the same call and are not in the list above, because each belongs to only some of the kinds: a velocity on a fixed body, belt= on a Segment, and sensor= on a Ball, a Box or a Segment.

what every body tells yousettings and actions

ball.x, ball.y
where its center is, in meters.
ball.vx, ball.vy
how fast it is going, meters per second.
ball.mass .bounce .friction .color .layer
what it was made with. Any of them can be set again at any time.
ball.angle
which way it is pointing, in degrees counterclockwise. A Box is made at one; a body that turns changes it every step. Setting it on a body the engine is turning says so once, like setting x; place(x, y, angle=) is the way to mean it.
ball.spin
how fast it is turning, in degrees a second, counterclockwise when positive. Set it to throw something spinning; a body that cannot turn refuses one and says why.
ball.turns
whether the engine turns it: a ball that rolls, a Box made with turns=True, anything on a Pivot. Read only.
ball.name
what its columns are called, which is the name= you gave it or ball1 if you gave none. Read it; setting it part way through a run starts a fresh column and leaves the rows already written under the old one.
ball.kind
"ball", "box", "segment", "curve" or "agent". How you ask what a thing is when a collision or a click hands you one.
ball.distance(other)
meters between the two centers.
ball.place(x, y, angle=None)
put it somewhere on purpose, and at an angle if you give one. The engine carries on from there and the velocity and spin are untouched, so set those too if the move should change where it is going.
ball.remove()
takes it out, along with any spring or pin attached to it.
while sim.running:
    sim.monitor("speed", (ball.vx ** 2 + ball.vy ** 2) ** 0.5)
    if ball.y < -20:
        ball.remove()
        sim.stop()
    sim.step()

Setting ball.x on a body the engine moves says so once in the console, because the usual reason for doing it is not knowing about velocity. When you do mean it, say which: driven=True if you are moving it every step, and ball.place(x, y) for a one-off, a teleport or a body put back after something swept past it.

body.touchinggives an answer

What this body is touching right now: resting on, pressed against, or sliding along. A list of bodies, empty when it is in the air. Reading it does not empty it, so you can ask as often as you like and the answer is about this instant rather than about what has happened since.

while sim.running:
    if floor in hero.touching and "space" in sim.keys:
        hero.vy = 6            # it can only jump off something
    sim.step()

This is the other half of sim.new_collisions. That one answers did these two meet, and says so once, on the step they did. This one answers are these two touching, every step, for as long as they are.

A sensor is never in the list. A sensor is not touched, it notices, and what it notices comes through sim.new_collisions() every step it is overlapped. Scenery is in the list like anything else: a floor is a body, so floor in ball.touching is the usual way to ask.

a wall that movessetting

Give a Segment, or a fixed Box, a velocity and it travels. Nothing pushes it back, which is what fixed has always meant, but it shoves aside whatever it meets: a piston, a paddle, a lift, a moving platform.

wall = Segment(20, 0, 20, 12, vx=-1)      # closes at a meter a second
paddle = Box(x=0, y=-4, width=3, height=0.4, fixed=True)

while sim.running:
    paddle.vx = 6 if "right" in sim.keys else -6 if "left" in sim.keys else 0
    sim.step()

A ball meets it at the speed the two close at, not at its own, so a ball comes off a wall coming the other way faster than it arrived and off one pulled away slower. That is why a swung bat sends a ball further than a held one, and the engine gets it with no help: a ball at rest met by a wall closing at 2 m/s leaves at exactly 4.

A moving platform carries what rests on it, by friction, and what it carries is slowed and dragged in the ordinary way. It translates and never turns: a wall keeps its length and its angle.

A Curve cannot be given one. A curve holds a ball along its own surface, and moving it would mean working the contact out in a moving frame, which this engine does not do.

A wall that outruns what it is pushing arrives somewhere the others have not reached, and leaves them behind. Keep it slower than the things it is meant to sweep along, the way the gas demo keeps its piston slower than the gas.

See also what every body takes, and the gallery's Gas in a box, which squeezes sixty molecules with one.

ball.push(fx, fy, at=None)action

fx, fy
a force in newtons, right and up.
at
where it acts, as (x, y) in meters. Left out, it acts at the center.

Adds a force for this step only. Say it again every step or it stops. That is the point: every force in physics is something acting now, and a force that lasted would be one you had to remember to turn off.

Pushes add up, so a drag and a thrust in the same step are two calls. The engine turns the total into an acceleration by dividing by the mass, which is why a heavier body needs a bigger push for the same effect.

while sim.running:
    speed = (ball.vx ** 2 + ball.vy ** 2) ** 0.5
    ball.push(-0.5 * speed * ball.vx, -0.5 * speed * ball.vy)   # air drag
    sim.step()

A force off the center is also a torque, and a body that turns turns under it: the torque is the lever arm crossed with the force, (x - cx) * fy - (y - cy) * fx, and the angular acceleration is that over the moment of inertia. A body that cannot turn feels the force and not the twist.

wheel = Ball(x=0, y=3, radius=0.5, mass=2)          # a disc on a pivot
Pivot(wheel, 0, 3)

while sim.running:
    wheel.push(0, -4, at=(wheel.x + 0.5, wheel.y))   # 4 N down at the rim
    sim.step()

A push on a fixed or driven body does nothing, because nothing the engine computes moves either of them.

See also sim.show, which draws the arrow of what you pushed.

ball.turn_velocity(omega)action

omega
how fast to turn the velocity, in radians a second, counterclockwise when positive.

A force that turns the velocity without changing its speed, for this step only, like a push. The velocity is rotated by exactly omega * dt, so the speed is kept to the last digit.

This is the magnetic force on a charge, q v x B, which is always across the motion and so can never speed it up or slow it down. You write omega = q * B / m and its sign; the engine learns only that this force is a turn. A positive charge with B pointing out of the screen turns clockwise, by the right-hand rule, so there omega is negative.

sim.gravity = 0
q, B = 1.0, 2.0
charge = Ball(x=0, y=0, radius=0.2, mass=1.0, vx=3.0)

while sim.running:
    charge.turn_velocity(-q * B / charge.mass)
    sim.step()

The circle has the textbook's radius, m v / (q B), which here is 1.5 m, and the textbook's period, 2 pi m / (q B). Add a push for an electric field and you have the velocity selector: with E up and B out of the screen, a charge at v = E / B goes straight through.

Why not push(q * ball.vy * B, -q * ball.vx * B)? Because a push is read once, at the start of the step, and a force that depends on the velocity is then a step behind the velocity by the end of it. A charge pushed that way spirals outward and nearly doubles its speed in twenty seconds. Pushes are right for every force that does not depend on the velocity, and for drag, which only ever slows; a turn is the one force that needs to know it is a turn.

ball.label = ""setting

Text written on the body. Yours to write, and you may change it every step. Two carts of 1 kg and 3 kg look the same otherwise, and the collision is about the fact that they are not.

The ink is chosen from what the text sits on, so it reads on a yellow ball, a navy one, and on the bare canvas in either theme. Scenery and agents can wear one too.

cart = Ball(x=-5, y=0.5, radius=0.5, mass=2, label="2 kg")

while sim.running:
    cart.label = f"{cart.vx:.1f} m/s"
    sim.step()

sim.things sim.bodiessettings, read only

Every body and agent you made, in the order you made them, and the bodies only. Grids, springs and pins are in neither. This is how a program reaches things it kept no variable for.

for body in sim.bodies:
    body.label = f"{body.mass:.0f} kg"

Each kind

Ball(x, y, radius=0.5)action, gives a body

x, y
the center, in meters.
radius
meters. Read back as ball.radius.

A circle, and the shape to reach for unless you need a corner. It takes everything in what every body takes. It is the only one that rolls.

ball = Ball(x=0, y=8, radius=0.3, mass=2, bounce=0.7, color="orange")

Ball(..., roll=True)setting

roll
True, the default, for a solid sphere; "disc" or "hoop" for those shapes; False for a puck that cannot turn. Keyword only.
ball.turn
how far it has turned, in degrees. Negative rolling to the right. Read only.
ball.spin
how fast it is turning, in degrees a second. Read only.

A ball rolls. On ground that grips it, it turns as it goes, on anything it touches: a Curve, a Segment, a Box. Thrown at a floor it bounces onward, keeping 5/7 of its speed along the ground where a puck would stop dead. Part of every drop goes into the spin rather than the motion, and the share depends only on the shape, so a rolling thing loses the race to a sliding one and a hoop loses it worst: a sphere reaches the bottom at sqrt(1/1.4) of the sliding speed, a disc at sqrt(1/1.5), a hoop at sqrt(1/2). The mark drawn on the ball is the turning, so you can watch it happen.

rail = Curve(lambda x: -0.35 * x, -2, 22, friction=0.0)
x, y = rail.center(-1.5, 0.45)
hoop = Ball(x=x, y=y, radius=0.45, roll="hoop")

while sim.running:
    sim.monitor("turns", round(-hoop.turn / 360, 1))
    sim.step()

A ball rolls because the ground grips it, so friction decides. On a frictionless surface every ball slides, at the textbook's g sin(theta); a sphere needs friction of at least 2/7 tan(theta) to roll without slipping, a hoop 1/2 tan(theta), and below that it slips while the spin catches up. Once it is rolling it does not rub, so friction takes nothing from it and it will swing in a valley for ever; a lesson about friction taking energy wants a puck or a Box. A ball that lands with speed and no spin skids on the instant it arrives and comes out rolling at 5/7 of the speed it had, which is what the rub really costs. On a conveyor a ball is not carried at the belt's speed: it spins up and rolls along at 2/7 of it, which a crate does not do. It keeps its spin through a flight and through a bounce. spin= throws it spinning, and a ball that strikes a box off center turns the box, if the box is one that turns.

Box(x, y, width=1, height=1)action, gives a body

x, y
the center, in meters, not a corner.
width, height
meters. Read back as box.width and box.height.
angle
degrees counterclockwise, the way it is made. Keyword only.
turns
True lets the engine turn it: it can be thrown spinning, is turned by whatever strikes it off center, topples, and lies down flat. Off by default. Keyword only.

A rectangle. It takes everything in what every body takes. A box stays at the angle it was made at unless it turns, and it turns when you say so: turns=True, or a Pivot through it, which is a seesaw or a lever. A box that turns is a plate, with a moment of inertia of m (w^2 + h^2) / 12; it can be thrown with spin=, is twisted by a push off its center and by whatever strikes it off its center, and spins up a ball that rolls along it. A fixed box at an angle is a ramp, treated as its four edges, and cannot spin.

floor = Box(x=0, y=-0.5, width=20, height=1, fixed=True)
ramp = Box(x=4, y=1, width=6, height=0.3, angle=20, fixed=True)
crate = Box(x=0, y=5, width=0.8, height=0.8, mass=3)
thrown = Box(x=-5, y=3, width=1, height=0.4, turns=True, vx=4, vy=6, spin=200)

A box that turns lies down. Thrown spinning, it lands on a corner, rocks, and lies on a face, exactly flat; standing on its narrow end it stays standing, and pushed hard enough at the top it topples and lies on its side; a row of them falls as dominoes; a stack of them stands. The engine finds two points of contact under a face of a box that turns, one under each corner, so the weight moves to the leading corner when the box is pushed and the box does not rock on one point. A box that cannot turn is the same crate it always was, and a box that turns slides exactly as far on a floor as one that cannot. What turns=True still costs is the push you did not mean: a box struck off its center turns, and a box resting off center on another box that turns tips it, which is what a box does.

Segment(x1, y1, x2, y2)action, gives a body

x1, y1
one end, in meters.
x2, y2
the other end.
segment.x, segment.y
its midpoint, which is what x and y mean on it.
segment.angle
its slope in degrees. Read only.

A line at any angle, with no thickness: a floor, a wall, a straight ramp. It has no mass, so it takes only the arguments marked scenery too in what every body takes, and it is gray unless you say otherwise. Give it vx or vy and it is a wall that moves; give it a belt and its surface travels while the line itself stays put.

floor = Segment(-10, 0, 10, 0, bounce=0.8)
wall = Segment(10, 0, 10, 6)
slope = Segment(-10, 6, 0, 0, friction=0.1)

A body made with the line already through it is pushed out the nearer way, and the console says so once, with how deep it was and which way it went. Ball(x=0, y=0) on a floor at y=0 comes up and rests at 0.5; made a hair under the line, it is nearer the underside and goes down. Make a body where it fits: a ball of radius 0.3 on that floor belongs at y=0.3.

Segment(..., belt=0.0)setting

belt
meters a second along the line, from the first end toward the second. Keyword only.

A conveyor. The surface travels at that speed and the segment itself never moves: the two ends stay exactly where you put them, and what stands on the line is carried along it. Which way it runs is decided by which way you wrote the segment, because the speed is measured from the first end toward the second. Segment(-6, 0, 6, 0, belt=3) carries things to the right; the same line written Segment(6, 0, -6, 0, belt=3) carries them to the left, and so does belt=-3.0 on the first one.

Friction is the whole mechanism. A belt carries what stands on it by rubbing against it, which is what a real one does, so belt=3 with friction=0 does precisely nothing: the surface slides under the ball and the ball sits still. That is the lesson rather than a shortcoming, and it is worth setting friction=0 once on purpose to watch it happen.

A crate set down on a belt=3.0, friction=0.8 segment is dragged up to 3.000 m/s and then holds there; a ball is spun up instead and rolls along at 2/7 of the belt, which is its own story under roll=. The rubbing cannot overshoot, because what it is allowed to do in a step is capped by the slip it is working against: once the ball matches the belt there is no slip left and nothing more is taken or given.

conveyor = Segment(-12, 3, 12, 3, belt=3.0, friction=0.8)
crate = Box(x=-10, y=3.4, width=0.8, height=0.8, friction=0.8)

while sim.running:
    sim.monitor("crate m/s", round(crate.vx, 3))
    sim.step()

The page draws a belt with chevrons along it, pointing the way its surface runs, so which way round you wrote it is something you can see rather than something you have to work out.

Only a Segment can be a belt. A Box has four faces and no obvious way round a rectangle, and a Curve raises an error and says why: a belt runs along a straight face from one end to the other, and a curve has no such face.

See also a wall that moves, which is the opposite arrangement: that one travels and shoves what it meets, and a belt stays put and rubs.

Curve(f, x0=-5, x1=5)action, gives a body

f
a function of one number, giving the height of the ground there. A lambda or a def, yours either way.
x0, x1
where the ground starts and stops, in meters.
samples=200
how many points the page draws it with. Nothing to do with the physics: the engine asks your function directly.

Ground with a shape. You write the shape and the engine works out the slope at every point, keeps a ball on it, and lets go when the ball should fly. Like a Segment it is fixed scenery, so it takes only the five arguments marked scenery too in what every body takes.

The surface exists only between x0 and x1. Past either end there is nothing at all, and a ball that gets there becomes a projectile, which is correct and is often a surprise. Balls slide or roll along it; boxes and agents go straight through.

hill = Curve(lambda x: 0.05 * x * x, -10, 10, friction=0.0)
x, y = hill.center(-9, 0.4)
ball = Ball(x=x, y=y, radius=0.4)

while sim.running:
    sim.step()

Your function is only ever asked about an x the curve covers, so a dome may be an honest square root with nothing to say past the circle.

import math

dome = Curve(lambda x: math.sqrt(16 - x * x), -3.5, 3.5)

A ball wider than the hollow it sits in would really bridge across and rest higher than the engine puts it. Keep the radius smaller than the bend it has to follow.

See also roll= and what a curve knows.

what a curve knowsactions, give answers

A curve will answer questions about its own shape. They are written curve. here; in your program they carry whatever name you gave the curve, so with the hill above they are hill.height(x) and the rest. None of them computes a quantity a lesson teaches: they are the geometry you would otherwise have to write out a second time, having already written it as f.

curve.center(x, radius)
where a ball of that radius has its center when it rests at that x. Gives back a pair, and they are the two numbers to start a ball at.
curve.height(x)
how high the ground is there.
curve.slope(x)
how steep, as a rise over a run, so 1.0 is 45 degrees.
curve.angle(x)
how steep, in degrees, positive uphill to the right.
curve.curvature(x)
one over the radius of the bend there. Positive where the ground would hold water.
curve.normal(x)
the unit vector (nx, ny) straight out of the surface.
curve.tangent(x)
the unit vector (tx, ty) along it, pointing in +x.
curve.touch(x, y)
the x on the surface nearest a point.
curve.gap(x, y, radius=0)
how far a point is from the surface. Negative is inside the hillside.
curve.covers(x)
True where there is surface to stand on.
curve.points()
the sampled polyline, as (x, y) pairs.
hill = Curve(lambda x: 0.05 * x * x, -10, 10)
x, y = hill.center(-9, 0.4)
ball = Ball(x=x, y=y, radius=0.4)
sim.graph("t", "steepness")

while sim.running:
    sim.record("steepness", hill.angle(ball.x))
    sim.monitor("still on the hill", hill.covers(ball.x))
    sim.step()

A ball only touches the ground directly below it where the ground is straight. On a slope it touches off to one side, which is why center exists: height(x) + radius puts the ball slightly into the hillside, and the first step shoves it out.

Label(text, x, y) Label(text, thing)action, gives a label

text
what it says. Anything, and it can change every step.
x, y
where it goes, in meters: a point in the world.
thing
or a body, a curve or an agent, and the label follows it.
dx, dy
meters from that thing's center. Keyword only, and only with a thing.
color
any CSS color. Left out, it is picked for contrast with what is behind it.
size=1
a multiple of the ordinary label's size: 1.6 is half again as big. Text is sized on the screen, not in meters, so it stays readable at any zoom.
line=False
True rules a faint line from the words to the thing they belong to.
label.text
read it, and set it as often as you like.

ball.label writes ON a body and goes where the body goes. This is a label of its own, which can sit where there is nothing: a title, the name of a pivot, a reading written in the air above the thing it is about.

Label("the pivot", 0, 2)                     # at a point in the world
Label("2 kg", cart, dy=0.6)                  # beside a body, and it follows
Label("the ramp", block, dx=1.5, line=True)  # with a line to it

note = Label("", -4, 9, size=1.4)
while sim.running:
    note.text = f"{ball.y:.2f} m"
    sim.step()

dx and dy are along the world's own axes, so a note above a plank stays above it when the plank turns. A mark that turns with the body is what label= on the body is for.

It is writing, not a thing in the world. Nothing collides with it, sim.near does not find it, a click goes through it to whatever is behind, sim.wrap leaves it where it was put, and it has no columns in the data. Removing the thing a label is beside removes the label too, the way removing a body removes its pins.

See also ball.label, words written on a body, and sim.monitor, a number beside the controls.

Springs and rods

Spring(a, b, k=10, length=None, damping=0)action, gives a link

a, b
the two bodies it joins.
k
newtons per meter of stretch. Stiffer is faster, and a very stiff spring needs a smaller sim.dt or it shakes itself apart: the step can follow a spring well up to about six steps a swing, and the console says so, once, when a spring is past that, with the largest k the step can take. On a 1 kg body at the default step that is about 14000.
length
the rest length in meters. Left out, it is however far apart they are when you make it.
damping
newtons per meter per second, against the rate of stretch. 0 bounces for ever.

Hooke's law between two bodies, applied as a force, so it really stretches and the two ends pull on each other. Use it for anything springy: a suspension, a chain of masses, a soft joint.

anchor = Ball(x=0, y=8, radius=0.2, fixed=True)
bob = Ball(x=0, y=5, radius=0.3, mass=1)
Spring(anchor, bob, k=40, damping=0.5)

See also Pin, a rod that does not stretch at all.

Pin(a, b) Pin(a, x, y)action, gives a link

a
the body held.
b
another body, for a rod between two things.
x, y
or a fixed point in meters, for a pendulum.
length
the rod's length. Left out, it is the distance when you make it.

A rigid rod. The distance is held exactly rather than sprung, so this is a pendulum, a rigid arm, or a chain of rods.

A rod is solved together with everything else that is touching its ends, so a rod and a floor that disagree are settled in one go rather than one after the other. A pendulum keeps 0.9998 of its swing over a minute and its length to the last digit, where the obvious way of doing it bleeds the swing away in seconds.

bob = Ball(x=3, y=5, radius=0.3, mass=1)
Pin(bob, 0, 8)                        # a pendulum hanging from a point

lower = Ball(x=6, y=5, radius=0.3)
Pin(bob, lower)                       # a second rod: a double pendulum

Removing either body removes the link with it.

See also Rope, the same shape but pulling only, and link.tension, the force the rod is carrying.

Rope(a, b) Rope(a, x, y)action, gives a link

a
the body held.
b
another body, for a rope between two things.
x, y
or a fixed point in meters, for a pendulum on a string.
length
the rope's length. Left out, it is the distance when you make it.

The same shape as Pin, and the same constraint with half the job given up: a rope pulls and never pushes. While its ends are closer together than it is long it does nothing at all, so a bob thrown up past its anchor rises freely where a rod would hold it down. The measured path of that throw on a slack rope is bit-identical to the same throw with no rope in the program at all.

Taut, it is as good as a rod: a pendulum on a rope keeps 0.9999 of its swing over a minute, exactly like one on a rod.

A slack rope is drawn sagging on the page, so you can see that it is not pulling.

Rope is not in the import line at the top of this page. Add it where you need it.

from pysims import sim, Ball, Rope

bob = Ball(x=3, y=5, radius=0.3, mass=1)
rope = Rope(bob, 0, 8)                 # a pendulum on a string

while sim.running:
    sim.monitor("pulling", rope.tension > 0)
    sim.step()

See also Pin, which holds the distance both ways, breaks=, which parts it at a force, and link.tension.

Pulley(a, b, x, y)action, gives a link

a, b
the two bodies, one on each end of the rope.
x, y
the wheel the rope runs over, in meters.
x2, y2
a second wheel for b's side, or the far corner of a table edge. Leave them out and both sides hang from the one wheel.
ratio=1
2 is a block and tackle: b moves half as far and feels twice the pull.
length
how much rope there is; by default, exactly what it finds between its ends.
pulley.tension
the pull in the rope, in newtons, the same all along it. Read only.

A rope over a wheel, with a body on each end: one side rises exactly as the other falls, and the rope pulls and never pushes. The wheel has no mass and no friction, so it bends the rope and does nothing else, which is the pulley every problem set assumes. Hang two different masses from it and you have the Atwood machine, which falls slowly enough to read g from.

light = Ball(x=-1, y=5, radius=0.3, mass=1.0)
heavy = Ball(x=1, y=5, radius=0.3, mass=3.0)
rope = Pulley(light, heavy, -1, 9, 1, 9)

while sim.running:
    sim.monitor("tension (N)", round(rope.tension, 2))
    sim.step()

The numbers are the textbook's to the digit. One and three kilograms accelerate at (3 - 1) g / (3 + 1), which is 4.9, and the rope carries 2 (1)(3) g / 4, which is 14.7 N. Tie a block on a ramp over the ramp's top edge to a weight hanging below, Pulley(block, weight, edge_x, edge_y, edge_x + 0.5, edge_y), and the whole thing moves at (m_w g - m_b g sin(theta)) / (m_b + m_w).

A rope longer than the two hanging sides is slack, and the bodies fall as if it were not there until it runs out; then it catches them exactly where it does. Lift one side by hand and the rope goes slack and reads 0 N rather than holding the other up. breaks= parts it at a force, like a Rope.

Pivot(body, x, y) Pivot(a, b, x, y)action, gives a link

body
a Ball or a Box. A Box on a pivot turns, with no turns=True needed; a ball on one turns if it rolls, which every ball does unless it is a puck.
x, y
where the pin goes, in meters: a point of the body, wherever on it that is, nailed to that point of the world.
a, b, x, y
or two bodies, pinned to each other at (x, y): a hinge.
pivot.force
what the pin is carrying this step, in newtons. Read only.
pivot.x, pivot.y
where the pin is now, which for a hinge between two bodies moves with them. Read only.

A pin through a body. The point stays put and the body turns about it, which is a seesaw, a lever, a swinging door, a pendulum that is a plank rather than a bob on a string. Put the pin at the center and the body only spins; put it at an end and the body swings.

plank = Box(x=0, y=2, width=6, height=0.2, mass=2, friction=1.0, bounce=0)
Pivot(plank, 0, 2)                                      # a seesaw
heavy = Ball(x=-1, y=2.3, radius=0.2, mass=2, friction=1.0, bounce=0)
light = Ball(x=2, y=2.3, radius=0.2, mass=1, friction=1.0, bounce=0)

while sim.running:
    sim.monitor("tilt (deg)", round(plank.angle, 2))
    sim.step()

The numbers are the textbook's. The seesaw above balances because 2 kg x 1 m = 1 kg x 2 m, and its pivot carries 49 N, the weight of everything on it; move either weight and it tips toward the bigger torque. A plank pivoted at one end and let go from horizontal swings at alpha = m g (L/2) / (I + m (L/2)^2), which with the plate's I is within a percent of a thin rod's 3 g / (2 L), and it comes back up to within a degree of where it started.

Two bodies on one pivot never collide with each other. A hinge between two planks is inside both of them, and a contact there would fight the pin. Everything else touches them as usual, so a ball on the seesaw rolls along it and tips it.

See also Pin, which holds a distance rather than a point, and push(at=), a force off the center.

Pin(..., breaks=None) Rope(..., breaks=None)setting

breaks
newtons. Left out, the link holds whatever it is given and never parts.

Rope(a, b, breaks=50) parts the moment it carries more than 50 newtons. The link is then gone from the run and nothing holds the two ends. It costs nothing to ask for, because the force it parts at is the force the link was already working out in order to hold.

Use it for a tow line, an overloaded chain, a cable with a rating written on the drum.

from pysims import sim, Ball, Segment, Rope

Segment(-20, 0, 20, 0)
truck = Ball(x=0, y=0.5, radius=0.5, mass=5)
trailer = Ball(x=-3, y=0.5, radius=0.5, mass=40)
tow = Rope(truck, trailer, breaks=200)

while sim.running:
    truck.push(600, 0)                 # pull harder than the line can take
    sim.monitor("tow line", round(tow.tension, 1))
    sim.step()

The link object is still a Python name after it parts, and its tension stops changing. Watch sim.things or the drawing to see that it has gone, not the variable.

link.tensionsetting, read only

The force the link is carrying this step, in newtons. Positive pulls the two ends together. A rod goes negative when it is holding them apart, and a rope never does: slack, it carries nothing and reads 0.

In a textbook the tension in a string is the quantity you cannot see and have to work out. Here it is a number you can monitor, graph, and hold up against the weight it is carrying, which is most of what a tension problem asks you to believe.

bob = Ball(x=3, y=5, radius=0.3, mass=1)
rod = Pin(bob, 0, 8)
sim.graph("t", "tension")

while sim.running:
    sim.record("tension", rod.tension)
    sim.step()

It is on a Pin, a Rope and a Pulley and not on a Spring: a spring's pull is k times its stretch, and the program can work that out itself.

Contact and neighbors

sim.new_collisions()action, gives an answer

A list of the meetings since you last asked, oldest first, and asking empties it. Each one has .a and .b, the two things that touched, .x and .y, where they touched in meters, and .speed, how fast they were closing when they met.

A meeting is reported once, on the step the two come together. A ball that lands on the floor and stays there is one collision, not one a step for ever, so counting bounces is counting the list. To ask instead whether something is still resting on something, which is a different question, use body.touching.

Ask once per step. Ask twice and the second list is empty, which looks like the collisions stopped happening.

while sim.running:
    for hit in sim.new_collisions():
        if hit.a is ball or hit.b is ball:
            print("bang at", round(hit.x, 2), "doing", round(hit.speed, 2))
    sim.step()

A driven body is reported and never resolved, which is how you tell that your own hand hit something. Use .kind on .a or .b to ask what you were handed.

Two things make a count come out short, and both are silent. Two meetings inside one step are one meeting, because the step is fixed and nothing is hidden inside it, so a pair exchanging faster than sim.dt is undercounted and a shorter step is the whole cure. And a pair that never gets further apart than two millimeters has never parted, so it never meets again: that is the margin at which the engine calls two bodies touching, and nothing narrower is a gap. gallery/pi_collisions.py runs into both of them and works out its step and its spacing from the mass ratio because of it.

See also a region that watches, which is a body reported here and deliberately never resolved.

a region that watchessetting

sensor=False
True on a Ball, a Box or a Segment. Keyword only.

A sensor notices and never blocks. Every contact with it is reported through sim.new_collisions like any other, and then nothing happens: no impulse, no push-out, no trajectory bent by a hair. A finish line, a doorway, a tripwire, the region a model counts what is inside of.

A ball sent at vx=4 through a sensor gate comes out the far side still doing 4.000 while the gate reports the contact. The same gate without sensor=True does not let it past at all: it arrives, bounces, and goes back the way it came. Running the one line both ways is the way to see what the word means.

sim.gravity = 0
gate = Box(x=2, y=1, width=0.5, height=4, fixed=True, sensor=True,
           color="rgba(0, 200, 255, 0.3)")
runner = Ball(x=0, y=1, radius=0.3, vx=4)

while sim.running:
    for hit in sim.new_collisions():
        if hit.a is gate or hit.b is gate:
            print("crossed at", round(sim.time, 2), "still doing", round(runner.vx, 3))
    sim.step()

A sensor obeys layer like everything else, so a sensor on layer 2 sees layer-2 things and nothing else. Two sets of tripwires in the same place, each minding its own traffic, is one line of setting apiece.

The page draws a sensor as a broken dashed outline, because a line that stops nothing must not look like a line that stops everything.

A sensor is left out of the check that keeps a fast body from passing through a thin wall, because there is nothing there to bounce off. So something small and fast can cross a thin sensor inside a single step and never be reported at all. Make the sensor thicker than one step of travel: a ball doing 20 m/s covers a third of a meter in a sixtieth of a second, so a gate half a meter wide catches it and a bare line does not.

A Curve cannot be a sensor, and raises rather than pretending: a sensor is a shape things pass through and are counted in, and ground with a shape is not that.

See also Segment(belt=), the other setting that lives on the surface rather than on the body, and what every body takes for layer.

sim.near(thing, radius)action, gives an answer

thing
a body or an agent. Not a point.
radius
meters.

Everything within that many meters of that thing, nearest first, not counting the thing itself. This is the one call a flock or a herd is built on, and it is fast enough to ask for every agent every step: a thousand agents asking is a grid lookup, not a thousand comparisons each.

It ignores layers, because finding is not touching.

flock = [Agent(x=i - 10, y=0, shape="sheep") for i in range(20)]

while sim.running:
    for sheep in flock:
        close = sim.near(sheep, 3)
        if close:                      # nearest first
            sheep.face(close[0])
            sheep.forward(0.05)
    sim.step()

See also grid.near, which is about cells and does take a point.

The keyboard and the mouse

sim.keyssetting, read only

The set of key names held down right now: "left", "right", "up", "down", "space", "enter", "shift", "escape", "tab", "backspace", the letters "a" to "z", the digits "0" to "9", and "comma", "period", "minus", "equals", "slash".

It is polled, not a callback: you ask what is held down now, every step, which is how a game loop reads a keyboard and is simpler than being interrupted.

A program that reads this takes the plain keys for itself. While it runs the page stops using Space and the arrows for pause and step, so your game gets them. The bar's buttons still work, and so does Escape to stop.

while sim.running:
    if "right" in sim.keys:
        hero.push(40, 0)
    if "up" in sim.keys and abs(hero.vy) < 0.01:
        hero.vy = 10
    sim.step()

Click the canvas once before you expect keys: the page has to have the keyboard before it can pass it on.

sim.new_clicks()action, gives an answer

The clicks on the canvas since you last asked, and asking empties it. Each has .x and .y in meters, .button, and .thing, which is the body or agent under the pointer or None.

while sim.running:
    for click in sim.new_clicks():
        if click.thing is None:
            Ball(x=click.x, y=click.y, radius=0.3)
        else:
            click.thing.remove()
    sim.step()

sim.allow("drag")action

Lets the page drag a body the engine owns, and fling it on release. This is the only way anything outside your program moves a body, and it is off until you ask for it.

While a body is held it does not fall and nothing pushes it aside. Let go and it leaves at the speed your hand was going.

sim.allow("drag")

A fixed or driven body cannot be dragged: the first never moves and the second is yours to move.

See also sim.new_drags.

sim.new_drags()action, gives an answer

The drags that finished since you last asked. Each has .thing, .x, .y where it was let go, and .vx, .vy, how fast it was thrown. Nothing arrives unless you called sim.allow.

for drag in sim.new_drags():
    print("thrown at", round((drag.vx ** 2 + drag.vy ** 2) ** 0.5, 2), "m/s")

Data

A run records itself. Every ball and box the engine moves gets four columns, its x, its y and its two velocities, and one that turns gets its angle and its spin as well, and the row they land in is the same row your own numbers land in, so everything across a row describes one instant.

what a column is calledsettings

A body's columns are its name and a dot: ball1.x, ball1.y, ball1.vx, ball1.vy. Bodies are numbered in the order they were made, so the second ball is ball2 whatever you called the variable.

That number is fine until a graph has three lines in it and the legend reads ball1, ball2, ball3. name= in the call is how you fix that, and it is the only thing name= is for.

heavy = Ball(x=-4, y=5, radius=0.4, mass=3, name="heavy")
light = Ball(x=4, y=5, radius=0.4, mass=1, name="light")
sim.graph("t", ["heavy.y", "light.y"])

while sim.running:
    sim.step()

A fixed body gets no columns at all, because they would be columns of the same number repeated, so there is nothing to gain by naming a floor. An agent gets none either: the record is of the physics, and a model of sheep records what it wants with sim.record.

It is the name in the call and not the variable you keep the body in, because an object in Python cannot see the variable it was assigned to. cart = Ball(...) with no name= makes columns called ball1.x, and the console will call it ball1 too. Give the name when you are going to read the columns, and let it be numbered when you are not.

sim.record(name, value)action

name
what the column is called. A new name makes a new column.
value
a number you worked out.

Puts your own number in the row for this instant. The engine records positions and velocities on its own; this is for everything it refuses to compute, which is energy, momentum, period, range and every coefficient.

It does not matter whether you call it before or after sim.step: the value lands in the row for the moment it describes either way.

while sim.running:
    speed2 = ball.vx ** 2 + ball.vy ** 2
    sim.record("kinetic", 0.5 * ball.mass * speed2)
    sim.record("potential", ball.mass * 9.8 * ball.y)
    sim.step()

See also sim.graph to watch a column, and sim.save to keep it.

sim.monitor(label, value)action

Records a column and shows it beside the controls, updating as it goes. The same as sim.record with a readout attached. A number is shown to sim.digits figures; anything else is shown as it prints.

while sim.running:
    sim.monitor("height", ball.y)
    sim.monitor("on the ground", abs(ball.vy) < 0.01)
    sim.step()

sim.histogram(label, values, bins=10, low=None, high=None)action, gives a list

label
what is being counted. It names the pane and starts the name of every bin.
values
the numbers to count, worked out by you: one per body, per agent, per cell, per anything.
bins
how many bins, or a list of the edges you want: bins=[0, 1, 2, 5, 10] is four bins and the last is wide.
low, high
where the bins start and stop. Left out, they come from the first call's values.

Counts your numbers into bins and draws them as bars: how the thing you are measuring is spread, right now, rather than what one of them has done since the start. The bins are ordinary columns named "speed 0-1" and so on, so a distribution is in sim.data, in the file sim.save writes, and under the scrubber: park it anywhere in a finished run and the bars show the spread at that moment.

Gives back the counts, so a program that wants the tallest bin or the total does not count twice.

molecules = [ball, other, cart]
sim.gravity = 0

while sim.running:
    speeds = [(b.vx ** 2 + b.vy ** 2) ** 0.5 for b in molecules]
    sim.histogram("speed", speeds, bins=10, low=0, high=10)
    sim.step()

The range is fixed by the first call. An axis that moves under you is not one you can compare with what it showed a moment ago, which is the whole reason to watch a spread form. A value outside the bins is counted in the end bin, and the console says so once, with the low= and high= to write if you want a wider range.

counts = sim.histogram("energy", [a.energy for a in flock], bins=[0, 5, 10, 20, 50])
sim.monitor("hungriest group", max(counts))

See also sim.graph, which is the other question: what has one number done over time. A gas wants both, and the panel holds the distribution alongside the three graphs rather than instead of one of them.

The engine does the counting and nothing else. What the bins mean, what to measure, and what shape to expect are the lesson: a gas started with every molecule somewhere between -4 and 4 m/s in each direction has nothing slower than 1 m/s and nothing faster than 6, and twenty seconds of collisions turn that into the bell every chemistry book draws. The engine will not fit a curve to it for you.

sim.spread(label, value, bins=10, low=None, high=None)action, gives a list

label
what is being counted. It names the pane and starts the name of every bin.
value
one number, counted into a bin this step.
bins, low, high
as sim.histogram: a count or a list of edges, and a range fixed by the first call.

sim.histogram's sibling, and the other question. That one asks how a quantity is spread across many bodies at this moment; this one asks how often one quantity has sat at each value over the whole run. One number a step, and the counts add up.

while sim.running:
    sim.spread("where the bob is", bob.x, bins=12, low=-3, high=3)
    sim.step()

A pendulum answers that with a bucket and not a bell. It spends most of its time near the ends of the swing, where it is moving slowest, and hardly any in the middle, where it is fastest: measured over a minute, about 880 steps in each of the outer bins against 256 in each of the middle ones. Nothing in the program says so, and no graph against time shows it.

The counts only grow, so the bars climb and the shape settles rather than moving about. A range that is a physical bound is worth using: the bob can never get further from the pin than the rod is long, so low=-3, high=3 on a rod of 2.83 m can never be outside, however hard the bob is dragged.

A spread and a histogram cannot share a label, because their bins are the same column names. And a value outside the bins is counted in the nearest end, never dropped, with the console saying so once.

sim.bars(columns, color=)action

Draws columns you recorded as bars, read fresh every step. This is the pane sim.histogram is built on, for a program that would rather count for itself, or that is not counting at all: three bodies' speeds side by side, or a census a rule worked out.

sim.bars(["trees", "fire", "ash"])

while sim.running:
    for name in ("trees", "fire", "ash"):
        sim.record(name, g.count(name))
    sim.step()

The bars are the moment on show, so the pane follows the scrubber, and the count axis grows to the tallest bar the run has shown and never shrinks: a pane whose axis moved could not be compared with itself a moment earlier. Columns that share a first word are titled by it and labeled with the rest, which is what makes a histogram's bins read 0-1 and 1-2 under a pane called speed.

One pane of bars at a time, and it does not spend one of the three the panel draws: a program that has a distribution almost always wants a line beside it.

sim.datasetting, read only

The run so far, as a list of dicts, one per instant, keyed by the column names. This is the run in Python, for the analysis you do after it.

ball = Ball(x=0, y=8, radius=0.3, name="drop")   # the variable and the name
                                                 # need not match, and here do not
while sim.running:
    sim.step()

top = max(r["drop.y"] for r in sim.data)
print("it reached", round(top, 2), "meters")

A run longer than 18000 steps is thinned, keeping every tenth step and then every hundredth, so the record always spans the whole run instead of stopping. The steps you recorded something on are kept in full, and a note column appears saying what was done.

sim.columnssetting, read only

The column names in order, which is the header row of the CSV. This is how you walk the data without naming the bodies by hand.

for name in sim.columns:
    if name.endswith(".y"):
        print(name, "reached", round(max(r[name] for r in sim.data), 2))

sim.save(path, digits=None)action

path
a file name, like "run.csv". It lands in the files pane, where you can download it.
digits
significant figures to round the numbers in the file to. Left out, every figure goes in.

Writes the run as CSV: the column names, then one row per instant. This is the bridge out of pysims, into a spreadsheet or a notebook.

digits is the difference between 0.016666666666666666 and 0.01667 in a column of a spreadsheet. It rounds the file and never the run. The clock is written the way sim.dt makes it: a step of 0.1 writes tenths, and a step of a sixtieth is written in full, because 0.02 is not a sixtieth and anyone working a speed out of that column would get the wrong answer.

sim.save("run.csv")
sim.save("tidy.csv", digits=4)

sim.graph(x, y, colors=, lines=, square=, log=, stack=, band=, xaxis=, yaxis=)action

x
the column along the bottom. Given one argument only, it is t.
y
the column up the side, or a list of them to share one pair of axes.
colors
a color for a column, by name: {"total": "white"}. Any CSS color. Anything you do not name keeps the palette.
lines
how a column is drawn, by name: "solid", "dashed", "dotted", or "dots" for the points without a line through them. Anything else is an error. One value rather than a dict styles every column: lines="dots".
square
one scale on both axes, for a pane whose two columns are the same quantity: a path in meters, where a circular orbit has to come out round.
log
"y", "x" or "both": that axis is spaced by decades. A value at or below zero has no logarithm and is left off.
stack
pile the columns on top of each other, so the top line is their total.
band
two of the columns to shade between: band=("slowest", "fastest").
xaxis, yaxis
where that axis starts and stops, as (low, high), instead of fitting what has been drawn.

Draws a graph beside the simulation, growing as the run goes. Say it once, before the loop. The columns can be the engine's, like "ball.y", or your own from sim.record. A body you mean to graph is a body worth giving a name.

A list puts several columns on one pair of axes, each in its own color, with the readout along the top doubling as the legend. That is how you watch one kind of energy fall as another rises and the total stay flat.

sim.graph("ball.y")
sim.graph("t", ["kinetic", "potential", "total"])
sim.graph("ball.x", "ball.y")        # the path, not a time series

The x is a column like any other, so a graph need not be against time. Plot a position against a velocity and you have the phase portrait: a loop, gone round once a swing, which closes on itself exactly when nothing is taking energy out and spirals in when something is. Plot a position against a position and you have the path. For a path, where both axes are meters, square=True gives them one scale so a circular orbit comes out round instead of oval.

sim.graph("ball.y", "ball.vy")                    # a loop once a bounce
sim.graph("planet.x", "planet.y", square=True)    # the orbit, round

Expanded, a pane like this reads out the point nearest the pointer on the canvas rather than the nearest along the bottom, because a closed curve crosses every x twice. A graph against t is unchanged: there the pointer's x is the question.

An x for each column. x may be a list as long as y, and then every column brings its own x to the one pair of axes. With lines="dots" that is the diagram traffic flow is taught from: every car's position up the side and time along the bottom, where a jam is a dark band that drifts backwards while every car in it goes forwards. Dots rather than a line, because a line is drawn straight back across the pane every time a wrapped world sends a car round.

for i, c in enumerate(cars):
    sim.record(f"car {i}", c.x)
sim.graph("t", [f"car {i}" for i in range(len(cars))], lines="dots")

A log axis, where a straight line means a constant factor. This is the difference between it is getting smaller and it is halving every second, and an evenly spaced axis cannot show it: everything interesting happens in the first tenth of the pane and the rest is a line along the bottom. gallery/terminal_velocity.py graphs the gap between the falling speed and the terminal speed this way, and it comes out straight for fifteen decades.

sim.graph("t", "gap to terminal", log="y")

while sim.running:
    speed = (ball.vx ** 2 + ball.vy ** 2) ** 0.5
    terminal = (ball.mass * 9.8 / 0.5) ** 0.5      # where this drag balances the weight
    sim.record("gap to terminal", terminal - speed)
    sim.step()

Zero and the negatives have no logarithm, so those points are left off a log pane and the console says how many once. A column that is sometimes negative wants an even axis, or a different column: the thing worth graphing on a log axis is usually a gap, a difference or an amount left.

A stack, so a total is a line and not a sum in your head. stack=True piles the columns, and the top of the pile is their total: for the two energies of a bouncing ball, that is a flat ceiling you can watch hold between bounces and drop at each one. Each column keeps its own number in the record and in the readout; it is only the drawing that is piled.

sim.graph("t", ["ke", "pe"], stack=True)

A stack wants columns that are parts of one whole. gallery/forest_fire.py stacks its census, where every cell that was a tree is now a tree, a fire or ash, so the ceiling never moves and the fire is a band crossing the pane.

An axis you set yourself. A pane otherwise fits its axis to what has been drawn, which has two costs: the axis moves as the run goes, so the same quantity drawn twice is drawn at two scales and the two cannot be compared; and it starts at the lowest value there is, so for anything that never comes near zero a small change looks like the whole story. yaxis=(0, 30) says where it starts and stops. The reader can still zoom an expanded pane; this is where it starts from.

sim.graph("t", "hits per second", yaxis=(0, 30))
sim.graph("t", ["trees", "burning", "ash"], stack=True, yaxis=(0, 4000))

A stacked pane always has zero on its axis whether you ask or not, because a stack is measured from zero and a band drawn from zero that sits below the window is a band the reader cannot see.

A band, so an ensemble is one pane. Record the smallest, the middle and the largest of whatever you have many of, and band= shades between two of them. Twenty-five cars become a band with a line in it, and a jam is the moment the bottom of the band falls to zero while the top stays high, which no single average shows.

sim.graph("t", ["slowest", "average", "fastest"], band=("slowest", "fastest"))

while sim.running:
    speeds = [c.speed for c in cars]
    sim.record("slowest", min(speeds))
    sim.record("average", sum(speeds) / len(speeds))
    sim.record("fastest", max(speeds))
    sim.step()

A body lends its color to its own lines. Give a ball a color and its columns are drawn in it without being asked, so the orange ball's height is an orange line and a graph of two balls needs no legend at all. A body you left white keeps the palette, because two white bodies would draw the same line twice. A color too dark to see on the panel is lifted off it and keeps its hue, so a navy ball is still blue and still readable.

red = Ball(x=-6, y=0.5, radius=0.5, mass=2, vx=3, color="red", name="red")
blue = Ball(x=5, y=0.5, radius=0.5, mass=1, vx=-1, color="blue", name="blue")
sim.graph("t", ["red.vx", "blue.vx"])            # a red line and a blue one

sim.graph("t", ["kinetic", "potential", "total"],
          colors={"total": "white"}, lines={"total": "dashed"})

Dashes are worth more than they look: they are the one difference that survives a photocopy and a reader who cannot tell the colors apart. The usual use is one line for what you measured and a dashed one for what the theory says.

Naming a column in colors or lines that this graph does not plot is an error, because it is always a typo. A color the page cannot parse is not: that line falls back to the palette and the console says which color it did not know.

A column is named by a string, so it can be asked for one that does not exist. A pane with nothing to draw says no column called ball.y across itself rather than sitting blank, and when the run ends the console names every column it did have, which is the quickest way to find out that your ball is called ball1.

Three graphs is the most the panel draws. A fourth is named in the panel and the console rather than quietly dropped; several columns on one pair of axes is the way round it.

sim.scatter(x, y, color=, square=, xaxis=, yaxis=)action

x, y
lists of column names, as long as each other. A dot for each pair.
color
one color for the whole cloud. The dots are all the same quantity, so they are all the same color.
square
one scale on both axes, which a velocity space wants: the spread of a gas is the same in every direction and should look it.
xaxis, yaxis
where an axis starts and stops. Left out, the axes grow to hold everything the run has shown and never shrink.

A dot for each pair of columns, at the moment on show. This is the two-column sibling of sim.histogram: that one asks how one quantity is spread right now, this one asks how two of them go together. A cloud and not a history, so it follows the scrubber, and the axes grow to hold everything the run has shown and never shrink.

molecules = [ball, other, cart]
sim.scatter([m.name + ".vx" for m in molecules],
            [m.name + ".vy" for m in molecules], square=True)

That is a gas in velocity space, and nothing had to be recorded for it: every body the engine moves already has .vx and .vy columns. It fills a disc, not a ring and not a cross, because nothing in the box prefers a direction, and the disc's edge is the fastest molecule there is.

A cloud of the whole history would be a smear: sixty molecules over a minute is two hundred thousand points, and all but sixty of them are of a moment gone by. So this draws one moment, and the scrubber is how you look at the others.

Controls

sim.slider(label, low=0, high=1, start=None)action, gives a control

label
what it is called on the page. The same label twice is the same slider.
low, high
the ends.
start
where it begins. Left out, it begins at the low end.

A slider on the page. Keep what it gives back and read .value, usually every step. This is how a model becomes something a reader can play with rather than only watch.

A slider stays where you put it. Move it, press Run again, and the program gets the value on the screen rather than the start in the code, from its very first line, so a slider can decide how a body is made. Three things put it back: the reset button, which appears in the panel once something has been moved; reloading the page; and changing the start in the code, because a slider the program describes differently is a new slider and the program wins.

It also carries .label, .low, .high and .start, which are what it was made with.

g = sim.slider("gravity", 0, 30, start=9.8)
heavy = sim.slider("mass", 1, 9, start=2)

ball = Ball(x=0, y=6, radius=0.4, mass=heavy.value)
while sim.running:
    sim.gravity = g.value
    sim.step()

sim.switch(label, start=False)action, gives a control

A checkbox. .value is True or False. It keeps its position across runs the same way a slider does, and a switch has no .low or .high.

air = sim.switch("air resistance", start=True)

while sim.running:
    if air.value:
        ball.push(-0.5 * ball.vx, -0.5 * ball.vy)
    sim.step()

Instruments

Things the page draws. None of them changes the run by so much as a bit: turn them all on and a drop lands in exactly the same place.

sim.strobe = 0setting

Seconds between ghosts. Set it to 0.1 and every moving thing leaves a faint copy of itself every tenth of a second: the motion diagram out of a textbook, where even spacing is constant speed and spreading out is acceleration. 0 turns it off. Scenery is not ghosted.

sim.strobe = 0.1

sim.trail = Falsesetting

True draws the path behind each moving thing: the last fifteen seconds of it, not the whole run. An orbit, a projectile's arc, the track of a wandering sheep.

sim.trail = True

sim.show = Nonesetting

An arrow on every body. "velocity" for how fast and which way, "force" for the net force including gravity, and None for neither.

"velocity xy" and "force xy" draw the same arrow and its two parts, from the body, with the rectangle they close. That is the diagram a physics lesson draws by hand, and on a projectile it shows the sideways part never changing while the upward one falls away at g.

Arrows are scaled like a textbook diagram: the biggest one seen so far in the run spans a little under an eighth of the view's width and the rest are in proportion, so they can be compared with each other.

sim.show = "velocity xy"

Any other word raises an error rather than drawing something else. A typo used to draw velocity arrows in silence.

Agents

Agent(x, y, heading=0, color="white", size=0.5, shape="arrow")action, gives an agent

x, y
where it is, in meters.
heading
degrees counterclockwise from the x direction: 0 points right, 90 up.
size
meters across.
shape
one of eleven, below. Five of them turn to face the heading and six stand the same way up whatever the heading is.

A thing with a heading that you move. The engine never touches it: no mass, no velocity, no gravity, no contact. It is the NetLogo turtle, and it is for models where the rule is a sentence rather than a force.

Any attribute you set on it is yours: sheep.energy = 10 is how a model of hunger is written. It is not in sim.data, so record what you want to keep.

import random

flock = []
for i in range(40):
    sheep = Agent(x=random.uniform(-8, 8), y=random.uniform(-5, 5),
                  heading=random.uniform(0, 360), shape="sheep", color="silver")
    sheep.energy = 10                 # yours, from the moment you set it
    flock.append(sheep)

Asking for a mass is an error, on purpose. An agent has no physics, and a mass that did nothing would be a promise the engine does not keep.

Turns to face the heading
arrowthe default: a dart, so you can see which way it points
trianglethe same idea with a flat back
fisha body and a tail fin
bugan oval body with a small round head
cara box with four wheels
Stands the same way up
circlea disc, when the heading does not matter
squarea block
sheepa round body on four legs, with a head
wolfa lean body on four legs, with a snout, a pricked ear and a raised tail
personhead, body, arms and legs
treea canopy over a trunk

A shape that turns is drawn pointing along the x direction at heading 0 and rotates from there. One that does not is drawn upright however the agent is facing, which is what you want for a sheep: a sheep walking left is still a sheep the right way up.

Every shape is a path, not a picture, so it is sharp at any size and takes the agent's color. An agent can also wear a label.

how an agent movesactions and settings

a.forward(d)
move d meters the way it faces. a.back(d) is the other way.
a.turn(deg)
turn left by that many degrees. Negative turns right.
a.heading
degrees counterclockwise from the x direction. Set it directly to point somewhere.
a.face(other)
turn to face a thing, or a point given as a pair.
a.towards(other)
the heading that would face it, without turning. For deciding before committing.
a.distance(other)
meters between centers.
a.remove()
take it out. Remove it from your own list too.
for sheep in list(flock):
    sheep.turn(random.uniform(-20, 20))
    sheep.forward(0.08)
    sheep.energy -= 0.1
    if sheep.energy <= 0:
        sheep.remove()
        flock.remove(sheep)

Loop over a copy when you remove while looping: for s in list(flock). Removing from the list you are walking skips the next one, and the bug looks like half your sheep being immortal.

Cells

Grid(width, height, cell=1, x=0, y=0)action, gives a grid

width, height
how many cells across and up. Whole numbers.
cell
how many meters across one cell is.
x, y
the bottom left corner, in meters. Read back as g.x0 and g.y0.

A grid of cells, each an object you can read and write. The NetLogo patch. A grid made before any sim.view call fills the view, which is usually what you wanted.

Read back: g.width, g.height, g.cell, g.x0, g.y0.

g = Grid(40, 30, cell=0.5)
g.fill("tree")

a cellsettings

g[i, j] gives the cell object at those indices, counted from the bottom left. Each cell carries two things the page can paint, and any attribute of your own.

c.name
a string, or None. g[i, j] = "tree" sets it.
c.value
a number. g[i, j] = 3 sets it.
c.x, c.y
its indices.
c.cx, c.cy
its center, in meters.
g[4, 7] = "fire"
g[4, 7].heat = 3            # your own, and the engine will not touch it
print(g[4, 7].name, g[4, 7].cx)

reading a gridactions, give answers

g.cells()
every cell, row by row from the bottom left.
g.at(thing)
the cell under a body or an agent, or under a point given as a pair. None if it is off the grid.
g.near(cell, 1)
the four neighbors. 1.5 gives the eight. Takes a cell or a point. The grid does not wrap, so a cell at the edge has fewer.
g.count("tree")
how many cells over the whole grid have that name.

What cells() and near() give back is a list that also answers .count("fire") and .with_name("fire"), so a rule reads like a sentence.

under = g.at(sheep)
if under is not None and under.name == "grass":
    under.name = None
    sheep.energy += 4

burning = g.near(cell, 1).count("fire")

painting a gridactions

g.colors({"tree": "green"})
the color for each name. Names you do not list get colors of their own.
g.scale(low, high, color_a, color_b)
paint values instead: low is the first color, high the second, with everything between mixed. Any CSS color, alpha included, so "rgba(0,0,0,0)" at the low end leaves empty cells clear.
g.show("value")
which of the two is painted when both are set. "name" is the other.
g.fill("tree")
set every cell at once, to a name or a number.
g.colors({"tree": "forestgreen", "fire": "orangered", "ash": "#444"})
g.scale(0, 5, "rgba(0,0,0,0)", "limegreen")
g.show("value")

grid.copy()action, gives an answer

A copy of the grid as it is now, to read from while you write to the real one. This is the whole of how a cellular automaton works, and the single line that makes the difference between Life and nonsense.

Without it, a cell you changed earlier in the loop is read as already changed by a cell you reach later, so fire crosses the forest in one step along whichever way the scan happens to run, and Life does not work at all.

before = g.copy()
for c in before.cells():
    if c.name == "tree" and before.near(c, 1).count("fire") > 0:
        g[c.x, c.y] = "fire"

The copy answers cells(), near(), at() and count() like any grid. It is not drawn.

Angles, units and the three rungs

Degrees everywhere, measured from the x direction, counterclockwise: 0 points right, 90 up. That is an agent's heading, a fixed box's angle, and a curve's angle. Meters, seconds, kilograms and newtons everywhere else.

A program that needs a sine or a cosine uses Python's: math.sin(math.radians(d)). Remember the radians, because math.sin does not take degrees. The package has its own sine and cosine in degrees, used by the engine so that a run comes out identical on every machine, but they are not part of the API and a program cannot import them.

There are three ways to move something, and a program may use all three at once:

Nothing on this page computes a quantity a lesson teaches. There is no energy, no momentum, no period, no range and no coefficient of anything, because each of those is one line of your own Python and writing it is the point.

There is no water, either, and no air. A fluid is not something pysims has; a force is, and Archimedes is three lines of your own: how much of the block is below the waterline, the volume of water that would have been there, and the weight of that water, pushed up.

under = min(max(0 - (block.y - block.height / 2), 0), block.height)   # below the waterline at y = 0
volume = block.width * under                 # for a block a meter deep into the screen
block.push(0, 1000 * 9.8 * volume)           # the weight of the water it displaced, up

A block denser than water sinks, and one less dense settles with exactly its density's share under; draw the water as a sensor Box, so it is a region and not a wall. The gallery's Floating and sinking program is this with a density slider. Air drag is one line the same way, as the gallery's Terminal velocity shows, and so is the lift on a wing or the thrust of a rocket: anything a fluid does to a body is a push you can write.