A spinning ball's lift coefficient, live¶
Sawicki, Hubbard and Stronge's fit of a spinning ball's lift coefficient to its spin ratio S, the ball's surface speed about its centre over its speed through the air: C_L = 1.5 S below S = 0.1 and 0.09 + 0.6 S above, the two arms meeting at 0.15, measured to S of about 0.5. Below, the spin ratio is swept from 0 to 0.5, 0.005 a step, twenty steps a second, and at each the library's fit (lusine.laws.lift_coefficient) gives the coefficient on your GPU as you read.
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 ball: the chart draws the lift coefficient against the spin ratio, a point a step over one whole sweep: a steep line from nothing, bending at S = 0.1 into a shallower one. Below it, the spin ratio and the coefficient there.
- The checks: the coefficient at S = 0.1 and just below it, at the largest binary32 number under 0.1, the one on each arm; no spin, no lift, true while the coefficient at no spin has been exactly nothing; rises with S, while the coefficient has risen at every step of the sweep; the arms meet, while both values have stayed at 0.15, the upper arm's within binary32's rounding and the lower arm's, a hair below the bend, within a millionth.
Every number here is computed by the program on the GPU; the page only reads the values back and draws them. The sweep's step is the program's setting, which the host may write; this page offers no control to change it yet.
The program¶
The sweep, the law's answers and the checks, the panels the page draws, and the one procedure, main, run each step: the sweep moves on, the fit is asked at the spin ratio, at the bend and at no spin, and the checks are taken.
// The lift coefficient's live page: a ball's spin ratio swept over the range its fit was measured on, the library's
// lift coefficient at each, with the page's checks computed here on the GPU and shown by the panels the program
// declares.
namespace lift_coefficient;
lusine.version version = { { number = 1 } };
// The reader's settings: the sweep's step.
host struct { float stride = 0.005; } settings;
// The sweep: the step it is at, from 0 to 100 and round again, the spin ratio there, and whether a step has been
// taken before this one.
struct { integer step; float spin_ratio; bool begun; } sweep;
// The lift coefficient at the spin ratio and at the last step's; where the two arms meet, at S = 0.1, and just below
// it, at the largest binary32 under 0.1.
struct { float lift; float before; float meeting; float below; } law;
// The checks: no spin, no lift; the coefficient rising with the spin ratio at every step of the sweep; and the arms
// meeting at 0.15, the lower arm's value just below S = 0.1 within a millionth of it.
struct { bool no_spin = true; bool rises = true; bool meets = 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);
}
output table<lusine.widget> panels = {
lusine.series { { panel = "the ball", chart = "lift coefficient", quantity = law.lift, steps = 101,
against = sweep.spin_ratio, label = "C_L" } },
lusine.value { { panel = "the ball", quantity = sweep.spin_ratio, label = "spin ratio" } },
lusine.value { { panel = "the ball", quantity = law.lift, label = "lift coefficient" } },
lusine.value { { panel = "the checks", quantity = law.meeting, label = "at S = 0.1" } },
lusine.value { { panel = "the checks", quantity = law.below, label = "just below it" } },
lusine.value { { panel = "the checks", quantity = checks.no_spin, label = "no spin, no lift" } },
lusine.value { { panel = "the checks", quantity = checks.rises, label = "rises with S" } },
lusine.value { { panel = "the checks", quantity = checks.meets, label = "the arms meet" } },
};
void main()
{
sweep.step = sweep.step >= 100 ? 0 : sweep.step + 1;
sweep.spin_ratio = float(sweep.step) * settings.stride;
law.before = law.lift;
law.lift = lusine.laws.lift_coefficient(sweep.spin_ratio);
law.meeting = lusine.laws.lift_coefficient(spin_ratio = 0.1);
law.below = lusine.laws.lift_coefficient;
checks.no_spin = checks.no_spin && lusine.laws.lift_coefficient(spin_ratio = 0.0) == 0.0;
checks.rises = checks.rises && (!sweep.begun || sweep.step == 0 || law.lift > law.before);
sweep.begun = true;
checks.meets = checks.meets && abs(law.meeting - 0.15) <= 0.00000025 && abs(law.below - 0.15) <= 0.000001;
lamps.colour = lamp_colour(holds = checks.no_spin && checks.rises && checks.meets);
}
The fit's other proof (against a reference over the measured range) is on its proof page.