Yoga is purely functional, meaning that every function takes inputs and returns outputs, without any side effects.

Yoga programs have an implicit main loop, which is run every time a hardware sensor reports new values. It then propagates updates through the program like a spreadsheet.

Yoga’s lexical syntax is close enough to JavaScript that you can use JavaScript syntax highlighting in your editor.

Types

Primitive types include R (a double-precision real), bool, and string.

Matrix types are written like R[4,4].

Structs are a lot like C:

struct GreppyBalanceState {
  R tiltbackIntegral;
  R vel;
  R speedErrFilter;
  R steer;
  R accel;
}

A onehot structure is like a struct, but the sum of all the terms must either be 0 or 1:

onehot GreppyNavMode {
  initializing;
  standup;
  driveable;
};

Functions

Functions have 3 kinds of parameters: in, out, and update. This is syntactic sugar on top of a purely functional runtime. Consider the following function, which implements a low-pass filter:

Note that an update parameter refers to different values depending on which side of an assignment it’s on. For example, when s is an update parameter, s.x = s.x + 1 means something like sNext.x = sPrev.x + 1.

Being able to provide small parts of the update or output parameters at a time, rather than all at once, is a major advantage when the state is large and complex.

Update parameters allow convenient calling of stateful functions like filters. For example,

struct JointState { ... }

function jointCtl(
  out JointCmd cmd,
  update JointState s,
  in JointSensors sens, 
  in JointGoal goal,
  in JointConfig c)
{
  posErr = goal.pos - sens.pos;
  velErr = goal.vel - sens.vel;
  cmd.torque = lpfilter2(., s.torqueFilter,
      c.torqueFilterPeriod, c.torqueFilterQ,
      c.torquePosGain * posErr + c.torqueVelGain * velErr);
}

The call to lpfilter2 materializes the state variables necessary for a 2nd-order low-pass filter under s.torqueFilter, and updates them at every iteration.

Any literal number in yoga can be designated as a parameter by means of the ~ operator. These parameters become targets of backpropagation. For example, we can write a robot leg controller like:

  cmd.knee = jointCtl(., state.knee, sensors.knee, goals.knee, JointConfig {
      torqueFilterQ: 3.7~10,
      torqueFilterPeriod: 0.05~0.1,
      torquePosGain: 5.5~10,
      torqueVelGain: 2.5~10,
  });
  cmd.hip = jointCtl(., state.hip, sensors.hip, goals.hip, JointConfig {
      torqueFilterQ: 3.7~10,
      torqueFilterPeriod: 0.08~0.1,
      torquePosGain: 8.5~10,
      torqueVelGain: 3.5~10,
  });

This defines the feedback parameters for two joints. All the parameters are marked with the ~ operator, which means they can be modified by backpropagation, or by manually dragging the parameter slider in the studio UI.

They are modified both in the parameter table used by the simulator, and in the source code. That’s right, dragging a slider in the studio UI will modify the source code on disk.

If statement

In Yoga, both branches of an if statement are computed, and the results are combined according to the if argument. If it’s a bool, the end result is a lot like languages you’re used to. If it’s an R in [0,1], the values assigned on each branch are smoothly blended together. (This helps programs be fully differentiable.)

Operators

Every type T is equipped with several functions that allow them to be used like vectors in a linear algebra:

  • T operator *(R aCoeff, T a): scalar multiplication
  • T linearComb(R aCoeff, T a, R bCoeff, T B): computes aCoeff*a + bCoeff*b
  • R linearMetric(T a, T b): computes a dot product

Matrices

Matrix constructors take arguments in column-major order, so

m = R[4,4](1,2,3,0, 5,6,7,0, 9,10,11,0, 0,0,0,1);

Corresponds to:

  1   5   9   0
  2   6   10  0
  3   7   11  0
  0   0   0   1

Runtime

A yoga program should specify a runtime block, tell it how to run live on a robot. See an example.

The runtime defines engines corresponding either to a yoga function or a hardware interface.

Performance

Yoga is compiled using the LLVM backend, so it’s pretty fast. Typical numerical computation is often faster than C, because the language has guaranteed aliasing semantics. It’s easily capable of controlling a complex system like a biped robot with a 1 kHz update loop on an Intel NUC.

Operator precedence

  () [] -> .                      left to right
  ! -                             unary right to left
  ~ (parameter)                   left to right
  ^ (exponentiation)              left to right
  | (pipe)                        left to right
  * / %                           left to right
  + -                             left to right
  < <= > >=                       left to right
  == !=                           left to right
  &&                              left to right
  ||                              left to right
  ?:                              right to left
  = += -= etc.                    right to left

Function reference

  • sin(theta), cos(theta), tan(theta)
  • min(a, b, ...), max(a, b, ...)
  • clamp(value, lo, hi): returns value limited to between lo and hi
  • abs(x), sign(x), sqrt(x)
  • exp(x), log(x)
  • normsq(x): squared norm of any value including structs and matrices (treated elementwise)
  • hypot(x): square root of normsq
  • sum(x): sum of all elements of any value

  • mat44Translation(x,y,z): returns a R[4,4] translation matrix
  • mat44RotationX(theta): returns an R[4,4] rotation matrix around X (also Y, Z)
  • fromHomo: convert an R[4] to an R[3] by dividing by last coordinate
  • matToHom: convert an R[3,3] to an homogeneous R[4,4]

  • lpfilter1(out R output, update R state, in R period, in R input): single pole low-pass filter
  • lpfilter2(out R output, update Filter2State state, in R period, in R input): two-polw low-pass filter
  • easeIn(out R output, in R phase): raised cosine transition function (also easeOut)