seabiscuit

Five sample policies

Examples

A few examples trained on synthetic data, with graphs drawn from the real compiler. Sample output from real runtime and Seabiscuit Cloud platform.

01 · shelf.sb

Shelf sag

planespriorscurveschecks

A phone scan of a shelf also catches the wall behind it and the brackets under it, at whatever heading the phone started. The wall is the biggest flat thing in the room, so it goes first. The user says roughly how high the shelf is, and that height becomes a prior: each point is weighted by how plausible its height is, and the weighted plane fit finds the board’s top.

A slice across the board, square to the wall, and a LOWESS curve along it give the sag, measured from the straight line between its ends, so a shelf that is merely tilted doesn’t count as sagging.

The pass rule is ordinary code with units: sag under span/360, and level within half a degree.

shelf.sbpolicy
policy(scan: Cloud, shelf_height: Length, height_sd: Length = 5 cm)    -> (sag: Length, tilt: Angle, ok: Bool) wall  = scan |> largest_plane(threshold: 1 cm) |> fit_plane   # the biggest flat thingboard = scan    |> discard(near(wall, within: 2 cm))     # the wall, and the brackets on it    |> weight(by: .z, prior: gaussian(shelf_height, sd: height_sd))    |> largest_plane(threshold: 5 mm)        # the board's toptop   = fit_plane(board)cut   = slice(board, plane(origin: top.origin, normal: wall.normal),              thickness: 2 cm)line  = lowess(cut, span: 0.3)               # along the board, end to enda, b  = line.domainlow   = argmin(line)chord = line(a) + (line(b) - line(a)) * (low - a) / (b - a)sag   = chord - line(low)tilt  = abs(atan2(line(b) - line(a), b - a))   # either end may be the high oneok    = sag < (b - a) / 360 and tilt < 0.5°        # span/360, and level
scan: CloudinputscanCloudshelf_height: Lengthinputshelf_heightLengthheight_sd: Lengthinputheight_sdLengthlargest_plane: Largest planelargest_planeLargest planeCloudfit_plane: Fit planefit_planeFit plane→ walldiscard: DiscarddiscardDiscardCloudgaussian: GaussiangaussianGaussianGaussian[Length]weight: Weight · .zweightWeight · .zCloudlargest_plane: Largest planelargest_planeLargest plane→ boardfit_plane: Fit planefit_planeFit plane→ topplane: PlaneplanePlanePlanetop.originexprtop.originPointwall.normalexprwall.normalDirectionslice: Slice · 20 mmsliceSlice · 20 mm→ cutlowess: LOWESS · span 0.3lowessLOWESS · span 0.3→ lineline.domainexprline.domain→ aargmin: argminargminargmin→ lowline(a) + (line(b) - line(a)) * (low - a) / (b - a)exprline(a) + (line(b) - …→ chordchord - line(low)exprchord - line(low)→ sagabs(atan2(line(b) - line(a), b - a))exprabs(atan2(line(b) - l…→ tiltsag < (b - a) / 360 and tilt < 0.5°exprsag < (b - a) / 360 a…→ oksag: LengthoutputsagLengthtilt: AngleoutputtiltAngleok: BooloutputokBool
shelf.sb · compiled graph · hover a node or a line, click a node for its details
The datasynthetic · seeded · generated for this page

A synthetic phone scan of an 80 cm wall shelf that sags 3 mm in the middle and is 0.2° off level: the 18 mm board, two steel brackets, the wall behind it and some stray points from the room, at a random heading. The user guessed the shelf's height to within 2.4 cm.

the scan (3,000 of 51,003 points)
drag to turn
what the policy measures (cut): 798 points, from the front, heights × 40
drag to turn
Sample outputreal runtime · synthetic data

loaded shelf

scan
51,003 points
shelf_height
-27.4 cm (true: -25.0 cm)
sag
2.7 mm
tilt
0.20°
ok
false

02 · plant.sb

Greenhouse plant height

curvestrainingcma_es

A LOWESS curve through the top of a vertical slice finds the crown, and a second one finds the pot rim. Their difference is the geometry you can write down.

What you can’t write down is how much of the plant the scanner misses: thin tips and a smoothed peak. That is one tensor, lift, trained against measured heights, with a learned noise level next to it.

plant.sbpolicy
policy(scan: Cloud) -> height: Gaussian[Length]optimiser cma_es(sigma0: 0.3)loss    { height: nll }metrics { err: abs_error(height, y.height) } lift = tensor(1 cm, lower: 0 cm, upper: 10 cm)   # tips the scanner missessd   = tensor(5 mm, lower: 1 mm) plant  = scan    |> remove_outliers(sigma: 3)    |> center(axis: "x")    |> center(axis: "y")sec    = slice(plant, plane(origin: (0 m, 0 m, 0 m), normal: forward),               thickness: 4 cm)crown  = lowess(keep(sec, top(.v, 0.25)), span: 0.3)pot    = lowess(keep(sec, bottom(.v, 0.05)))peak   = argmax(crown)height = crown(peak) - pot(peak) + lift + noise(sd)
scan: CloudinputscanCloudlift: Lengthtensor · trainedliftLengthsd: Lengthtensor · trainedsdLengthremove_outliers: Remove outliers · 3σremove_outliersRemove outliers · 3σCloudcenter: Center (x)centerCenter (x)Cloudcenter: Center (y)centerCenter (y)→ plantplane: PlaneplanePlanePlaneslice: Slice · 40 mmsliceSlice · 40 mm→ seckeep: KeepkeepKeepSectionlowess: LOWESS · span 0.3lowessLOWESS · span 0.3→ crownkeep: KeepkeepKeepSectionlowess: LOWESS · span 0.667lowessLOWESS · span 0.667→ potargmax: argmaxargmaxargmax→ peaknoise: NoisenoiseNoiseGaussian[Length]crown(peak) - pot(peak) + lift + noise(sd)exprcrown(peak) - pot(pea…→ heightheight: Gaussian[Length]outputheightGaussian[Length]
plant.sb · compiled graph · hover a node or a line, click a node for its details
The datasynthetic · seeded · generated for this page

24 synthetic plant scans (30–60 cm tall) with hand-measured heights. The scanner misses the thinnest 1.5 cm of tips, so the crown curve reads short.

plant-09, measured 32.8 cm
plant-23, measured 47.1 cm
plant-16, measured 59.2 cm
drag to turn (all together)
404050506060measured height (cm)held-out prediction ± sd
each entry predicted by the fold that didn't train on it · dashed: perfect
entryscanheight (measured)height (held-out prediction)
plant-0013,397 points48.8 cm49.1 cm ± 4.5 mm
plant-0113,440 points55.8 cm55.3 cm ± 4.3 mm
plant-0213,434 points53.2 cm52.9 cm ± 4.1 mm
plant-0313,418 points50.9 cm50.9 cm ± 4.5 mm
plant-0413,247 points36.5 cm36.4 cm ± 3.0 mm
plant-0513,426 points51.4 cm51.2 cm ± 4.5 mm
show all 24 entries ↓
entryscanheight (measured)height (held-out prediction)
plant-0613,188 points33.4 cm34.0 cm ± 4.1 mm
plant-0713,390 points47.1 cm47.1 cm ± 4.1 mm
plant-0813,449 points57.3 cm56.3 cm ± 3.0 mm
plant-0913,201 points32.8 cm33.0 cm ± 3.0 mm
plant-1013,246 points37.8 cm38.0 cm ± 4.5 mm
plant-1113,366 points43.7 cm43.7 cm ± 4.3 mm
plant-1213,310 points39.7 cm40.1 cm ± 4.5 mm
plant-1313,405 points44.6 cm44.5 cm ± 4.3 mm
plant-1413,372 points46.0 cm46.2 cm ± 4.1 mm
plant-1513,302 points39.3 cm39.6 cm ± 4.3 mm
plant-1613,443 points59.2 cm58.1 cm ± 3.0 mm
plant-1713,401 points54.8 cm54.4 cm ± 4.1 mm
plant-1813,342 points41.2 cm41.6 cm ± 3.0 mm
plant-1913,355 points42.2 cm42.6 cm ± 4.3 mm
plant-2013,403 points47.5 cm47.7 cm ± 4.5 mm
plant-2113,359 points43.0 cm43.5 cm ± 4.1 mm
plant-2213,372 points49.5 cm49.3 cm ± 4.3 mm
plant-2313,387 points47.1 cm47.0 cm ± 3.0 mm
download plant.csv
Sample outputreal runtime · synthetic data

Trained · 4-fold cross-validation, cma_es · 24 entries

lift
20.5 mm
sd
4.0 mm
err (held out)
3.3 mm ± 1.2 mm

a new plant, 47.0 cm tall

scan
a synthetic scan
height
47.0 cm ± 4.0 mm

03 · fuse.sb

Sensor fusion

uncertaintybayes_update

The wall unit is a prior; the probe is a likelihood. bayes_update is a single node that returns the posterior and the evidence, and the posterior’s sd is exact for Gaussians.

The evidence is the density of the probe reading under the prior, so its negative log is a ready-made disagreement score.

fuse.sbpolicy
# Temperatures in °C are plain Scalars: ° on its own means an angle.policy(wall: Scalar, probe: Scalar,       wall_sd: Scalar = 0.8, probe_sd: Scalar = 0.3)    -> (temp: Gaussian[Scalar], surprise: Scalar) prior          = gaussian(wall, sd: wall_sd)temp, evidence = bayes_update(prior, likelihood(probe, noise: probe_sd))surprise       = -log(evidence)        # large when the two disagree
wall: ScalarinputwallScalarprobe: ScalarinputprobeScalarwall_sd: Scalarinputwall_sdScalarprobe_sd: Scalarinputprobe_sdScalargaussian: GaussiangaussianGaussian→ priorlikelihood: LikelihoodlikelihoodLikelihoodLikelihood[Scalar]bayes_update: Bayes updatebayes_updateBayes update→ temp-log(evidence)expr-log(evidence)→ surprisetemp: Gaussian[Scalar]outputtempGaussian[Scalar]surprise: ScalaroutputsurpriseScalar
fuse.sb · compiled graph · hover a node or a line, click a node for its details
Sample outputreal runtime · synthetic data

Two readings in °C, with the default noise levels (0.8 and 0.3).

readings agree

wall
21.9
probe
21.2
temp
21.3 ± 0.281
surprise
1.1

the wall unit sits in the sun

wall
26.0
probe
21.2
temp
21.8 ± 0.281
surprise
16.5

04 · parcel.sb

Package dimensioning

geometryunitsuncertainty

Two vertical slices through the centred cloud, with u pointing up. In each, a LOWESS curve over height tracks a wall’s mean position, so the scanner’s noise averages out instead of widening the box. wall is a fn, so each of its four calls is its own group in the graph.

Every number has a unit, so the planes, thicknesses and noise all type-check. The outputs are Gaussians: sigma rides along through the arithmetic, and the ± is exact Gaussian algebra, not a rule of thumb.

parcel.sbpolicy
# The capture app crops the scan to the parcel; this measures it.policy(scan: Cloud, sigma: Length = 1.5 mm)    -> (length: Gaussian[Length], width: Gaussian[Length], height: Gaussian[Length]) box = scan |> remove_outliers(sigma: 2.5) |> centero   = (0 m, 0 m, 0 m)z   = (0 m, 0 m, 1 m)xs  = slice(box, plane_through_line(o, z, normal: right), thickness: 1 cm)    # u: up, v: xys  = slice(box, plane_through_line(o, z, normal: forward), thickness: 1 cm)  # u: up, v: y # a wall's mean position, as a curve over height, read 5 cm below the centrefn wall(sec: Section, lo: Length, hi: Length) -> Length {    lowess(keep(sec, .u < 0 m and lo < .v < hi))(-5 cm)} z0, z1 = lowess(xs).domainlength = wall(ys, 0 m, 1 m) - wall(ys, -1 m, 0 m) + noise(sigma)width  = wall(xs, 0 m, 1 m) - wall(xs, -1 m, 0 m) + noise(sigma)height = z1 - z0 + noise(sigma) + noise(sigma)   # two extreme points, two errors
wall → length.wallwall → length.wallwall → width.wallwall → width.wallscan: CloudinputscanCloudsigma: LengthinputsigmaLengthremove_outliers: Remove outliers · 2.5σremove_outliersRemove outliers · 2.5σCloudcenter: CentercenterCenter→ boxplane_through_line: Plane through lineplane_through_linePlane through linePlaneslice: Slice · 10 mmsliceSlice · 10 mm→ xsplane_through_line: Plane through lineplane_through_linePlane through linePlaneslice: Slice · 10 mmsliceSlice · 10 mm→ yslowess: LOWESS · span 0.667lowessLOWESS · span 0.667Curvelowess(xs).domainexprlowess(xs).domain→ z0keep: KeepkeepKeepSectionlowess: LOWESS · span 0.667lowessLOWESS · span 0.667Curvelowess(keep(sec, .u < 0 m and lo < .v < hi))(-5 cm)exprlowess(keep(sec, .u <…Lengthkeep: KeepkeepKeepSectionlowess: LOWESS · span 0.667lowessLOWESS · span 0.667Curvelowess(keep(sec, .u < 0 m and lo < .v < hi))(-5 cm)exprlowess(keep(sec, .u <…Lengthnoise: NoisenoiseNoiseGaussian[Length]wall(ys, 0 m, 1 m) - wall(ys, -1 m, 0 m) + noise(sigma)exprwall(ys, 0 m, 1 m) - …→ lengthkeep: KeepkeepKeepSectionlowess: LOWESS · span 0.667lowessLOWESS · span 0.667Curvelowess(keep(sec, .u < 0 m and lo < .v < hi))(-5 cm)exprlowess(keep(sec, .u <…Lengthkeep: KeepkeepKeepSectionlowess: LOWESS · span 0.667lowessLOWESS · span 0.667Curvelowess(keep(sec, .u < 0 m and lo < .v < hi))(-5 cm)exprlowess(keep(sec, .u <…Lengthnoise: NoisenoiseNoiseGaussian[Length]wall(xs, 0 m, 1 m) - wall(xs, -1 m, 0 m) + noise(sigma)exprwall(xs, 0 m, 1 m) - …→ widthnoise: NoisenoiseNoiseGaussian[Length]noise: NoisenoiseNoiseGaussian[Length]z1 - z0 + noise(sigma) + noise(sigma)exprz1 - z0 + noise(sigma…→ heightlength: Gaussian[Length]outputlengthGaussian[Length]width: Gaussian[Length]outputwidthGaussian[Length]height: Gaussian[Length]outputheightGaussian[Length]
parcel.sb · compiled graph · hover a node or a line, click a node for its details
The datasynthetic · seeded · generated for this page

One synthetic phone scan of a 42 × 31 × 22 cm parcel: four sides and the lid, 1.5 mm of sensor noise, in the phone's own frame.

the scan (2,400 of 18,000 points)
drag to turn
Sample outputreal runtime · synthetic data

parcel scan

scan
18,000 points
sigma
1.5 mm (the default)
length
42.0 cm ± 1.5 mm
width
30.9 cm ± 1.5 mm
height
22.0 cm ± 2.1 mm

05 · pallet.sb

Pallet stacking risk

trainingdelegateheld-out metrics

linear has a built-in fitter, so under optimiser delegate it solves its own coefficients in closed form. Features are made dimensionless explicitly (height / 1 m), so the coefficients come out in centimetres.

The metrics block is evaluated only on held-out folds. The output is a distribution, so the risk of sliding past 3 cm is a cdf away.

pallet.sbpolicy
policy(height: Length, lean: Angle, load: Scalar)    -> (shift: Gaussian[Length], risk: Scalar)optimiser delegateloss { shift: nll }metrics {    err:   abs_error(shift, y.shift)    cover: coverage(shift, y.shift)       # inside the 90% interval?} w     = tensor([0 cm, 0 cm, 0 cm])shift = linear((height / 1 m, lean / 1°, load / 100), coef: w,               intercept: tensor(0 cm), noise: tensor(1 cm, lower: 0 cm))risk  = 1 - cdf(shift, 3 cm)          # P(the top layer slides > 3 cm)
height: LengthinputheightLengthlean: AngleinputleanAngleload: ScalarinputloadScalarw: Vector[Length]tensor · trainedwVector[Length]shift.intercept: Lengthtensor · trainedshift.interceptLengthshift.noise: Lengthtensor · trainedshift.noiseLengthlinear: LinearlinearLinear→ shiftexprexprexpr(Scalar, Scalar, Scalar)cdf: CDFcdfCDFScalar1 - cdf(shift, 3 cm)expr1 - cdf(shift, 3 cm)→ riskshift: Gaussian[Length]outputshiftGaussian[Length]risk: ScalaroutputriskScalar
pallet.sb · compiled graph · hover a node or a line, click a node for its details
The datasynthetic · seeded · generated for this page

60 synthetic pallets with a measured slide after a vibration test, 2–4 mm of it unexplained noise.

2020303040405050measured shift (mm)held-out prediction ± sd
each entry predicted by the fold that didn't train on it · dashed: perfect
entryheightleanloadshift (measured)shift (held-out prediction)
pallet-0094.1 cm1.50°60131.3 mm32.7 mm ± 3.3 mm
pallet-0196.3 cm2.78°20335.1 mm37.1 mm ± 3.4 mm
pallet-02184.3 cm1.87°42743.3 mm44.2 mm ± 3.4 mm
pallet-03152.9 cm0.83°25333.0 mm31.5 mm ± 3.2 mm
pallet-04153.7 cm1.54°76345.9 mm42.6 mm ± 3.5 mm
pallet-05187.9 cm0.61°56536.0 mm39.8 mm ± 3.0 mm
show all 60 entries ↓
entryheightleanloadshift (measured)shift (held-out prediction)
pallet-06118.9 cm1.77°32629.0 mm35.0 mm ± 3.4 mm
pallet-07175.4 cm0.39°50033.9 mm35.0 mm ± 3.2 mm
pallet-0889.1 cm2.69°47236.7 mm37.8 mm ± 3.5 mm
pallet-09154.1 cm0.61°82635.3 mm38.7 mm ± 3.0 mm
pallet-1083.6 cm0.60°40918.7 mm24.4 mm ± 3.3 mm
pallet-11179.7 cm2.09°40444.7 mm45.1 mm ± 3.5 mm
pallet-1297.6 cm2.99°49544.4 mm41.0 mm ± 3.0 mm
pallet-1386.0 cm0.10°78421.4 mm26.7 mm ± 3.0 mm
pallet-14114.0 cm0.95°21728.0 mm28.2 mm ± 3.3 mm
pallet-1582.7 cm2.52°50036.5 mm36.5 mm ± 3.0 mm
pallet-16161.3 cm0.59°19632.2 mm32.0 mm ± 3.4 mm
pallet-17178.5 cm0.08°75441.6 mm37.9 mm ± 3.0 mm
pallet-1890.2 cm0.05°37025.7 mm19.0 mm ± 3.2 mm
pallet-19134.2 cm2.56°31342.0 mm40.8 mm ± 3.3 mm
pallet-20108.4 cm2.93°85642.4 mm46.5 mm ± 3.0 mm
pallet-21128.0 cm0.94°71034.5 mm35.3 mm ± 3.5 mm
pallet-2287.4 cm1.21°33434.9 mm25.7 mm ± 3.2 mm
pallet-23161.6 cm1.64°64649.2 mm42.1 mm ± 3.4 mm
pallet-24165.9 cm2.78°26244.5 mm45.6 mm ± 3.3 mm
pallet-2595.8 cm1.33°74035.8 mm33.3 mm ± 3.4 mm
pallet-26163.5 cm0.11°42028.6 mm31.9 mm ± 3.5 mm
pallet-27189.9 cm0.43°33339.7 mm35.2 mm ± 3.2 mm
pallet-2886.7 cm2.61°62737.6 mm38.7 mm ± 3.4 mm
pallet-29134.8 cm0.24°60827.4 mm31.0 mm ± 3.5 mm
pallet-3084.3 cm0.35°56623.6 mm22.5 mm ± 3.2 mm
pallet-31115.7 cm1.93°41435.8 mm36.0 mm ± 3.3 mm
pallet-32114.7 cm1.19°83428.8 mm37.1 mm ± 3.0 mm
pallet-3389.5 cm1.68°87340.3 mm35.9 mm ± 3.3 mm
pallet-34157.0 cm0.20°75539.4 mm35.9 mm ± 3.0 mm
pallet-3595.8 cm1.39°18724.7 mm26.3 mm ± 3.2 mm
pallet-36159.0 cm2.41°72049.4 mm47.3 mm ± 3.3 mm
pallet-37166.9 cm0.75°25333.2 mm34.1 mm ± 3.4 mm
pallet-38134.8 cm0.86°60431.5 mm34.9 mm ± 3.0 mm
pallet-39106.4 cm1.87°41842.8 mm34.4 mm ± 3.3 mm
pallet-40111.9 cm2.40°46141.0 mm38.6 mm ± 3.3 mm
pallet-41154.1 cm1.56°34331.0 mm38.2 mm ± 3.0 mm
pallet-4290.6 cm0.97°56229.6 mm29.1 mm ± 3.4 mm
pallet-4397.0 cm2.40°73842.3 mm40.0 mm ± 3.2 mm
pallet-44179.2 cm2.27°37444.1 mm45.5 mm ± 3.3 mm
pallet-45117.4 cm2.25°43939.7 mm38.6 mm ± 3.4 mm
pallet-46176.4 cm2.07°70951.8 mm46.9 mm ± 3.4 mm
pallet-47166.1 cm1.34°57438.4 mm41.0 mm ± 3.5 mm
pallet-48141.1 cm2.44°67942.4 mm45.8 mm ± 3.2 mm
pallet-49134.6 cm2.64°23139.5 mm40.7 mm ± 3.5 mm
pallet-50120.8 cm0.27°61431.3 mm27.9 mm ± 3.2 mm
pallet-51127.1 cm2.56°25342.4 mm39.5 mm ± 3.5 mm
pallet-52125.5 cm1.58°52536.8 mm36.9 mm ± 3.0 mm
pallet-53136.3 cm2.59°27839.5 mm41.1 mm ± 3.5 mm
pallet-5487.4 cm1.38°88134.0 mm33.7 mm ± 3.2 mm
pallet-55189.0 cm1.61°24043.8 mm41.7 mm ± 3.5 mm
pallet-56102.8 cm2.14°55637.0 mm37.2 mm ± 3.4 mm
pallet-57108.1 cm2.60°72539.3 mm42.5 mm ± 3.5 mm
pallet-58124.6 cm2.21°87841.6 mm43.4 mm ± 3.3 mm
pallet-5997.5 cm1.09°53134.6 mm28.8 mm ± 3.2 mm
download pallet.csv
Sample outputreal runtime · synthetic data

Trained · 5-fold cross-validation, delegate (closed form) · 60 entries

w
(12.7 mm, 5.8 mm, 1.1 mm)
intercept
5.6 mm
noise
3.3 mm
err (held out)
2.8 mm ± 0.7 mm
cover (held out)
0.83 ± 0.10

a 1.2 m stack, leaning 1°, 400 kg

height
1.2 m
lean
1°
load
400
shift
31.1 mm ± 3.3 mm
risk
0.63

In the field

Where it started.

Seabiscuit runs in production at Equimorphic, which fits saddles from phone scans of horses. A few pictures from that work, featuring Gerald.

Gabe scanning Gerald, Equimorphic’s inflatable test horse, with a phone.
Scanning GeraldGabe scanning Gerald, Equimorphic’s inflatable test horse, with a phone.
A close-up of Gerald’s face, as captured by the app’s LiDAR scan.
The point cloudA close-up of Gerald’s face, as captured by the app’s LiDAR scan.
The visualiser showing a cross-section plane through a scan of a person.
A slice through a scanThe visualiser showing a cross-section plane through a scan of a person.
A scan in the mobile app, with the policies its owner can run on it.
The capture appA scan in the mobile app, with the policies its owner can run on it.

Open source

The language will be open source.

We plan to open-source the Seabiscuit language. Join the list and you'll hear first when it's out.

JOIN THE LIST →

Early access

Want to build on it now?

Seabiscuit Cloud is open to a small group of teams while we get it ready for everyone. Tell us what you're building and we'll get back to you.

REQUEST EARLY ACCESS →