Implementing a Shaper

Implementing a Shaper

Introduction

This guide will walk you through the process of implementing a new input shaper. We will create an input shaper that adds a sine wave to the position with a user-defined frequency and magnitude.

If you have not yet set up a local copy of the simulator, then you should first follow the development setup guide.

The following list gives a brief overview of the capabilities of input shapers in Prunt and the constraints placed on them:

  • Input shapers are implemented as arbitrary functions which receive a series of positions at fixed time intervals for a single axis.
  • Input shapers must output a position each time a new position is received.
  • Input shapers may define arbitrary persistent internal variables as required for processing.
  • Prunt can shift inputs forwards or backwards an arbitrary amount in time to allow for the synchronisation of different shapers (practically limited by the stack size, which may be trivially changed in prunt-step_generator-generator.ads).
  • Prunt can send a series of extra positions at the end of a motion block to allow for input shapers to flush their internal buffers.

Adding the shaper parameters

The shaper parameters should be added to prunt-input_shapers.ads. We will also add these options to the GUI later.

Start by adding the Sine_Wave_Demo to the Shaper_Kind enumeration type:

type Shaper_Kind is (No_Shaper, Zero_Vibration, Extra_Insensitive, Sine_Wave_Demo);

Continue by adding our required parameters to the variant part of Shaper_Parameters:

type Shaper_Parameters (Kind : Shaper_Kind := No_Shaper) is record
   case Kind is
      when No_Shaper =>
         null;
      when Zero_Vibration =>
         ...
      when Sine_Wave_Demo =>
         Sine_Wave_Magnitude : Length;
         Sine_Wave_Frequency : Frequency;
   end case;
end record;

The types we are using above are floating-point numbers defined in prunt.ads. These types automatically have dimensional analysis applied to them and will cause compile-time errors to be emitted in the case of a dimension mismatch.

Implementing the shaper

Package specification

We will start by implementing the package specification for the shaper in prunt-input_shapers-sine_wave_demo_shapers.ads. Refer to the commentary below the following code for details on what each part does.

with Prunt.Input_Shapers.Shapers;

package Prunt.Input_Shapers.Sine_Wave_Demo_Shapers is

   type Sine_Wave_Demo_Shaper
     (Input_Offset : Cycle_Count; Extra_End_Time : Cycle_Count; Buffer_Size : Cycle_Count)
   is
     new Shapers.Shaper with private;

   function Create
     (Parameters         : Shaper_Parameters;
      Interpolation_Time : Time;
      Start_Position     : Length)
      return Sine_Wave_Demo_Shaper with
     Pre => Parameters.Kind = Sine_Wave_Demo;

   overriding function Do_Step (This : in out Sine_Wave_Demo_Shaper; Step : Length) return Length;

private

   type Buffer_Array is array (Cycle_Count range <>) of Length;

   type Sine_Wave_Demo_Shaper
     (Input_Offset : Cycle_Count; Extra_End_Time : Cycle_Count; Buffer_Size : Cycle_Count)
   is
   new Shapers.Shaper (Input_Offset => Input_Offset, Extra_End_Time => Extra_End_Time) with record
      Buffer               : Buffer_Array (0 .. Buffer_Size);
      Current_Buffer_Index : Cycle_Count;
      Next_Sine_Input      : Dimensionless;
      Sine_Wave_Magnitude  : Length;
      Sine_Wave_Frequency  : Frequency;
   end record;

end Prunt.Input_Shapers.Sine_Wave_Demo_Shapers;

Partial declaration of Sine_Wave_Demo_Shape

Input_Offset is passed to a discriminant of the parent type and defines how many cycles the input to Do_Step will be delayed by. The applied offset should result in the predicted movement of the axis matching as closely as possible to the movement of an ideal axis without shaping to allow axes with different shaper parameters to stay synchronised with each other. This value can be (and usually should be) a negative value to receive inputs that are a given number of cycles into the future. Note that Input_Offset is relative to offsets requested by other shapers, so requesting an offset of 1000 will not guarantee 1000 repeats of the initial position if all other shapers also request an offset of 1000.

Extra_End_Time is also passed to a discriminant of the parent type. After all moves are complete, Do_Step will be called Extra_End_Time times with a repeat of the finishing position to allow for flushing of any remaining outputs that have been buffered internally by the shaper.

Buffer_Size is a discriminant that we will use to create a buffer to demonstrate how Prunt can handle a delay introduced by our shaper by shifting moves forward in time.

Function declarations

