Source code for stdcomQtC20

import argparse
import random

import numpy as np
import time
from six.moves import range


"""
    This is where Stec misc control objects are kept
"""

[docs]class ExpFilter : """ Exponetial filter """ FI = 1.0 FV = None reset = True def __init__(self, FI : float = .20): """ .01 to 1 where 1 in no filter default is .2 """ self.FV = 0.0 self.reset = True self.FI = FI
[docs] def Reset(self): """ clean out history """ self.reset = True
[docs] def XtoC(self, X : float): """ in with value return filtered value """ if self.reset is True : self.FV = X self.reset = False else: self.FV = (self.FV * (1 - self.FI)) + ( X * self.FI) return self.FV
[docs] def SetFI(self, FI : float = 1): """ sets to new FI """ self.FI = FI
[docs]class RollingAverageFilter : """ rolling average filter """ nbrSamples=5 Samples=[] def __init__(self, nbrSamples : int = 5): """ number of samples to use for filter default is 5 """ self.nbrSamples = nbrSamples
[docs] def Reset(self): """ clears history """ self.Samples.clear()
def XtoC(self, X: float): self.Samples.insert(0,X) if len(self.Samples > self.nbrSamples) : self.Samples.pop(len(self.Samples) -1 ) FV = sum(self.Samples) / len(self.Samples) return FV
[docs]class Dahlin : """ simple Dahlin controller, if you don't like the PID controller """ G = 1.0 TC = 30 LAMBDA = 1 Em1 = 0.0 now = time.time() then = now reset = True T = 0.0 def __init__(self, TC: float = 15.0, LAMBDA : float = .1, GAIN : float = 1): self.G = GAIN self.TC = TC self.LAMBDA = LAMBDA self.Em1 = 0.0 self.now = time.time() self.then = self.now self.reset = True
[docs] def Reset(self): """ clears history """ self.Em1 = 0.0 self.reset = True
[docs] def XtoC(self, X : float ): """ in with error from 0, where 0 is the target, return controller output """ self.now = time.time() if self.reset is True : self.then = self.now self.reset = False seconds = self.now - self.then print("Seconds", seconds) if seconds == 0: T = float(.0001) else: T = float(seconds) self.then = self.now L = 1 - np.exp(-T / self.TC) Q = 1 - np.exp(-self.LAMBDA * T) K = Q / (L * self.G) En = X X = K * ( En - (1-L) * self.Em1) self.Em1 = En return X
def Time(self): return self.now
[docs]def twiddle(evaluator, tol=0.001, params=3, error_cmp=None, initial_guess=None): """ A coordinate descent parameter tuning algorithm. https://en.wikipedia.org/wiki/Coordinate_descent Params: evaluator := callable that will be passed a series of number parameters, which will return an error measure tol := tolerance threshold, the smaller the value, the greater the tuning params := the number of parameters to tune error_cmp := a callable that takes two error measures (the current and last best) and returns true if the first is less than the second initial_guess := parameters to begin tuning with """ def _error_cmp(a, b): # Returns true if a is closer to zero than b. return abs(a) < abs(b) if error_cmp is None: error_cmp = _error_cmp if initial_guess is None: p = [0] * params else: p = list(initial_guess) dp = [1] * params best_err = evaluator(*p) steps = 0 while sum(dp) > tol: steps += 1 print('steps:', steps, 'tol:', tol, 'best error:', best_err) for i, _ in enumerate(p): # first try to increase param p[i] += dp[i] err = evaluator(*p) if error_cmp(err, best_err): # Increasing param reduced error, so record and continue to increase dp range. best_err = err dp[i] *= 1.1 else: # Otherwise, increased error, so undo and try decreasing dp p[i] -= 2. * dp[i] err = evaluator(*p) if error_cmp(err, best_err): # Decreasing param reduced error, so record and continue to increase dp range. best_err = err dp[i] *= 1.1 else: # Otherwise, reset param and reduce dp range. p[i] += dp[i] dp[i] *= 0.9 return p
[docs]class PID(object): """ Simple PID control. """ def __init__(self, p=0, i=0, d=0, **kwargs): self._get_time = kwargs.pop('get_time', None) or time.time # initialze gains self.Kp = p self.Ki = i self.Kd = d # The value the controller is trying to get the system to achieve. self._target = 0 # initialize delta t variables self._prev_tm = self._get_time() self._prev_feedback = 0 self._error = None def reset(self): # initialize delta t variables self._prev_tm = self._get_time() self._prev_feedback = 0 self._error = None @property def error(self): return self._error @property def target(self): return self._target @target.setter def target(self, v): self._target = float(v) def __call__(self, feedback, curr_tm=None): """ Performs a PID computation and returns a control value. This is based on the elapsed time (dt) and the current value of the process variable (i.e. the thing we're measuring and trying to change). """ # Calculate error. error = self._error = self._target - feedback # Calculate time differential. if curr_tm is None: curr_tm = self._get_time() dt = curr_tm - self._prev_tm # Initialize output variable. alpha = 0 # Add proportional component. alpha -= self.Kp * error # Add integral component. alpha -= self.Ki * (error * dt) # Add differential component (avoiding divide-by-zero). if dt > 0: alpha -= self.Kd * ((feedback - self._prev_feedback) / float(dt)) # Maintain memory for next loop. self._prev_tm = curr_tm self._prev_feedback = feedback return alpha
[docs]class ComputerSL: """ computers the slope and intercept for interpolation of history array """ def __init__(self, X1,X2, Y1,Y2): self.s = (Y2 - Y1) / (X2 - X1) self.i = Y1 - (self.s * X1) def Y(self, X): c = (X * self.s) + self.i return c def X(self, Y): c = None if self.s != 0.0 : c = (Y -self.i) / self.s return c
[docs]class HistoryDelay : """ This is a history array, it will work with either number storage IE steps, or if tau is not None, then it will work off of Tau It is meant as to not store more than necessary if tau is given.. if steps are used, then storage will be in steps and the use must ensure then number of steps is suffecient to store what is needed. No Block rate is needed, this works of actual seconds. """ history = [] historyLimit = 40 tau = None def __init__(self, steps : int = 40, tau : float = None): self.historyLimit = 40 self.history = [] self.tau = tau def Insert(self, v): tm = time.time() pair = (tm,v) self.history.insert(0,pair) if self.tau is None : if len(self.history) > self.historyLimit : self.history.pop(len(self.history) -1 ) else: for i1 in range(0, len(self.history)) : tm1, v1 = self.history[i1] df1 = tm - tm1 if df1 > self.tau : i2 = i1 + 1 if i2 < len(self.history) : del self.history[len(self.history) - i2 : ] return def ComputeSumToPosition(self, seconds, multi : float = 1): sum = 0.0 tm = time.time() for i in range(0, len(self.history)) : t, v = self.history[i] df = tm - t if df <= seconds : sum = sum + (v * multi) else: if i >= 1 : t1, v1 = self.history[i - 1] df2 = df df1 = tm - t1 nv = ComputerSL(df1,df2,v,v1) computedV = nv.Y(seconds) sum = sum + ( computedV * multi) return sum return sum def ComputeListToPosition(self, seconds, Q : float = 1): tm = time.time() sum = 0.0 lastT = 0.0 N = 0 for i in range(0, len(self.history)): t, v = self.history[i] df = tm - t N = i if df <= seconds: lastT = v * Q sum = sum + lastT else: if i >= 1: t1, v1 = self.history[i - 1] df2 = df df1 = tm - t1 nv = ComputerSL(df1, df2, v, v1) computedV = nv.Y(seconds) lastT = ( computedV * Q) return sum, lastT, i return sum, lastT, N
import operator
[docs]class F25(HistoryDelay) : """ F25 Lambda tuned Algorith """ G = None TC = None LAMBDA = None TAU = None TC = None e_m_1 = 0.0 y_m_1 = 0.0 lastTime = None def __init__(self, G : float, LAMBDA : float, TC : float, TAU : float, steps: int = 40): """ G gain ... LAMBDA TC Time Constant TAU steps is really not used, it is for history storage, but this algorithm uses tau and tc to efficently project storage """ super().__init__(steps, (TC + TAU)) self.G = G self.TC = TC self.LAMBDA = LAMBDA self.TAU = TAU self.e_m_1 = 0.0 self.y_m_1 = 0.0 self.lastTime = None
[docs] def Control(self, value : float ): """ 0 is always the target so the value is delta to 0, it is the error returns control move """ if self.lastTime is None : self.lastTime = time.time() nowTime = time.time() dfTime = nowTime - self.lastTime self.lastTime = nowTime if dfTime == 0: T = float(.0001) else: T = float(dfTime) M = self.TAU / T Q = 1 - np.exp(-self.LAMBDA * T) L = 1 - np.exp(-T / self.TC) K = Q / (L * self.G) APrime = -(1 - L) SMY, YNEM1, N = self.ComputeListToPosition( self.TAU, -Q) Y = YNEM1 * (-M*Q) + SMY + K * ( value + APrime * self.e_m_1) self.e_m_1 = value self.Insert(Y) return Y
[docs] def Reset(self): """ reset, normally just make a new controller, but if you want you can reset """ self.lastTime = None self.e_m_1 = 0.0 self.y_m_1 = 0.0\
try: from stdcomQtargs import stdcomQtargs as arguments except : from stdcomQt.stdcomQtargs import stdcomQtargs as arguments
[docs]class MapToZones() : """ args = edge_allow_0=True allows 0's at the edges middle_allow_0=False allows 0's in the middle start_s=1 starting slice number or databox number end_s=20 ending slice number of databox number left_s=1 if rev with right_s then porfile is to be reversed right_s=20 offset=0.0 offset in units of total_s_width actuator to headbox total_s_width=100 total slice width or headbox width min_d_width=6 units of total_s_width, the min actuator wisth d_count=10 number of actuators maps profile from scanner to actuator zones """ edge_allow_0 = True middle_allow_0 = False start_s = int(1) end_s = int(20) left_s = int(1) right_s = int(20) offset = float(0) total_s_width = float(100) min_d_width = float(6) d_count = int(10) THRESHOLD = float(.0000001) MAXVAL = float(1.0E38) def __init__(self, args): a = arguments(args) self.edge_allow_0 = bool (a.getDefault("edge_allow_0", self.edge_allow_0)) self.middle_allow_0 = bool(a.getDefault("middle_allow_0", self.middle_allow_0)) self.start_s = float(a.getDefault("start_s", self.start_s )) self.end_s = float(a.getDefault("end_s", self.end_s )) self.left_s = float(a.getDefault("left_s", self.left_s )) self.right_s = float(a.getDefault("right_s",self.right_s)) self.offset = float(a.getDefault("offset", self.offset)) self.total_s_width = float(a.getDefault("total_s_width", self.total_s_width)) self.min_d_width = float(a.getDefault("min_d_width", self.min_d_width )) self.d_count = int (a.getDefault("d_count", self.d_count)) def CheckBound(self, left, right, start, end, invert): if invert == 1: left += self.THRESHOLD right -= self.THRESHOLD else: left -= self.THRESHOLD right += self.THRESHOLD if left <= start: # out on start side if right >= (end + 1): # & on end side */ return 5 elif right <= start: return 3 # completely out on start side */ else: return 1 # partially out on start side */ elif left >= (end + 1): # out on end side */ if right <= start: # & on start side */ return 5 elif right >= (end + 1): return 4 # completely out on end side */ else: return 2 # partially out on end side */ else: if right <= start: return 1 # partially out on start side */ elif right >= (end + 1): return 2 # partially out on end side */ else: return 0 # in bounds */ def ZeroCheck(self, src): data = [] for x in range(0, len(src)): if (src[x] < 0.0): src[x] = 0.0 return src
[docs] def Control(self, src): """ Enter with profile, return with profile mapped to zone """ self.inv = 1 self.num_s = abs(self.end_s - self.start_s) + 1 if (self.left_s > self.right_s): self.inv = -1 interm = abs(self.right_s - self.left_s) interm += 1 self.s_width = self.total_s_width / interm self.step = self.inv * self.min_d_width / self.s_width self.right_edge = self.left_s + self.inv * (self.offset / self.s_width) if self.inv == -1: self.right_edge += 1. dest = [0.0] * self.d_count right = left = 0 src = self.ZeroCheck(src) s = 0 lcstart_s = self.start_s for x in range(0, len(src)): if (src[x] > 0): break s += 1 lcstart_s += 1 s -= lcstart_s lcend_s = self.end_s p = s + lcend_s while src[int(p)] <= 0.0: lcend_s -= 1 p -= 1 right_edge = self.right_edge left_edge = right_edge for x in range(0, self.d_count): left_edge = right_edge # next d in s space * / right_edge += self.step bounds = self.CheckBound(left_edge, right_edge, lcstart_s, lcend_s, self.inv) if bounds == 0: # completely in bounds left = left_edge right = right_edge elif bounds == 1: # partially out on start side if self.inv == 1: left = lcstart_s right = right_edge else: left = left_edge right = lcstart_s elif bounds == 2: # partially out on end side if self.inv == 1: left = left_edge right = lcend_s + 1 else: left = lcend_s + 1 right = right_edge elif bounds == 3: # completely out on start side left = lcstart_s right = lcstart_s + 1 elif bounds == 4: # completely out on end side left = lcend_s right = lcend_s + 1 elif bounds == 5: # completely out of bounds left = lcstart_s right = lcend_s + 1 if left > right: # inverted so right edge is less than left temp = left # swap to do summation * / left = right right = temp j = int(left) k = int(right) min = self.MAXVAL max = 0.0 sum = 0.0 divisor = abs(right - left) if src[j] <= 0.: divisor -= (j + 1 - left) # reduce divisor by width of this partial s elem * / else: value = src[ int(j + s)] # sum or max / min an edge * / sum += (j + 1 - left) * value # ..................................................................... if (j == k): # d lies completely within a single s * / divisor = (k + 1) - left else: value = src[ int(k + s)] if src[ int(k)] <= 0.: divisor -= (right - k) # reduce divisor by width of this partial s elem * / else: # sum or max a partial edge * / sum += (right - k) * value m = j + 1 for m in range(j + 1, k): if (src[ int(m)] <= 0.): divisor -= 1. else: sum += src[int(m)] if bounds == 3 or bounds == 4: dest[int(x)] = 0. else: if divisor > self.THRESHOLD and sum > 0.: # check valid divisor * / dest[int(x)] = sum / divisor # * new average in d * / else: dest[int(x)] = dest[int(x - 1)] # use the adjacent value return dest
if __name__ == '__main__': databoxes = [ 1,2,3,4,5,6,7,8,9,10] parameters= [ "start_s=1", "end_s=10", "left_s=1", "right_s=10", "offset=3", "total_s_width=120", "min_d_width=2", "d_count=40" ] mapclass = MapToZones(parameters) key = mapclass.Control(databoxes) for i, item in enumerate(key, start=1): print(i, item) sleepTime = 5.0 f25 = F25( -1, .018, 1, 40, 40) spt = 0 value = 40 hl = [0] * 9 T1 = [] Q1 = [] t = [] for i in range(0,50): v = f25.Control(value) hl.insert(0,v) hl.pop(len(hl) - 1) value = value + hl[len(hl) - 1] print(i, ":" ,v,":", value) time.sleep(sleepTime) t.append(i * sleepTime) T1.append(value) Q1.append(v) import matplotlib.pyplot as plt import pandas as pd n = 50 tm = np.linspace(0, n - 1, n) plt.figure(figsize=(12, 8)) plt.subplot(2, 1, 1) plt.grid() plt.plot([0, tm[-1] / 60.0], [50, 50], 'k-', label=r'$T_1$ SP') plt.plot(t, T1, 'r.', label=r'$T_1$ PV') plt.ylabel(r'Eng Value ($^oC$)') plt.legend() plt.subplot(2, 1, 2) plt.grid() plt.plot(t, Q1, 'b-', label=r'$Q_1$') plt.ylabel(r'Actuator (%)'); plt.xlabel('Time (Seconds)') plt.legend() plt.show()