Skip to content

Rolling resistance, live

A body rolling along the ground over a step, slowed by its rolling resistance, the deceleration μ_r g against its motion: v′ = v − sign(v) μ_r g dt, never past zero, so a body slowed to rest stops rather than reversing. Under a constant deceleration a ball from v₀ stops after v₀ / (μ g), having run v₀² / (2 μ g). Below, three balls are rolled from 3 m/s on grounds of μ = 0.05, 0.1 and 0.2, slowed by the library's rolling resistance (lusine.integrator.rolling_resistance), a hundredth of a second a step and a hundred steps a second, on your GPU as you read. Beside them four balls roll on a world of 8 m/s² with μ = 1/8, at steps of 1/16 to 1/128 s, where every number of the roll is exact in binary32, and a ball that changes each step is held never to reverse.

This page runs the program on your GPU through WebGPU, so it needs a browser with WebGPU (a recent Chrome, Edge, Safari or Firefox). Without it the program does not run and nothing is drawn below.

What it shows

  • The strip above the controls is green while every check holds, and turns red for good once one fails.
  • The roll: the chart draws the three balls' speeds against the time, a point a step: each falls in a straight line of slope −μ g and stays at rest once it reaches zero. Every seven seconds they are rolled again. Below it, the time, and when the smoothest and the roughest ball stopped and how far each ran: 6.12 s and 9.16 m, 1.53 s and 2.28 m, each a hair short of v₀² / (2 μ g) for the steps' sum.
  • The order: from 2 m/s on the order's world a ball stops after 2 s and 2 m; the distance summed step by step falls short by exactly v₀ dt / 2, so halving the step halves the shortfall. Below, the distance run at the coarsest and the finest step.
  • The checks: stops on time, each ball stopped within two steps of v₀ / (μ g); runs its distance, within 3 v₀ dt / 2 and 0.1% of v₀² / (2 μ g); first order, each of the order's balls short of 2 m by exactly v₀ dt / 2; never reverses, the changing ball never turned round or sped up, and the same ball rolling the other way exactly its mirror; the limits, no resistance leaving a speed exactly alone, and a ball slower than a step's slowing, or exactly at it, stopped at zero either way. Each is held at every step.

Every number here is computed by the program on the GPU; the page only reads the values back and draws them. Gravity, the balls, the grounds, the step and the order's world are the program's settings, which the host may write; this page offers no control to change them yet.

The program

The roll, the order's balls, the changing ball and the checks, the panels the page draws, and the one procedure, main, run each step: one step of every ball, the distances and stops after it, and the checks.

// The rolling resistance's live page: three balls rolled from 3 m/s on three grounds, slowed by the library's rolling
// resistance to rest, against the time v0 / (mu g) and the distance v0^2 / (2 mu g) they must stop in; four more
// rolled at four step lengths, the distance's first order shown exactly; and a ball that changes each step held never
// to reverse, with the page's checks computed here on the GPU and shown by the panels the program declares.

namespace rolling_resistance;

lusine.version version = { { number = 1 } };

// The reader's settings: gravity; the balls' starting speed; the three grounds' coefficients; the step; how long each
// roll is followed; and the order's world, its gravity, coefficient and starting speed, every number of whose roll is
// exact in binary32.
host struct {
  float<m/s2> gravity = 9.80665 m/s2; float<m/s> speed = 3.0 m/s; float smooth = 0.05; float middle = 0.1;
  float rough = 0.2; float<s> dt = 0.01 s; float<s> roll = 7.0 s;
  float<m/s2> order_gravity = 8.0 m/s2; float order_friction = 0.125; float<m/s> order_speed = 2.0 m/s;
} settings;

// The roll: its time; each ball's speed, the distance it has run, and the time it stopped (none until it has).
struct {
  float<s> time; float<m/s> smooth; float<m/s> middle; float<m/s> rough; float<m> smooth_run; float<m> middle_run;
  float<m> rough_run; float<s> smooth_stop; float<s> middle_stop; float<s> rough_stop;
} roll;

