Every name a program can say, one line each, in the order a student meets them. The engine knows position, velocity, mass and time. It does not know energy, momentum, period, range or a coefficient of anything: those are one line of your own Python.
from pysims import sim, Ball, Box, Segment, Curve, Spring, Pin, Rope, Pulley, Pivot, Label, Agent, Grid
Defaults are shown as name=value. Name your arguments. Only the first few of any call can be given by position, and the rest must be written mass=2, not slotted in by counting commas. Writing Ball(0, 5, 0.3, 2) and meaning a mass of 2 is an error, and the page will say so.
The page shows where a program starts before it runs. Reset the scene, beside Stop (Ctrl+Shift+Enter), shows it again for the code as it is written now, without running it.
sim.step() | advance one step and show it; the loop's one line |
sim.running | True until Stop is pressed or sim.stop() is called |
sim.stop() | end the loop from the program |
sim.dt = 1/60 | the step, in seconds |
sim.tick | how many steps so far |
sim.time | seconds so far, exactly tick * dt, worked out afresh each step: change dt half way and the clock changes with it |
sim.reset() | clear every body and the record, keep the settings; a sweep calls this between runs |
sim.gravity = 9.8 | meters per second squared, downward; 0 for space |
sim.G = 6.674e-11 | the gravitational constant, for your own force law. The engine never reads it: an orbit is a push you wrote. Set it to 1 for a toy solar system |
sim.speed = 1 | 1 is real time, 0.1 is slow motion |
sim.realtime = True | False runs as fast as it can, for a sweep |
sim.wrap = False | True makes the view a torus: off one edge, on at the other |
sim.digits = 4 | significant figures the page shows a number with |
sim.view(width=, height=, center=, follow=) | what is shown, in meters; follow=body keeps a body centered |
sim.view_bounds() | what is shown right now, as (xmin, ymin, xmax, ymax) in meters |
sim.digits changes what is shown, in a graph's readout, a monitor and the readout under the pointer. 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.
x and y are always the center. Every body takes color, and any attribute you care to set on it (ball.team = "red" just works). name is only what its columns are called: see Data.
Ball(x, y, radius=0.5) | a circle |
Box(x, y, width=1, height=1) | a rectangle, upright unless it is fixed |
Segment(x1, y1, x2, y2) | a fixed line at any angle: a floor, a wall, a straight ramp. .angle reads back its slope in degrees |
Segment(..., vx=, vy=), Box(..., fixed=True, vx=) | a wall that moves: nothing pushes it back, it shoves aside what it meets, and a ball leaves it at the speed the two closed at. A piston, a paddle, a moving platform. A Curve cannot have one |
Segment(..., belt=3.0) | a conveyor: the surface runs at 3 m/s along the line, from the first end toward the second, and the segment itself never moves. Friction is the whole mechanism, so belt=3 with friction=0 does nothing at all; belt=-3.0 runs the other way. A crate on belt=3.0, friction=0.8 reaches 3.000 m/s and stays; a ball rolls along at 2/7 of it. Only a Segment: a Box has four faces and no obvious way round, and a Curve raises. Drawn with chevrons pointing the way the surface runs |
Ball(..., sensor=True), and on a Box or a Segment | a region that watches and never blocks: reported by sim.new_collisions() and never resolved, so no impulse, no push-out, no trajectory changed. A finish line, a doorway, a trigger, a region a model counts what is inside. A ball at vx=4 crosses one still doing 4.000. It obeys layer, so a sensor on another layer sees nothing; a Curve raises. Drawn as a broken dashed outline |
Curve(f, x0, x1) | fixed ground shaped like y = f(x); its own section below |
| Every ball and box takes | |
|---|---|
mass=1 | kilograms |
vx=0 vy=0 | the velocity it starts with, meters per second |
bounce=0.5 | 0 sticks, 1 bounces back as fast as it came; two bodies' values multiply. Below a crawl the bounce is dropped altogether, so a ball comes to rest instead of buzzing |
friction=0.3 | 0 is ice; of two bodies' values the smaller wins |
fixed=False | True never moves: a wall, the Sun |
driven=False | True means you move it; the engine draws it and reports contact, never moves it |
layer=0 | bodies touch only others on the same layer; a higher layer draws on top |
color="white" name=None | any CSS color, alpha included; the name its columns take, which only matters if you graph it. A Segment and a Curve are "gray" |
angle=0 on a Box | degrees counterclockwise, the way it is made; a fixed box at an angle is a ramp |
turns=False on a Box | True lets the engine turn it: a plate, m (w^2 + h^2) / 12, twisted by a push off its center and by whatever strikes it off center. It lands, rocks and lies down on a face exactly flat, stands on its end, topples when pushed hard enough at the top and rocks back when not, falls as a domino, and stacks. A Pivot through a box turns it with no flag needed |
spin=0 | degrees a second counterclockwise, the way it starts; refused on a body that cannot turn |
roll=True on a Ball | a ball rolls, as a solid sphere, on whatever grips it; "disc" or "hoop" for those shapes, False for a puck that cannot turn |
Label(text, x, y), Label(text, thing) | a label of its own: words at a point, or beside a thing, which it then follows. dx, dy are meters from that thing's center along the world's axes; color, size=1 (a multiple of the ordinary label, sized on the screen), line=True rules a line from the words to the thing. .text can change every step. It is writing, not a thing in the world: nothing collides with it, sim.near misses it, a click goes through it, wrap leaves it, and it has no columns. Removing the thing removes the label |
label="" | text written on the body: label="2 kg". Two carts of 1 kg and 3 kg look the same otherwise, and the collision is about the fact that they are not |
A Segment and a Curve are fixed scenery, so of that list they take only bounce, friction, layer, color and name. Giving one a mass or fixed=False is an error: they have no mass. A Segment also takes the vx, vy, belt and sensor of the rows above; a Curve takes none of the four and raises if it is given one.
Layers are the whole rule and there is no exception to them. Two pendulums on layers 1 and 2 swing through each other; a floor on layer 0 stops layer-0 things and nothing else. Pins and springs have no layer, and sim.near ignores layers because finding is not touching.
| On a body | |
|---|---|
.x .y | position, meters |
.vx .vy | velocity, meters per second |
.mass .bounce .friction .color .name .layer | read or set at any time |
.radius on a Ball, .width .height .angle on a Box | its shape |
.kind | "ball", "box", "segment", "curve", "agent": what a thing is, when you are handed one |
.push(fx, fy, at=None) | a force in newtons, for this step only; say it every step. at=(x, y) applies it there, and off the center that is a torque, (x - cx) fy - (y - cy) fx, on a body that turns |
.angle .spin .turns | which way it points in degrees and how fast that changes in degrees a second, both settable (setting the angle of a body the engine is turning says so once; place(x, y, angle=) is the way to mean it); whether the engine turns it, read only |
.turn_velocity(omega) | a force that turns the velocity without changing its speed, for this step only: the velocity is rotated by exactly omega * dt radians, counterclockwise when positive. The magnetic force on a charge: you write omega = q * B / m and its sign. The speed is kept to the last digit and the circle has radius m v / (q B), where a push of the same force spirals outward |
.distance(other) | meters between centers |
.label | text drawn on it, which you may change every step: cart.label = f"{cart.vx:.1f} m/s". A Segment and a Curve can wear one too, at the middle. Dark or light is chosen from what it sits on, so it reads on any color of body and in either theme |
body.place(x, y) | put it somewhere on purpose, with no sentence about it; the velocity is untouched |
.remove() | take it out |
On ground that grips it, a rolling ball spends part of its drop on spinning, so it reaches the bottom slower than one that slides: a sphere at sqrt(1/1.4) of the speed, a disc at sqrt(1/1.5), a hoop at sqrt(1/2).
Ball(..., roll=True) | a solid sphere; "disc" and "hoop" are the other shapes |
ball.turn | how far it has turned, in degrees; negative is rolling to the right |
ball.spin | how fast it is turning, in degrees a second |
A ball rolls because the ground grips it. On a frictionless surface it slides at g sin(theta); with grip of at least 2/7 tan(theta) a sphere rolls without slipping, and below that it slips while the spin catches up. Rolling, it does not rub: friction takes nothing and it swings in a valley for ever, so a lesson about friction wants a puck (roll=False) or a Box. Thrown at a floor it keeps 5/7 of its speed along the ground where a puck stops dead; on a conveyor it rolls along at 2/7 of the belt. It rolls on a Curve, a Segment and a Box alike, keeps its spin through a flight and a bounce, is thrown spinning with spin=, and turns a box it strikes off center if that box turns. The mark drawn on it is the turning, so you can watch it happen.
Ground with a shape. You write the shape as a function of x and the engine works out the slope at every point. The surface exists only between x0 and x1: past either end there is nothing, and a ball that gets there falls. Balls slide or roll along it; boxes and agents ignore it.
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)
The questions below are written curve. and in a program they carry the name you gave the curve, so here they are hill.height(x) and the rest.
Curve(f, x0=-5, x1=5) | plus bounce, friction, layer, color, name, and samples=200, which is how finely the page draws it and has nothing to do with the physics |
curve.center(x, radius) | where a ball of that radius has its center when it rests at x: the two numbers to start it at |
curve.height(x) | how high the ground is |
curve.slope(x) | how steep, as a rise over a run, so 1.0 is 45 degrees |
curve.angle(x) | how steep, in degrees |
curve.curvature(x) | one over the radius of the bend; positive where it holds 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) | whether there is surface there at all |
curve.points() | the sampled polyline, as (x, y) pairs |
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 there is a center call: height(x) + radius puts the ball slightly into the hillside and the first step shoves it out.
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, or the valley gentler than the ball.
Spring(a, b, k=10, length=None, damping=0) | Hooke's law between two bodies; length is the rest length and defaults to how far apart they are when you make it |
Pin(a, b) | a rigid rod between two bodies |
Pin(a, x, y) | a rigid rod to a fixed point: a pendulum |
Pin(..., length=) | a rod of your own length, rather than the distance it was made at |
Rope(a, b) | a rope between two bodies: it pulls and never pushes |
Rope(a, x, y) | a rope to a fixed point: a pendulum on a string |
Pulley(a, b, x, y) | a rope over a wheel at (x, y) with a body on each end: one side rises as the other falls; x2, y2 a second wheel or a table corner for b's side; ratio=2 a block and tackle; length, breaks, .tension like a rope. The wheel has no mass and no friction. The Atwood machine: 1 kg and 3 kg accelerate at exactly 4.9 with 14.7 N in the rope |
Pivot(body, x, y) | a pin through a body at (x, y): the point stays put and the body turns about it. A seesaw, a lever, a plank pendulum. A Box on one turns with no flag needed; a puck cannot go on one. .force is what the pin carries in newtons, .x .y where it is. A seesaw balances at m1 d1 = m2 d2 and its pivot carries the weight of everything on it |
Pivot(a, b, x, y) | a hinge: two bodies pinned to each other at (x, y), which moves with them. The two never collide with each other |
Rope slack | while its ends are closer together than it is long it does nothing at all, and is drawn sagging |
Pin(..., breaks=50) | parts the moment it carries more than 50 newtons; Rope takes it too |
link.tension | the force it is carrying this step, in newtons, read only; positive pulls the ends together, and a rod goes negative when it is holding them apart |
.remove() | on any of them; removing a body removes its links too |
sim.new_collisions() | the meetings since you last asked, each reported once on the step the two came together: each has .a, .b, .x, .y, .speed |
body.touching | the bodies it is resting on, pressed against or sliding along right now; empty in the air. The standing question, where new_collisions is the meeting one. A sensor is never in it |
a sensor body | reported here and never resolved; and left out of the check that catches a fast body, so make one thicker than one step of travel or something fast will cross it unseen |
sim.near(thing, radius) | every thing within radius meters of that one, nearest first; it takes a body or an agent, not a point |
sim.keys | the set of key names held down right now: "left", "a", "space", "comma" and so on |
sim.new_clicks() | clicks since you last asked: .x, .y in meters, .button, and .thing under it or None |
sim.allow("drag") | let a body the engine owns be dragged with the mouse and flung on release |
sim.new_drags() | the drags that ended since you last asked: .thing, .x, .y, .vx, .vy |
A program that reads sim.keys takes the plain keys for itself, so while it runs the page stops using Space and the arrows for pause and step.
Every ball and box the engine moves gets four columns, named for the body and a dot: ball1.x, ball1.y, ball1.vx, ball1.vy, numbered in the order the bodies were made, and one that turns gets ball1.angle and ball1.spin as well. Ball(..., name="bob") calls them bob.x and the rest instead, which is worth doing when a graph of three balls needs a legend you can read, and is not worth doing otherwise. It is the name in the call and not the variable, because a Python object cannot see the variable it was assigned to. A fixed body and an agent get no columns at all.
sim.record(name, value) | add a column you computed to the row for this instant; before or after sim.step() makes no difference |
sim.monitor(label, value) | the same, and show it beside the controls |
sim.spread(label, value, bins=10, low=, high=) | count one number into a bin every step and keep the counts: how often this quantity has sat at each value over the whole run, where histogram asks how many bodies are at each value now. A pendulum comes out a bucket, not a bell |
sim.bars([columns], color=) | draw columns you recorded as bars, read fresh every step: a census, or three bodies side by side. The pane histogram is built on |
sim.histogram(label, values, bins=10, low=, high=) | count your numbers into bins and draw them as bars: how something is spread now, rather than what one number has done since the start. The bins are columns named "speed 0-1", so they are in the data and the file too; the range is fixed by the first call, and a value outside it is counted in the end bin and said once. Gives back the counts. bins=[0, 1, 2, 5, 10] for edges of your own |
sim.data | the run so far, one dict per instant: t, then name.x, name.y, name.vx, name.vy for every ball and box that is not fixed, a driven one included, then your own columns. Everything in a row describes the same moment |
sim.columns | the column names, in order |
sim.save("run.csv") | write the run as CSV, into your files |
sim.save("run.csv", digits=4) | the same, with the numbers in the file rounded; the run itself is not rounded |
sim.graph(y) | plot a column against t as the run goes |
sim.graph(x, y, colors=, lines=) | a color and a line per column, by name: colors={"total": "white"}, lines={"total": "dashed"} with "solid", "dashed", "dotted". A body's own color is used for its columns without being asked |
| a pane asked for a column that does not exist says so across itself, and the console names the columns the run did have | |
sim.graph(x, y) | plot a column against another: a position against a velocity is the phase portrait, a position against a position is the path |
sim.graph(x, y, square=True) | one scale on both axes, so a circular orbit comes out round |
sim.graph(x, y, log="y") | that axis spaced by decades ("x", "y", "both"), where a straight line means a constant factor. Values at or below zero are left off |
sim.graph(x, y, stack=True) | pile the columns, so the top line is their total: two energies stacked make a flat ceiling where they are conserved |
sim.graph(x, y, band=(lo, hi)) | shade between two of the columns, which is how an ensemble becomes one pane: the band is the spread, the line inside it the middle |
sim.graph(x, y, yaxis=(low, high)) | where that axis starts and stops (xaxis= too), instead of fitting what has been drawn, so two runs are at one scale. Works on sim.scatter as well, and a distribution's yaxis is how tall the bars go |
sim.graph([xs], [ys], lines="dots") | an x for each column on one pair of axes, and the points without a line through them: every car's position against time is the space-time diagram. One value for lines or colors styles every column |
sim.scatter([xs], [ys], color=, square=) | a dot for each pair, at the moment on show: how two quantities go together across all the bodies, the way histogram shows how one is spread. Follows the scrubber; the axes only ever grow |
sim.graph("t", ["ke", "pe"]) | several columns on one pair of axes, each in its own color |
sim.things sim.bodies | every body and agent you made, and the bodies only, in the order you made them. Grids, springs and pins are in neither |
Three graphs is the most the panel draws, and it says so in the panel and the console when a fourth is asked for. Several columns on one pair of axes is the way round it.
A fixed body gets no columns, because they would be columns of the same number. An agent gets none either: the record is of the physics, so a model of sheep records what it wants with sim.record. A run longer than 18000 steps is thinned, keeping every tenth step and then every hundredth, so the record always spans the whole run; the steps you recorded something on are kept in full, and a note column appears in the CSV saying what was done.
g = sim.slider("gravity", 0, 30, start=9.8) | a slider; start defaults to the low end |
s = sim.switch("show trails", start=False) | a switch |
g.value | read it every step: a number, or True and False for a switch |
g.label g.low g.high g.start | what it was made with; a switch has no low or high |
A control stays where you put it. Move a slider, press Run again, and the
program gets the value on the screen, not the start in the code.
It is read before the program's first line, so a slider can decide how a body
is made: Ball(mass=heavy.value) works on the second run. The
reset button appears in the Controls panel once something has
been moved and puts every control back to what the program asks for; so does
reloading the page. Changing the start in the code is the third
way: a control the program describes differently is a new control, and the
program's number wins.
Read-only. They draw on the page and change nothing in the run.
sim.strobe = 0.1 | leave a ghost of everything that moves every 0.1 s: the motion diagram. Scenery is not ghosted |
sim.trail = True | draw the path behind each moving thing, the last fifteen seconds of it |
sim.show = "velocity" | an arrow on every body; "force" for the net force; None for none |
sim.show = "velocity xy" | the same arrow and its x and y parts, with the rectangle they close; "force xy" too |
| hover | the pointer's position in meters, top right of the canvas |
| drag on the canvas | a ruler: length in meters and angle in degrees; double-click clears it |
| the bar | run, stop, pause, a step back and a step on, and a speed the program never sees |
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, the rest in proportion. Stepping back walks the run already recorded, because the engine cannot run in reverse.
import math
from pysims import sim, Ball
sim.realtime = False
for degrees in range(10, 81, 5):
sim.reset()
speed = 14
ball = Ball(x=0, y=0, radius=0.3,
vx=speed * math.cos(math.radians(degrees)),
vy=speed * math.sin(math.radians(degrees)))
while sim.running and (ball.vy > 0 or ball.y > 0):
sim.step()
print(degrees, round(ball.x, 2))
sim.realtime = False runs as fast as it can. sim.reset() clears the bodies and the record and keeps the settings, so each run starts clean and the loop around them collects one number per run. Make the bodies again inside the loop: reset removed the last run's.
There is no ground in that sweep, on purpose. The stopping rule is the physics question itself, "has it come back to the height it left from", and a floor to bounce off would answer a different question. Add one and the longest throw stops being the 45 degrees the textbook promises, because a shallow throw bounces further.
A thing with a heading that you move. The engine never touches it: no mass, no velocity, no contact, no gravity. sim.near finds it and sim.wrap carries it across the edge. It is not in sim.data, so record what you want to keep.
Agent(x, y, heading=0, color="white", size=0.5, shape="arrow") | shapes that turn to face the heading: arrow, triangle, fish, bug, car. Shapes that stand the same way up: circle, square, sheep, wolf, person, tree |
.forward(d) .back(d) | move d meters the way it faces |
.turn(deg) | turn left by deg; right is negative |
.heading | degrees from +x, counterclockwise; set it directly if you like |
.face(other) .face((x, y)) | turn to face a thing or a point |
.towards(other) | the heading that would face it, without turning |
.distance(other) | meters between centers |
.remove() | take it out; remove it from your own list too |
a.energy = 10 | any attribute you set is yours |
Loop over a copy when you remove while looping: for s in list(sheep):. Removing from the list you are walking skips the next one.
A grid of cells, width by height, each cell meters across, with its bottom left corner at (x, y). Each cell is an object. You write the rule; the page paints the result.
g = Grid(width, height, cell=1, x=0, y=0) | made before any view() call, it fills the view |
g[i, j] | the cell object, by index from the bottom left |
g[i, j] = "tree" | set its name; g[i, j] = 3 sets its value |
c.name c.value | a string or None; a number |
c.x c.y | its indices |
c.cx c.cy | its center, in meters |
g.cells() | every cell, row by row from the bottom left |
g.at(agent) g.at((x, y)) | the cell under a thing or a point, or None |
g.near(cell, 1) | the four neighbors; 1.5 is the eight; it takes a cell or a point. The grid does not wrap, so a cell at the edge has fewer |
.count("fire") .with_name("fire") | on what near or cells gives back: how many, and which |
g.copy() | a copy to read from while you write to g |
g.count("tree") g.fill("tree") | over the whole grid |
g.colors({"tree": "green"}) | the color for each name; other names get colors of their own |
g.scale(0, 5, "brown", "green") | paint values instead: low is the first color, high the second; any CSS color, alpha included, so "rgba(0, 0, 0, 0)" at the low end leaves empty cells clear |
g.show("value") | which is painted when both are set |
g.width g.height g.cell | how many across, how many up, how many meters each |
g.x0 g.y0 | the bottom left corner in meters, the x and y it was made with |
The rule for a cellular automaton reads a copy and writes the grid:
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"
Without the copy, fire crosses the forest in one step along the scan direction, and Life does not work at all.
Degrees everywhere, measured from +x, counterclockwise: 0 points right, 90 up. Box(angle=20, fixed=True) tilts up to the right, and so does Curve.angle and an agent's heading.
A program that needs a sine or a cosine uses Python's: math.sin(math.radians(d)), as the projectile in the gallery does. 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 the same on every machine, but they are not part of the API and a program cannot import them.