Skip to content
This repository has been archived by the owner on Sep 27, 2018. It is now read-only.

Control System Design Discussion #75

Open
arandell93 opened this issue Apr 27, 2017 · 18 comments
Open

Control System Design Discussion #75

arandell93 opened this issue Apr 27, 2017 · 18 comments

Comments

@arandell93
Copy link
Member

arandell93 commented Apr 27, 2017

As mentioned, for Rev 1.3 we need to come up with a control methodology. Here are some of the basics:

A control system is generally represented as a block diagram where each block is a self-contained series of transformations with inputs, outputs and parameters.
image

For us, the system is as follows:

  • The input in our case is waypoint we want to go to (lat, long).
  • The Error block is next in line, which computes Error = (waypoint) - (where we are). The result for us should be a vector comprised of distance to waypoint, bearing to waypoint.
  • The Controller is the block of software we use to turn the input (error) into an output (in our case, a motor speed pair). This can be achieved through one of many types of control methods including PID, Fuzzy, etc.
  • The Process block is the dynamic model of the system which represents how motor speed translates to position. Not shown in the figure is the so-called Disturbance which will change the way the process effects the output. For us, disturbance includes things like current, wind, and waves.
  • The Output of the system is where we are as read by the GPS. It is then used as the feedback signal to the Error block.

For Rev 1.3, it is appropriate for us to use PID control, as this is the simplest for us to implement due to its simple mathematical model and familiarity. Interestingly, PID theory was developed by observing helmsmen attempting to keep a vessel on course in varying wind and waves. PID control can be shown as follows:
image

  • P is Proportional control. The error gets multiplied directly by a constant gain, kp. This is used to determine how quickly a system reacts to changes.
  • I is Integral control. The sum of a constantly shifting set of previous error values (the last x errors) is multiplied by the integral gain, ki. This is used to dampen the oscillations due to P control and eliminate steady-state error.
  • D is Derivative control. The difference between the current output and the last measured output is multiplied by the derivative gain, kd. This is used to 'brake' the system to lessen overshoot and to slow the system reaction.

A few notes:

  • Our error calculation for bearing is a little non-intuitive. We first determine the bearing between our current position and the setpoint by taking the unit vector of the displacement vector (WPLat-CurrentLat, WPLong-CurrentLong) and then subtract that bearing from our bearing along our current path as stated by the GPS to get our bearing error (in degrees).
  • We may need to have two separately operating PID systems, one to control speed and the other to control bearing. This should still work if you combine (read: add) the motion values produced by each system before applying it to the motors.
  • In order for the GPS to accurately read bearing, we need to be moving fast enough such that the apparent 'motion' due to variance in position due to GPS accuracy is significantly less than the true change in position due to motion. From my experience, this speed is something >1 m/s.
  • If bearing proves to be a problem for us, we may need to add a magnetometer to get a reference to magnetic north and then apply the magnetic declination to correct it to true north.
  • A gyro would enable us to get a more accurate measure of our yaw rate/acceleration (compared with performing subtraction of GPS bearings with each step). This would likely improve our ability to tune the system for achieving the desired bearing and compensate for disturbances.
@arandell93
Copy link
Member Author

This paper is very relevant to our interests.

@arandell93 arandell93 added this to the Rev 1.3 milestone Apr 27, 2017
@whymarrh
Copy link
Member

Thanks for writing this up. I'll take a look at this in a bit.

@whymarrh
Copy link
Member

whymarrh commented Apr 30, 2017

I've taken a read through a few resources (1, 2, 3, 4, and 5) and I think I have a basic understanding of the idea here. What I'm still unsure about how our controller will work. A "basic" PID controller for a thermostat is easy enough:

double k_p = /* ??? */;
double k_i = /* ??? */;
double k_d = /* ??? */;

double startTime = System.currentTimeMillis();
double desiredOutput = 42;
double errorIntegral = 0; /* What does this start at? */
double lastError = 0; /* What does this start at? */

for (;;) {
    double now = System.currentTimeMillis();
    double dt = now - startTime;
    double error = desiredOutput - read();
    errorIntegral += (error * dt);
    double errorDerivative = (error - lastError) / dt;

    lastError = error;
    startTime = now;

    write((k_p * error) + (k_i * errorIntegral) + (k_d * errorDerivative));
    TimeUnit.MILLISECONDS.sleep(16)
}

Does that look correct? Assuming it is,

  1. How do we calculate our error? Euclidean distance? Haversine formula?
  2. Does the error integral grow unboundedly?
  3. How do we translate a desired output vector effect into output? (Is there a mapping function between surge and yaw and speed and course?)
  4. How does the polling interval of the GPS effect the computation?

@whymarrh
Copy link
Member

