seabiscuit

The language

A tour of Seabiscuit.

Seabiscuit is a small language for ML policies: typed programs that turn inputs (point clouds, measurements, readings) into outputs, compile to a graph of pure functions, and train only the numbers you mark as unknown. This page is the quick version; the language reference has every rule.

01

A policy is a typed function

One file, one policy. The signature names its inputs and outputs with their types; inputs may have defaults. Below it come header lines (optimiser, loss, metrics, import), then the body.

The body is bindings: name = expression. Rebinding a name shadows it, and each output is the last binding of its name. Indentation means nothing; a newline ends a statement unless the line obviously continues, inside brackets, after an operator, or on a line that starts with |>.

Every operation is a pure function from the prelude, so there's nothing to import to get started, and the compiler knows the type of every value.

gap.sbpolicy
policy(post: Point, gate: Point, hinge: Length = 3 mm)    -> (gap: Length, fits: Bool) gap  = horizontal_distance(post, gate) - 2 * hingefits = gap > 36 in                  # inches and millimetres mix freely

02

Pipes

x |> f(a, k: v) is exactly f(x, a, k: v), so a chain of operations on a cloud reads top to bottom. Arguments are positional first, then named; a literal argument is a frozen constant of that node.

Point selections take a predicate over per-point coordinates written with a leading dot: .x .y .z on a cloud, .u .v on a section. Comparisons chain, and and, or and not combine them.

rim.sbpolicy
policy(scan: Cloud) -> rim: Section rim = scan    |> largest_cluster(eps: 5 mm)    |> center    |> slice(plane(origin: (0 m, 0 m, 0 m), normal: forward), thickness: 1 cm)    |> keep(-20 cm <= .u <= 20 cm and .v > 2 cm)

03

Units are types

Every number has a dimension. Lengths are written m, cm, mm, ft or in; angles rad, deg or °. A bare number is a Scalar, and there's no default unit, not even for zero.

+, - and comparisons need matching dimensions; * and / combine them, so Length² and 1/Length fall out naturally. sin takes an Angle, atan2 returns one, and a postfix ° turns a Scalar expression into an Angle.

Get it wrong and the compiler says where, and what you probably meant (see Diagnostics). Everything is stored in metres and radians; units are for reading and writing.

tilt.sbpolicy
policy(left: Length, right: Length, span: Length = 1.2 m)    -> (tilt: Angle, shim: Length, quarter: Angle) tilt    = atan2(right - left, span)    # Length over Length gives an Angleshim    = span * tan(abs(tilt))        # tan takes an Angle, returns a Scalarquarter = (90 / 4)°                    # ° turns a Scalar into an Angle

04

Uncertainty is a value

Gaussian, Mixture, Grid, Samples and Categorical are ordinary values. A Gaussian is a mean plus coefficients on named, independent noise sources, so sums, differences and scaling are exact, and two values that share a source stay correlated.

Anything nonlinear on a Gaussian is a compile error that points you to sample(x, n) or .mean. Every distribution has .mean, .sd, interval, cdf and log_prob.

bayes_update(prior, likelihood) is one node with two outputs, the posterior and the evidence; take either by name. The sensor fusion example (fuse.sb) uses both.

mix.sbpolicy
policy() -> mid: Gaussian[Length] a   = gaussian(12 cm, sd: 2 mm)b   = gaussian(18 cm, sd: 3 mm)mid = a + 0.25 * (b - a) + noise(1 mm)# mid is a Gaussian[Length] with mean 13.5 cm and an exact sd:# √(0.75² · 2² + 0.25² · 3² + 1²) mm ≈ 1.95 mm
bayes.sbpolicy
policy(prior_mean: Length, reading: Length) -> (est: Gaussian[Length], lo: Length, hi: Length) prior  = gaussian(prior_mean, sd: 4 mm)est    = bayes_update(prior, likelihood(reading, noise: 2 mm)).posteriorlo, hi = interval(est, level: 0.9)

05

Tensors and optimisers

Every literal is frozen. Write tensor(...) where a number is unknown and it becomes trainable, with its unit, optional bounds and a typical step size. Bounds hold by reparameterisation, never by clipping. frozen: true makes a named, stored constant instead.

The optimisers are declared in the header: delegate lets nodes with a closed-form or convex fit (linear, stack, conditional_logit) solve their own tensors, and a built-in optimiser such as adam or cma_es trains the rest. loss says what good means against your labels; metrics are scored on held-out folds and never steer training.

Trained values are stored under stable keys named after the binding, so reordering lines never swaps them.