// The order's four balls, at steps of 1/16, 1/32, 1/64 and 1/128 s: each one's speed and distance.
struct {
  float<m/s> v16; float<m/s> v32; float<m/s> v64; float<m/s> v128; float<m> run16; float<m> run32; float<m> run64;
  float<m> run128;
} order;

// A ball that changes each step: its angle, speed, coefficient and step, and its speed after.
struct { float turn; float<m/s> speed; float friction; float<s> dt; float<m/s> after; } turning;

// The checks: each ball stopped within two steps of v0 / (mu g), and having run within 3 v0 dt / 2 and 0.1% of
// v0^2 / (2 mu g); on the order's world each ball's distance short of 2 m by exactly v0 dt / 2, first order in the
// step; no ball ever reversing or speeding up, and a ball rolling the other way exactly the mirror; no resistance
// leaving a speed exactly alone, and a ball slower than a step's slowing stopped at zero, either way.
struct {
  bool stops = true; bool distance = true; bool order = true; bool never_reverses = true; bool limits = true;
} checks;

// A strip of lamps the page draws, lit green while every check holds and red once one fails.
struct lamp { vector<float, 4> colour; };
lattice<lamp, 2> lamps(spacing = 1 m, boundary = wall);

vector<float, 4> lamp_colour(bool holds)
{
  return holds ? vector<float, 4>(0.15, 0.6, 0.3, 1.0) : vector<float, 4>(0.75, 0.15, 0.15, 1.0);
}

float<s> stopped_at(float<s> stop, float<m/s> velocity, float<s> time)
{
  return stop == 0.0 s && velocity == 0.0 m/s ? time : stop;
}

// Stopped within two steps of v0 / (mu g), once it has stopped.
bool on_time(float<s> stop, float<m/s> speed, float friction, float<m/s2> gravity, float<s> dt)
{
  return stop == 0.0 s || abs(stop - speed / (friction * gravity)) <= 2.0 * dt;
}

// Run within 3 v0 dt / 2 and 0.1% of v0^2 / (2 mu g), once it has stopped.
bool ran(float<s> stop, float<m> run, float<m/s> speed, float friction, float<m/s2> gravity, float<s> dt)
{
  return stop == 0.0 s || abs(run - speed * speed / (2.0 * friction * gravity)) <= 1.5 * speed * dt + 0.001 * run;
}

// Short of the order's 2 m by exactly v0 dt / 2, once stopped.
bool short_by(float<m/s> velocity, float<m> run, float<m/s> speed, float<s> dt)
{
  return velocity != 0.0 m/s || 2.0 m - run == 0.5 * speed * dt;
}

// Never reversed nor faster, and the mirror exact.
bool forward(float<m/s> speed, float<m/s> after, float friction, float<m/s2> gravity, float<s> dt)
{
  let mirror = lusine.integrator.rolling_resistance(velocity = -speed, friction, gravity, dt);
  return (speed >= 0.0 m/s ? after >= 0.0 m/s : after <= 0.0 m/s) && abs(after) <= abs(speed) && mirror == -after;
}

bool limits(float<m/s2> gravity)
{
  return lusine.integrator.rolling_resistance(velocity = 1.5 m/s, friction = 0.0, gravity, dt = 0.1 s) == 1.5 m/s
      && lusine.integrator.rolling_resistance(velocity = -0.25 m/s, friction = 0.0, gravity, dt = 0.1 s) == -0.25 m/s
      && lusine.integrator.rolling_resistance(velocity = 0.01 m/s, friction = 0.05, gravity, dt = 0.5 s) == 0.0 m/s
      && lusine.integrator.rolling_resistance(velocity = -0.01 m/s, friction = 0.05, gravity, dt = 0.5 s) == 0.0 m/s
      && lusine.integrator.rolling_resistance(velocity = 0.5 m/s, friction = 0.125, gravity = 8.0 m/s2, dt = 0.5 s)
         == 0.0 m/s
      && lusine.integrator.rolling_resistance(velocity = -0.5 m/s, friction = 0.125, gravity = 8.0 m/s2, dt = 0.5 s)
         == 0.0 m/s;
}