This reminds me of Fold (or a scan) as it's only ever looking at the previous values:

Scan operator

@arandell93
Copy link
Member Author

I will comment more thoroughly on your code later, as well as looking at Folds, I've never heard of that before.

How do we calculate our error? Euclidean distance? Haversine formula?

Euclidean is adequate for us given the short distances involved.

Does the error integral grow unboundedly?

No, its a running integral of the last 'x' errors, so oldest values get pushed out.

How do we translate a desired output vector effect into output? (Is there a mapping function between surge and yaw and speed and course?)

If I understand your question correctly, I sort of touched on this in my OP where I said:

We may need to have two separately operating PID systems, one to control speed and the other to control bearing. This should still work if you combine (read: add) the motion values produced by each system before applying it to the motors.

The mapping function between motion values for surge and yaw to speed and course will be the 'process' (our boat's dynamic model) which we don't know at the moment. You have to remember with control systems you're not controlling the output, you're controlling a device which affects an output indirectly. (You don't control temperature, you control a heater to affect temperature. In our case we control motors to affect our speed and course. By tuning the system we can get it to do what we want.

How does the polling interval of the GPS effect the computation?

Signifcantly. Our control system timestep must be greater than the slowest update involved in the system. This relates to the Nyquist Frequency which (TL;DR) states that for proper DSP your sampling frequency (timestep) must be at least twice that of your discretized input signal (our GPS signal). This is one of the reasons we need to increase our GPS update rate to 10Hz rather than 1Hz.

Our timestep is likely going to be limited by the boat itself, as it is definitely going to be sluggish to respond to changes. We may have to wait for it to accelerate and change bearing/speed somewhat before calculating again.

@whymarrh
Copy link
Member

whymarrh commented May 4, 2017

I had a go at a (generic) PID controller implementation and it doesn't seem to bad. It's not what this comment is about, but for the curious, this is pretty much what I've got (see #82 for all of it):

class PIDController<in T>(
    private val setpoint: T,
    private val dt: Long,
    private val gains: Gains,
    private val e: (T, T) -> Double
) {
    data class Gains(val kp: Double, val ki: Double, val kd: Double)

    companion object {
        const val BUFFER_SIZE = 4
    }

    private val errors = RingBuffer(BUFFER_SIZE)

    fun add(value: T) = errors.add(e(setpoint, value))

    fun nextOutput(): Double {
        val (kp, ki, kd) = gains
        val errors = errors.array
        val integral = errors.map({ it * dt }).sum()
        val de = errors[errors.lastIndex - 1] - errors[errors.lastIndex]
        val derivative = de / dt
        return (
              (kp * errors.last())
            + (ki * integral)
            + (kd * derivative)
        )
    }
}

I have a few questions about how we want to go about using this:

  • In what unit is ∆t expressed? (Milliseconds?)

  • What are we using for ∆t?

    We can either use a fixed sample sample rate (e.g. if we get a new GPS value every 500ms, ∆t is 500ms) and fix the value of ∆t or we can calculate the elapsed time between producing each output and use that as ∆t. The prior runs the risk of having the sample time skew slightly (as computers like to do) and not have that reflected in ∆t. The latter runs the risk of having ∆t skew slightly across values (and then our integral calculation may need to take that into account).

  • What exactly happens when we adjust the gains? That is, what happens when we adjust Kp, Ki, or Kd?

    Seeing as the ability to adjust the gains on the fly is necessary, what happens to the controller when that occurs. As I see it, we can either 1) produce out next outputs with the new gain values and the same integral and derivative, or 2) zero out our integral and derivative and start recollecting them, performing the next calculation with the new gains and a zero integral and a zero derivative.

  • Do we want to be able to configure the integral "size" on the fly?

  • Do we want to be able to configure the derivative "size" on the fly?

    We outlined above that the integral term does not grow unbounded, but we instead keep the last N error values and sum them. Do we need/want to be able to adjust N on the fly? If so, what happens when we do? Similarly for the derivative term, do we wan to be able to adjust from which two points (e.g. e(t - x) - e(t), x > 0) the slope is calculated? If so, what happens when we do?

@arandell93
Copy link
Member Author

arandell93 commented May 11, 2017

In what unit is ∆t expressed? (Milliseconds?)

Yes, ms.

What are we using for ∆t?

Start at 250 ms. We will need the ability to easily change this, however.

[...] we can calculate the elapsed time between producing each output and use that as ∆t.