droop.sbpolicy
policy(load: Scalar) -> droop: Gaussian[Length]optimiser delegateoptimiser cma_es(sigma0: 0.3)loss    { droop: nll }metrics { err: abs_error(droop, y.droop) } gain  = tensor(2 cm, lower: 0 cm)             # trainable, bounded belowfloor = tensor(5 mm, frozen: true)            # a named, stored constantdroop = linear(load, coef: gain, intercept: floor,               noise: tensor(1 mm, lower: 0 mm))
optimiser cma_es(sigma0: 0.3) bias = tensor(0°, lower: -2°, upper: 2°)rise   = atan2(dz, u1 - u0) - 0.4° + noise(0.3°)rise   = atan2(dz, u1 - u0) - bias + noise(0.3°)

06

Functions and groups

A fn is typed like a policy and inlined at every call site. In the graph, each call becomes a group labelled width_at → front: the callee and the name its result is bound to. A tensor declared inside a fn gets one copy per call site; one declared outside is shared.

Constants pass through, recursion is an error, and the compiler caps how much inlining a policy can do, so a graph never explodes silently.

widths.sbpolicy
policy(scan: Cloud) -> (front: Length, back: Length) part = scan |> center fn width_at(y: Length) -> Length {    cut    = slice(part, plane(origin: (0 m, y, 0 m), normal: forward),                   thickness: 1 cm)    lo, hi = lowess(cut).domain    hi - lo} front = width_at(10 cm)back  = width_at(-10 cm)
width_at → frontwidth_at → backscan: CloudinputscanCloudcenter: CentercenterCenter→ partplane: PlaneplanePlanePlaneslice: Slice · 10 mmsliceSlice · 10 mm→ cutlowess: LOWESS · span 0.667lowessLOWESS · span 0.667Curvelowess(cut).domainexprlowess(cut).domain→ lohi - loexprhi - lo→ frontplane: PlaneplanePlanePlaneslice: Slice · 10 mmsliceSlice · 10 mm→ cutlowess: LOWESS · span 0.667lowessLOWESS · span 0.667Curvelowess(cut).domainexprlowess(cut).domain→ lohi - loexprhi - lo→ backfront: LengthoutputfrontLengthback: LengthoutputbackLength
widths.sb · compiled graph · hover a node or a line, click a node for its details

07

Imports and git

Policies live in a git repository, one file each. import policy calls another policy as one node, using its current deployment; @sha pins a commit, and (frozen: false) inlines it so its tensors train as part of yours. import tensor brings in a stored value, frozen unless you say otherwise.

While you work, unpinned imports float. Deploying a policy writes a lock that pins every import, transitively, so production runs are reproducible, and rolling back restores code and trained values together.

site/check.sbpolicy
policy(scan: Cloud, wall: Scalar, probe: Scalar)    -> (length: Gaussian[Length], temp: Gaussian[Scalar]) import policy geo/parcel                      # frozen: its current deployment, as one nodeimport policy sensors/fuse@4f2a91c as fuse_v1 # pinned to a commitimport tensor calib/probe_sd                  # a stored value, frozen by default length = parcel(scan).lengthtemp   = fuse_v1(wall, probe, probe_sd: probe_sd).temp

08

Geometry is built in

Seabiscuit grew up on 3D scans, so point clouds, sections, planes, curves and paths are types, not plugins. The world frame is gravity-aligned with z up, and RANSAC and DBSCAN cleans are built in.

Nearly every operation is differentiable, geometry included, and extrema can come back as Gaussians with their error bars attached. The node reference lists them all.

slice(cloud, plane, thickness)
A slab of the cloud on any plane, as a 2-D section.
midline(cloud)
The top ridge line of an elongated shape, as an arc-length path.
argmin(curve, noise: sigma_h)
An extremum, returned as a Gaussian with σ = √(2σh/κ). The flatter the curve, the wider the error bar.
largest_plane largest_cluster
RANSAC and DBSCAN cleans, built in.
lowess spline trapezoid
Smoothing, splines and trapezoid fits for sections.
keep(sec, top(.v, 0.5, at_least: 2 cm))
Predicates over point coordinates, units included.

09

Diagnostics that help

Every diagnostic has a code, a span, a message and, where there is one, a concrete fix. They render rustc-style in the CLI and as JSON for the editor and the API.

Compilation tolerates errors: each top-level line compiles on its own, so one bad line never hides the rest of the graph. The error on the right is exactly what the compiler prints for box.sb, and the build checks it still is.

Next: see it all together in the examples, or start at the docs.

box.sbpolicy
policy(scan: Cloud) -> lean: Angle box    = scan |> largest_cluster(eps: 1 cm) |> centerside   = slice(box, plane(origin: (0 m, 0 m, 0 m), normal: forward), thickness: 1 cm)lid    = lowess(keep(side, top(.v, 0.1)))x0, x1 = lid.domaintilt   = atan2(lid(x1) - lid(x0), x1 - x0)lean   = tilt - 2           # undo the table's tilt
error[E0203]: can't subtract Scalar from Angle --> box.sb:8:10  |8 | lean   = tilt - 2           # undo the table's tilt  |          ^^^^^^^^ tilt is Angle, 2 is Scalar  = help: did you mean 2°?

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 →