The Create procedure used to create a new instance is created for each motion block (i.e. a large array of moves sharing the same kinematic parameters). This function is not overriding as dispatching is performed manually based on the shaper parameters. Interpolation_Time is the fixed time step between inputs passed to Do_Step and Start_Position is the position that the axis is idle at before the first call to Do_Step. The precondition here is not required but will help to prevent issues later.

We do not technically need to declare the Do_Step function here, but it makes things easier to keep track of. The overriding qualifier causes the compiler to emit an error if this function does not match the abstract function declared on the parent type.

Private declarations

The Buffer_Array type is an array of arbitrary size which may have its lower and upper index bounds set at runtime to any value of Cycle_Count.

Full declaration of Sine_Wave_Demo_Shaper

is new Shapers.Shaper (Input_Offset => Input_Offset, Extra_End_Time => Extra_End_Time) passes some of the discriminants to the parent type and with record allows us to append our own components to the type. Discriminants are similar to record components except for the fact that they cannot be changed after the record is initialised and may be used within the declaration of other record components, including components without a size known at compile time.

We use one of these discriminants in Buffer : Buffer_Array (0 .. Buffer_Size);, which declares an array which contains Buffer_Size + 1 values. This buffer will be a ring buffer which adds a delay between inputs and outputs. The mismatch in size here is just to make some later code a bit simpler. We can not subtract from a value that comes from a discriminant (or perform any other operation) to get the correct size while setting the lower bound to 0.

Current_Buffer_Index keeps track of our current position in the ring buffer and the other components are used to handle the sine wave output.

Package body

Now that we have a package specification, we can implement the package definition in prunt-input_shapers-sine_wave_demo_shapers.adb. Refer to the commentary below the following code for details on what each part does.

with Ada.Numerics.Generic_Elementary_Functions;
with Ada.Numerics; use Ada.Numerics;

package body Prunt.Input_Shapers.Sine_Wave_Demo_Shapers is

   package Dimensionless_Math is new Ada.Numerics.Generic_Elementary_Functions (Dimensionless);
   use Dimensionless_Math;

   function Create
     (Parameters         : Shaper_Parameters;
      Interpolation_Time : Time;
      Start_Position     : Length)
      return Sine_Wave_Demo_Shaper
   is
   begin
      if Parameters.Kind /= Sine_Wave_Demo then
         raise Constraint_Error with "Wrong create function called for given parameters.";
      end if;

      return
        (Input_Offset         => -100,
         Extra_End_Time       => 100,
         Buffer_Size          => 100,
         Buffer               => (others => Start_Position),
         Current_Buffer_Index => 0,
         Next_Sine_Input      => 0.0,
         Sine_Magnitude       => Parameters.Sine_Wave_Magnitude,
         Sine_Step            => Parameters.Sine_Wave_Frequency * Interpolation_Time * 2.0 * Pi);
   end Create;

   overriding function Do_Step (This : in out Sine_Wave_Demo_Shaper; Step : Length) return Length
   is
      Result : constant Length :=
        This.Buffer (This.Current_Buffer_Index) + Sin (This.Next_Sine_Input) * This.Sine_Magnitude;
   begin
      This.Current_Buffer_Index := (This.Current_Buffer_Index + 1) mod This.Buffer_Size;
      This.Buffer (This.Current_Buffer_Index) := Step;

      This.Next_Sine_Input :=
        Dimensionless'Remainder (This.Next_Sine_Input + This.Sine_Step, 2.0 * Pi);

      return Result;
   end Do_Step;

end Prunt.Input_Shapers.Sine_Wave_Demo_Shapers;

Imports and package instantiation

We import Ada.Numerics.Generic_Elementary_Functions to get access to a sine function and Ada.Numerics to get access to a pi constant.

Due to Ada’s strong typing we can not just have a sine function that works on all floating-point types. Instead we need to instantiate the generic package Generic_Elementary_Functions with the Dimensionless type as a parameter to get a sine function that takes a Dimensionless type as an input and returns the same as an output.

Create function

We start with an explicit check that the parameters are the right kind for this procedure. This is not required but will help to prevent issues later.

Next we create and return the shaper instance using a record aggregate. We request for Prunt to offset the inputs so that they arrive 100 cycles sooner than they otherwise would and add a 100 cycle delay buffer filled with the starting position for the sake of demonstration. As we are introducing a 100 cycle delay we need to also set Extra_End_Time to 100. We would have to set this higher if this were a shaper that applied smoothing or something similar.