This is the correct method by which PIDs are implemented. You must not update your PID at a rate any faster than half that of your slowest input signal (related to Nyquist Frequency. Since our GPS update will be 10Hz (100 ms), 200 ms will be our minimum ∆t.

What exactly happens when we adjust the gains? That is, what happens when we adjust Kp, Ki, or Kd?

When any of the gains are changed, the new gains are applied at the next calculation of the controller. No stored values are cleared (such as the array holding previous error values). Since the gains are added at the end, we will see immediately the affect on each of the three terms and thus on the output.

Do we want to be able to configure the integral "size" on the fly? If so, what happens when we do?

Yes. It is a parameter used in tuning. This is called the integral timebase, Ti and should be presented to the user in ms (therefore it is the array size times the controller timestep, ∆t. Assuming we are only storing n errors where n represents the error value Ti seconds ago, if Ti is increasing then we simply add trailing zeros for times father in the past than Ti_old and carry on as normal, with more of the integral term filling out with each timestep until we are full again. If Ti is decreasing, we simply cut off the Ti_new - Ti_old error values and use fewer error values in our next calculation.

Do we want to be able to configure the derivative "size" on the fly? If so, what happens when we do?

Yes. It is a parameter used in tuning. This is called the derivative timebase, Td and should be presented to the user in ms. In terms of actual meaning, it is the time since the mth error and the current error, where the derivative is calculated as (e_0 - e_m) - (0-m). Assuming we have an arbitrarily large error array (as the integral term usually uses more terms than the derivative), we simply have the next calculation use the error Td_new seconds ago instead of Td_old.

@arandell93
Copy link
Member Author

We will need to be able to configure the following parameters for each PID controller (surge and yaw) on the fly (via a settings page on the GUI).

  • Target waypoint/coordinates
  • Kp, proportional gain (unitless)
  • ∆t, controller timestep (milliseconds)
  • Ki, integral gain (unitless)
  • Ti, integral timebase (can be measured in integer multiples of ∆t or in ms but should be displayed in ms)
  • Kd, derivative gain (unitless)
  • Td, derivative timebase (can be measured in integer multiples of ∆t or in ms but should be displayed in ms)
  • Reset controller memory (zeroes out all stored errors, sum of errors and rate of change of errors)
  • Start/Stop controller

I envision the process for tuning the controller to be as follows:

  1. Set the waypoint location
  2. Put boat in the water in manual mode
  3. Confirm boat can calculate the distance to the waypoint correctly by driving to and from waypoint
  4. Drive boat in circles around waypoint and confirm the direction to waypoint is being calculated correctly
  5. Set the surge to a constant value and enable the yaw controller. Tune the yaw controller appropriately
  6. Enable the surge controller and tune appropriately.

@whymarrh
Copy link
Member

  1. Confirm boat can calculate the distance to the waypoint correctly by driving to and from waypoint
  2. Drive boat in circles around waypoint and confirm the direction to waypoint is being calculated correctly

Where and how are you expecting the waypoint distance to be displayed?

@arandell93
Copy link
Member Author

arandell93 commented May 28, 2017

In the 'mission information' box of the GUI would make the most sense. We could display distance and course to next waypoint.

Ref LakeMaps/hangashore#2

@whymarrh
Copy link
Member

Fair enough, that works

@arandell93
Copy link
Member Author

Stretch goal:
Draw a line on the map to the waypoint from the boat's current position with a display of distance and course on it like a vector.

@whymarrh
Copy link
Member

Stretch goal

No.

@whymarrh
Copy link
Member

Just kidding—I agree that a vector would be a good visualization to have. I'll see what I can do. (Let's move further ideas to LakeMaps/hangashore#50 where we can attach images and whatnot to further flesh out what it would look like.)

@whymarrh
Copy link
Member

With #82 and #97 merged, I think this discussion can turn to what's required to be able to tune the control system—that is, what else (if anything?) do we need in addition to the the need for a better GPS (#90) and/or an IMU (#93) to be productive with our tuning.

@arandell93
Copy link
Member Author

The config file should be able to modify the Ti and Td

@arandell93
Copy link
Member Author

arandell93 commented Dec 15, 2017

I envision the process for tuning the controller to be as follows:

  1. Set the waypoint location
  2. Put boat in the water in manual mode
  3. Confirm boat can calculate the distance to the waypoint correctly by driving to and from waypoint
  4. Drive boat in circles around waypoint and confirm the direction to waypoint is being calculated correctly
  5. Set the surge to a constant value and enable the yaw controller. Tune the yaw controller appropriately
  6. Enable the surge controller and tune appropriately.

I'd like to note that my above 6 steps for tuning are still a solid method to tune and I had forgotten about that plan. It should be the fastest method to tune once we know our instrumentation is accurate (via steps 1-4)

@whymarrh
Copy link
Member

Blocking our ability to run through those 6 steps are, at least: #115 and #130

@whymarrh whymarrh removed this from the Rev 1.3 milestone Feb 16, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Development

No branches or pull requests

2 participants