output table<lusine.widget> panels = {
  lusine.series { { panel = "the roll", chart = "speed", quantity = roll.smooth, steps = 700, against = roll.time,
                    label = "mu = 0.05" } },
  lusine.series { { panel = "the roll", chart = "speed", quantity = roll.middle, steps = 700, against = roll.time,
                    label = "mu = 0.1" } },
  lusine.series { { panel = "the roll", chart = "speed", quantity = roll.rough, steps = 700, against = roll.time,
                    label = "mu = 0.2" } },
  lusine.value { { panel = "the roll", quantity = roll.time, label = "time" } },
  lusine.value { { panel = "the roll", quantity = roll.smooth_stop, label = "mu = 0.05 stopped at" } },
  lusine.value { { panel = "the roll", quantity = roll.smooth_run, label = "having run" } },
  lusine.value { { panel = "the roll", quantity = roll.rough_stop, label = "mu = 0.2 stopped at" } },
  lusine.value { { panel = "the roll", quantity = roll.rough_run, label = "having run" } },
  lusine.value { { panel = "the order", quantity = order.run16, label = "run at 1/16 s" } },
  lusine.value { { panel = "the order", quantity = order.run128, label = "run at 1/128 s" } },
  lusine.value { { panel = "the checks", quantity = checks.stops, label = "stops on time" } },
  lusine.value { { panel = "the checks", quantity = checks.distance, label = "runs its distance" } },
  lusine.value { { panel = "the checks", quantity = checks.order, label = "first order" } },
  lusine.value { { panel = "the checks", quantity = checks.never_reverses, label = "never reverses" } },
  lusine.value { { panel = "the checks", quantity = checks.limits, label = "the limits" } },
};