We can see a benefit of using types with dimensional analysis here. If we made the mistake of thinking that Interpolation_Time was a frequency rather than a time and wrote Sine_Wave_Frequency / Interpolation_Time then we would get a compiler error pointing to the mistake:

prunt-input_shapers-sine_wave_demo_shapers.adb:21:09: error: dimensions mismatch in record aggregate
prunt-input_shapers-sine_wave_demo_shapers.adb:28:92: error: expected dimension [], found [Time**(-2)]

We can use Pi without a generic package here as Pi is a numeric literal defined without a type. This only works for dimensionless types, if we wanted to assign to a value of type Area we would need to write something similar to Pi * mm * mm to get a value of pi mm² or (Pi * mm)**2 to get the area of a square with pi mm sides. Dimensioned types work correctly with the exponent operator (**) as long as the exponent is an integer or a fraction with a numerator and denominator which are integers (e.g. 2 * mm**(1/3)).

Do_Step function

The Do_Step function is very simple. We get the next value from the buffer, add the dimensionless sine output multiplied by a length to it, and set that as the result. We then add the next value into the buffer and update the sine input before returning the result.

Integrating the shaper

Adding a call to Create

We need to add a small amount of code to allow Prunt to call our new Create function. This is straightforward and is done in the Create function in prunt-input_shapers-shapers.adb as shown below:

function Create
  (Parameters         : Axial_Shaper_Parameters;
   Interpolation_Time : Time;
   Initial_Position   : Position)
   return Axial_Shapers
is
   ...
begin
   for A in Axis_Name loop
      case Parameters (A).Kind is
         when No_Shaper | Zero_Vibration | Extra_Insensitive =>
            Result.Shapers.Insert
              (A, Basic_Shapers.Create (Parameters (A), Interpolation_Time, Initial_Position (A)));
         when Sine_Wave_Demo =>
            Result.Shapers.Insert
              (A,
               Sine_Wave_Demo_Shapers.Create
                 (Parameters (A), Interpolation_Time, Initial_Position (A)));
      end case;
   end loop;

We also need to add with Prunt.Input_Shapers.Sine_Wave_Demo_Shapers; to the top of the file.

Adding parameters to the GUI

This step is more involved than the previous step. We start by finding Shaper_Sequence in prunt-config.adb and then adding a new shaper type alongside the existing ones. After doing this, Shaper_Sequence should look like this:

Shaper_Sequence : constant Property_Parameters_Access :=
  Variant
    ("Input shaping method used for this axis.", --  Description
     "No shaper", --  Default value
     ["No shaper" =>
        Sequence
          ("No input shaping will be applied to this axis.",
           []),
      "Zero vibration (ZV/ZVD/ZVDD/etc.)" =>
        Sequence (...),
      "Extra insensitive (EI/2HEI/3HEI)" =>
        Sequence (...),
      "Sine wave demo" =>
        Sequence
          ("Adds a sine wave to the position.",
           ["Frequency" =>
              Float
                ("Frequency of the sine wave.",
                 Default => 200.0,
                 Min     => 0.0,
                 Max     => 1.0E100,
                 Unit    => "Hz"),
            "Magnitude" =>
              Float
                ("Magnitude of the sine wave,",
                 Default => 0.1,
                 Min     => 0.0,
                 Max     => 100.0,
                 Unit    => "mm")])]);

Next we move on to the JSON_To_Config function in the Config_File protected type in the same file. Here we need to find the code that parses the shaper parameters (search for Get (Data, "Input shaping$" & A'Image)) and add a new elsif for our new shaper, resulting in the following:

if Get (Data, "Input shaping$" & A'Image) = "No shaper" then
   Config.Shapers (A) := (Kind => No_Shaper);
elsif Get (Data, "Input shaping$" & A'Image) = "Zero vibration (ZV/ZVD/ZVDD/etc.)" then
   ...
elsif Get (Data, "Input shaping$" & A'Image) = "Extra insensitive (EI/2HEI/3HEI)" then
   ...
elsif Get (Data, "Input shaping$" & A'Image) = "Sine wave demo" then
   Config.Shapers (A) :=
     (Kind                => Sine_Wave_Demo,
      Sine_Wave_Frequency =>
        Get (Data, "Input shaping$" & A'Image & "$Sine wave demo$Frequency") * hertz,
      Sine_Wave_Magnitude =>
        Get (Data, "Input shaping$" & A'Image & "$Sine wave demo$Magnitude") * mm);
else
   raise Constraint_Error;
end if;

Testing

That covers everything required to implement a new shaper. You can now run the simulator as described here and test your new shaper after applying it to an axis on the configuration page.