How to use the Linear Generic Controller


Intro to generic_linear

The generic_linear controller is designed to be suitable for any practical linear control law, from the simple leaky integrator, to POLC with arbitrary order IIR filters, to LQG. The entire pipeline in it’s fullest generality is outlined below (python syntax for readability - though the actual implementation is in cublas accelerated c++).

Most General Pipeline:

centroids are computed in the centroider object, and the output of the controller is the com vector, to be sent to the DMs.

# compute appropriate slope vector including delay:
if POLC:
  s_now = comp_polc(centroids)
else:
  s_now = centroids

# update circular buffer of slopes:
for i in range(nslope_buffer-1):
  s[i+1] = s[i]
s[0] = s_now

# clear temporary variable:
x_now *= 0.0

# do recursions of x with matA:
for i in range(nstate_buffer):
  x_now += matA[i] @ x[i]

# do innovations of s with matL:
for i in range(nslope_buffer):
  x_now += matL[i] @ s[i]

# update circular buffer of states:
for i in range(nstate_buffer-1):
  x[i+1] = x[i]
x[0] = x_now

# modal projection:
if MODAL:
  u_now = matK @ x_now
else:
  u_now = x_now

# update circular buffer of iir inputs:
for i in range(n_iir_in-1):
  u_in[i+1] = u_in[i]
u_in[0] = u_now

# perform iir recursions:
if n_iir_in > 0:
	u_now *= 0.0
for i in range(n_iir_in):
  u_now += iir_b[i] * u_in[i]
for i in range(n_iir_out):
  u_now += iir_a[i] * u_out[i]

# update circular buffer of iir outputs:
for i in range(n_iir_out-1):
  u_out[i+1] = u_out[i]
u_out[0] = u_now

# do final modal projection to command space
if MODAL:
  com = matF @ u_now
else:
  com = u_now

Without a specific application in mind, the above pseudo-code can be overwhelming. To understand the pipeline, it is useful to consider a few example uses.

Leaky Integrator

The leaky integrator is classically defined as:

com = (1-leak) * com - gain * ( R @ centroids )

where 0.0 < leak < 1.0 is the leak factor (e.g., leak=0.01), gain is the gain applied to the closed loop error signal, and R is the reconstructor/control matrix. To implement the leaky integrator in generic_linear, the following parameters should be chosen at config-time:

p_controller.set_nslope_buffer(1)		# (default value)
p_controller.set_nstates(0)					# (default value)
p_controller.set_nstate_buffer(0)		# (default value)
p_controller.set_niir_in(1)
p_controller.set_niir_out(1)
p_controller.set_modal(False)  			# (default value)
p_controller.set_polc(False)  			# (default value)

Consider now the equivalent generic_linear pipeline with these parameters set, and the superfluous operations removed:

# compute appropriate slope vector including delay:
s[0] = centroids

# do innovations of s with matL:
u_in[0] = matL[0] @ s[0]

# perform iir recursions:
u_now  = iir_b[0] * u_in[0]
u_now += iir_a[0] * u_out[0]

# update circular buffer of iir outputs:
u_out[0] = u_now

# do final modal projection to command space
com = u_now

To match the leaky integrator, the appropriate RTC data can be set:

NACTU,NSLOPE = R.shape
supervisor.rtc.set_L_matrix(0,0,R)
supervisor.rtc.set_iir_a_vector(0,0,leak*np.ones(NACTU))
supervisor.rtc.set_iir_b_vector(0,0,-gain*np.ones(NACTU))

N.B. The iir_a and iir_b vectors are required to be set as vectors even if the operations are scalar. Compared to the unavoidable GEMV, these operations are still negligible.

POLC With EWMA Filter

The classical choice when implementing POLC is to use the Exponentially Weighted Moving Average* (EWMA) IIR filter. That is:

com = (1-g) * com - g * (R @ s_ol)

Where s_ol is the pseudo-open loop slope vector, not necessarily computed using the same commands as com (due to >1 delay, or voltage clipping). Here, g is the so-called POLC gain. Restricting the control law to the above template results in the EWMA filter, with unity DC gain. This is achieved in the generic_linear controller in a similar way to the leaky integrator. The configuration is:

p_controller.set_nslope_buffer(1)		# (default value)
p_controller.set_nstates(0)					# (default value)
p_controller.set_nstate_buffer(0)		# (default value)
p_controller.set_niir_in(1)
p_controller.set_niir_out(1)
p_controller.set_polc(True)
p_controller.set_modal(False)  			# (default value)

The only difference here is the POLC flag being set to True, which implies that the RTC will require the matD (interaction matrix) to be set. I.e., the appropriate RTC data is:

NACTU,NSLOPE = R.shape
supervisor.rtc.set_L_matrix(0,0,R)
supervisor.rtc.set_D_matrix(0,iMat)
supervisor.rtc.set_iir_a_vector(0,0,(1-g)*np.ones(NACTU))
supervisor.rtc.set_iir_b_vector(0,0,-g*np.ones(NACTU))

By default, all RTC matrices in the generic_linear controller are set to zero, so even if the iMat has been computed already, it will not be set in the generic_linear controller unless done by the user as above.

The general IIR filter when used in a POLC scheme can have higher order filtering than the EWMA filter above. In general, this would look like:

u_in[0] = R @ s_ol			# open loop modal command vector

u *= 0.0
for i in range(N):
  u += b[i] * u_in[i]		# modal iir filter (in)
for i in range(M):
  u += a[i] * u_out[i]	# modal iir filter (out)

u_out[1:] = u_out[:-1]	# update circular buffers
u_in[1:]  = u_in[:-1]
u_out[0]  = u
com = F @ u							# project modal iir output to commands space

The choice of a higher order (M ,N) IIR filter is useful for temporal filtering, including vibration rejection or suppression of WFS noise. The above controller can be achieved using the following configuration:

p_controller.set_nslope_buffer(1)		# (default value)
p_controller.set_nstates(0)					# (default value)
p_controller.set_nstate_buffer(0)		# (default value)
p_controller.set_niir_in(N)
p_controller.set_niir_out(M)
p_controller.set_polc(True)
p_controller.set_modal(True)

Then, setting the RTC data:

supervisor.rtc.set_L_matrix(0,0,R)
supervisor.rtc.set_D_matrix(0,iMat)
supervisor.rtc.set_F_matrix(0,F)
for i in range(N):
  supervisor.rtc.set_iir_a_vector(0,i,a[i])
for i in range(M):
  supervisor.rtc.set_iir_b_vector(0,i,b[i])

Where a and b are lists of vectors containing the desired IIR coefficients. Note that (e.g.) each mode can (and often will) be filtered independently, i.e., with different IIR coefficients. For example, a notch filter could be used to filter vibrations in tip/tilt but not in other modes.

LQG

The effective closed-loop LQG control law for a first-order discrete LTI system looks like:

x = A @ x + L @ s_ol
com = K @ x

which is achieved in the generic_linear controller by using the following configuration:

p_controller.set_nslope_buffer(1)		# (default value)
p_controller.set_nstates(NSTATES)
p_controller.set_nstate_buffer(1)
p_controller.set_niir_in(0)					# (default value)
p_controller.set_niir_out(0)				# (default value)
p_controller.set_modal(False)  			# (default value)
p_controller.set_polc(True)

where NSTATES = A.shape[0]. The appropriate RTC data is then set as:

supervisor.rtc.set_A_matrix(0,0,A)
supervisor.rtc.set_L_matrix(0,0,L)
supervisor.rtc.set_D_matrix(0,iMat)
supervisor.rtc.set_K_matrix(0,K)