void main()
{
  // Every roll's seconds the balls are set rolling again.
  roll.time = roll.time >= settings.roll ? 0.0 s : roll.time;
  roll.smooth = roll.time == 0.0 s ? settings.speed : roll.smooth;
  roll.middle = roll.time == 0.0 s ? settings.speed : roll.middle;
  roll.rough = roll.time == 0.0 s ? settings.speed : roll.rough;
  roll.smooth_run = roll.time == 0.0 s ? 0.0 m : roll.smooth_run;
  roll.middle_run = roll.time == 0.0 s ? 0.0 m : roll.middle_run;
  roll.rough_run = roll.time == 0.0 s ? 0.0 m : roll.rough_run;
  roll.smooth_stop = roll.time == 0.0 s ? 0.0 s : roll.smooth_stop;
  roll.middle_stop = roll.time == 0.0 s ? 0.0 s : roll.middle_stop;
  roll.rough_stop = roll.time == 0.0 s ? 0.0 s : roll.rough_stop;
  order.v16 = roll.time == 0.0 s ? settings.order_speed : order.v16;
  order.v32 = roll.time == 0.0 s ? settings.order_speed : order.v32;
  order.v64 = roll.time == 0.0 s ? settings.order_speed : order.v64;
  order.v128 = roll.time == 0.0 s ? settings.order_speed : order.v128;
  order.run16 = roll.time == 0.0 s ? 0.0 m : order.run16;
  order.run32 = roll.time == 0.0 s ? 0.0 m : order.run32;
  order.run64 = roll.time == 0.0 s ? 0.0 m : order.run64;
  order.run128 = roll.time == 0.0 s ? 0.0 m : order.run128;
  roll.smooth = lusine.integrator.rolling_resistance(velocity = roll.smooth, friction = settings.smooth,
                                                     settings.gravity, settings.dt);
  roll.middle = lusine.integrator.rolling_resistance(velocity = roll.middle, friction = settings.middle,
                                                     settings.gravity, settings.dt);
  roll.rough = lusine.integrator.rolling_resistance(velocity = roll.rough, friction = settings.rough,
                                                    settings.gravity, settings.dt);
  roll.time = roll.time + settings.dt;
  roll.smooth_run = roll.smooth_run + roll.smooth * settings.dt;
  roll.middle_run = roll.middle_run + roll.middle * settings.dt;
  roll.rough_run = roll.rough_run + roll.rough * settings.dt;
  roll.smooth_stop = stopped_at(stop = roll.smooth_stop, velocity = roll.smooth, roll.time);
  roll.middle_stop = stopped_at(stop = roll.middle_stop, velocity = roll.middle, roll.time);
  roll.rough_stop = stopped_at(stop = roll.rough_stop, velocity = roll.rough, roll.time);
  order.v16 = lusine.integrator.rolling_resistance(velocity = order.v16, friction = settings.order_friction,
                                                   gravity = settings.order_gravity, dt = 0.0625 s);
  order.v32 = lusine.integrator.rolling_resistance(velocity = order.v32, friction = settings.order_friction,
                                                   gravity = settings.order_gravity, dt = 0.03125 s);
  order.v64 = lusine.integrator.rolling_resistance(velocity = order.v64, friction = settings.order_friction,
                                                   gravity = settings.order_gravity, dt = 0.015625 s);
  order.v128 = lusine.integrator.rolling_resistance(velocity = order.v128, friction = settings.order_friction,
                                                    gravity = settings.order_gravity, dt = 0.0078125 s);
  order.run16 = order.run16 + order.v16 * 0.0625 s;
  order.run32 = order.run32 + order.v32 * 0.03125 s;
  order.run64 = order.run64 + order.v64 * 0.015625 s;
  order.run128 = order.run128 + order.v128 * 0.0078125 s;
  turning.turn = turning.turn + 0.53;
  turning.speed = 3.0 m/s * cos(1.9 * turning.turn);
  turning.friction = 0.15 + 0.15 * sin(turning.turn);
  turning.dt = 0.0505 s + 0.0495 s * cos(0.7 * turning.turn);
  turning.after = lusine.integrator.rolling_resistance(velocity = turning.speed, turning.friction, settings.gravity,
                                                       turning.dt);
  checks.stops = checks.stops
                 && on_time(stop = roll.smooth_stop, settings.speed, friction = settings.smooth, settings.gravity,
                            settings.dt)
                 && on_time(stop = roll.middle_stop, settings.speed, friction = settings.middle, settings.gravity,
                            settings.dt)
                 && on_time(stop = roll.rough_stop, settings.speed, friction = settings.rough, settings.gravity,
                            settings.dt);
  checks.distance = checks.distance
                    && ran(stop = roll.smooth_stop, run = roll.smooth_run, settings.speed, friction = settings.smooth,
                           settings.gravity, settings.dt)
                    && ran(stop = roll.middle_stop, run = roll.middle_run, settings.speed, friction = settings.middle,
                           settings.gravity, settings.dt)
                    && ran(stop = roll.rough_stop, run = roll.rough_run, settings.speed, friction = settings.rough,
                           settings.gravity, settings.dt);
  checks.order = checks.order
                 && short_by(velocity = order.v16, run = order.run16, speed = settings.order_speed, dt = 0.0625 s)
                 && short_by(velocity = order.v32, run = order.run32, speed = settings.order_speed, dt = 0.03125 s)
                 && short_by(velocity = order.v64, run = order.run64, speed = settings.order_speed, dt = 0.015625 s)
                 && short_by(velocity = order.v128, run = order.run128, speed = settings.order_speed,
                             dt = 0.0078125 s);
  checks.never_reverses = checks.never_reverses
                          && forward(speed = turning.speed, after = turning.after, turning.friction, settings.gravity,
                                     turning.dt);
  checks.limits = checks.limits && limits(settings.gravity);
  lamps.colour = lamp_colour(holds = checks.stops && checks.distance && checks.order && checks.never_reverses
                                     && checks.limits);
}

The law's other proof (two hundred balls at random, never reversing) is on its proof page.