Logic Machine Forum
PID script - Printable Version

+- Logic Machine Forum (https://forum.logicmachine.net)
+-- Forum: LogicMachine eco-system (https://forum.logicmachine.net/forumdisplay.php?fid=1)
+--- Forum: Scripting (https://forum.logicmachine.net/forumdisplay.php?fid=8)
+--- Thread: PID script (/showthread.php?tid=2385)



PID script - josdegroot - 12.12.2019

Hopefully someone can help me with the following "problem".



I would like to use the PID script to control the temperature in one of the bedrooms.



The temperature have to be set to 18 degrees... if under setpoint set groupadress ON, if setpoint reached set groupadress OFF.



I saw this example script, but the setpoint must not be an groupadress but a fixed value..




  1. -- init pid algorithm

  2. if not p then

  3.   p = PID:init({

  4.     current = '1/1/1',

  5.     setpoint = '18',

  6.     output = '1/1/3'

  7.   })

  8. end

  9.  

  10. -- run algorithm

  11. p:run()




This script gives the following error:



Resident script:3: attempt to index global 'PID' (a nil value)
stack traceback:




So I think I made a mistake in the setpoint notation... can somebody help me?



Best,



Jos


RE: PID script - Erwin van der Zwart - 12.12.2019

Hi,

You are probably missing the functions that are in the user lib, did you add these to user.pid?

To change the setpoint to a static value you probably need to change the function that gets the value from the object in user.pid, but i would advice to create a virtual object and just set the static value there once.

Next to that you also need require('user.pid') at the start of your script.

There is also a PID block in the FBEditor, maybe this one fits better to your needs.

BR,

Erwin


RE: PID script - josdegroot - 24.12.2019

Thanks Erwin,

I'm already a step further and it lookalike the scripts is working.


If I run the diagnostics inside ETS I see that the Logic machine sends an ON and OFF message to the group that has to turn the electrical radiator on... but nothing will happen....

The scripts that I'm using?

Do you have any idea why the group doesn't respond to the On and Off command? Do I have to set a datatype (switch?) somewhere? 

Code:
-- init pid algorithm
if not p then
  p = PID:init({
    current = '12/2/1',
    setpoint = '32/1/1',
    output = '12/0/7'
  })
end

-- run algorithm
p:run()



RE: PID script - Erwin van der Zwart - 25.12.2019

Hi,

Does all other bus communication works normal? My first reaction would be that the KNX interface is set to TCP/IP instead of TPUART.

BR,

Erwin


RE: PID script - josdegroot - 25.12.2019

All the scheduled scripts @ the Logic machine work fine.

Maybe you can see something @ the screen recording (see attachment)

Link to screen recording: https://www.dropbox.com/s/1n1o9me1vb3ku1h/Screen%20Recording%202019-12-25%20at%2010.26.02.mov?dl=0


BTW... on the screen recording the script is off... Smile. That's not the issue Smile.


RE: PID script - Erwin van der Zwart - 25.12.2019

Hi,

Can you check the script if it uses grp.update instead of grp.write?

grp.update sends to IP but not TP so you can see it on ETS monitor IP side but it’s not send to the TP, i guess that’s the issue you are facing.

BR,

Erwin


RE: PID script - admin - 25.12.2019

From group monitor screenshot it looks like you have KNX TP/IP loop. Do you have any other IP interfaces connected to the same TP line?


RE: PID script - josdegroot - 25.12.2019

(25.12.2019, 11:00)Erwin van der Zwart Wrote: Hi,

Can you check the script if it uses grp.update instead of grp.write?

grp.update sends to IP but not TP so you can see it on ETS monitor IP side but it’s not send to the TP, i guess that’s the issue you are facing.

BR,

Erwin

It uses grp.write... to be sure... this is the script
Code:
PID = {
  -- default params
  defaults = {
    -- invert algorithm, used for cooling
    inverted = false,
    -- minimum output value
    min = 0,
    -- maximum output value
    max = 100,
    -- proportional gain
    kp = 1,
    -- integral gain
    ki = 1,
    -- derivative gain
    kd = 1,
  }
}

-- PID init, returns new PID object
function PID:init(params)
  local n = setmetatable({}, { __index = PID })
  local k, v

  -- set user parameters
  n.params = params

  -- copy parameters that are set by user
  for k, v in pairs(PID.defaults) do
    if n.params[ k ] == nil then
      n.params[ k ] = v
    end
  end

  -- reverse gains in inverted mode
  if n.params.inverted then
    n.params.kp = -n.params.kp
    n.params.ki = -n.params.ki
    n.params.kd = -n.params.kd
  end

  return n
end

-- resets algorithm on init or a switch back from manual mode
function PID:reset()
  -- previous value
  self.previous = grp.getvalue(self.params.current)
  -- reset iterm
  self.iterm = 0
  -- last running time
  self.lasttime = os.time()

  -- clamp iterm
  self:clampiterm()
end

-- clamps iterm value
function PID:clampiterm()
  self.iterm = math.max(self.iterm, self.params.min)
  self.iterm = math.min(self.iterm, self.params.max)
end

-- clamp and set new output value
function PID:setoutput()
  local t, object, value

  self.output = math.max(self.output, self.params.min)
  self.output = math.min(self.output, self.params.max)

  value = math.floor(self.output)
  local t = type(self.params.output)

  -- write to output if object is set
  if t == 'string' or t == 'table' then
    if t == 'string' then
      self.params.output = { self.params.output }
    end

    for _, output in ipairs(self.params.output) do
      grp.write(output, value, dt.scale)
    end
  end
end

-- algorithm step, returns nil when disabled or no action is required, output value otherwise
function PID:run()
  local result

  -- get manual mode status
  local manual = self.params.manual and grp.getvalue(self.params.manual) or false

  -- in manual mode, do nothing
  if manual then
    self.running = false
  -- not in manual, check if reset is required after switching on
  elseif not self.running then
    self:reset()
    self.running = true
  end

  -- compute new value if not in manual mode
  if self.running then
    -- get time between previous and current call
    local now = os.time()
    self.deltatime = now - self.lasttime
    self.lasttime = now

    -- run if previous call was at least 1 second ago
    if self.deltatime > 0 then
      result = self:compute()
    end
  end

  return result
end

-- computes new output value
function PID:compute()
  local current, setpoint, deltasc, deltain, output

  -- get input values
  current = grp.getvalue(self.params.current)
  setpoint = grp.getvalue(self.params.setpoint)

  -- delta between setpoint and current
  deltasc = setpoint - current

  -- calculate new iterm
  self.iterm = self.iterm + self.params.ki * self.deltatime * deltasc
  self:clampiterm()

  -- delta between current and previous value
  deltain = current - self.previous

  -- calculate output value
  self.output = self.params.kp * deltasc + self.iterm
  self.output = self.output - self.params.kd / self.deltatime * deltain

  -- write to output
  self:setoutput()

  -- save previous value
  self.previous = current

  return self.output
end

(25.12.2019, 11:09)admin Wrote: From group monitor screenshot it looks like you have KNX TP/IP loop. Do you have any other IP interfaces connected to the same TP line?

Where can you see this? I'm using a Siemens Ip interface to connect ETS to the TP line. I only would use the logic machine to run scripts..
Can you help me what to change?


RE: PID script - Erwin van der Zwart - 25.12.2019

Hi,

You don’t need to use the Siemens interface as the controller does the same functions, you can remove it. if you keep using it then disable KNX IP features in the KNX settings.

BR,

Erwin


RE: PID script - josdegroot - 25.12.2019

(25.12.2019, 16:13)Erwin van der Zwart Wrote: Hi,

You don’t need to use the Siemens interface as the controller does the same functions, you can remove it. if you keep using it then disable KNX IP features in the KNX settings.

BR,

Erwin

check disabled the checkbox @ the logic machine.

Does it make sense that I have the address 1.1.100 for the logic machine?

Will check if the script is working fine now.

I see that the IP loop is gone Smile. not 4 messages anymore Smile.

But the Switch actor doesn't respond to the On or Off messages the logic machine is sending to group 12/0/7. (See attachment)

If I put A switch object from a taster in this group everything works fine...


RE: PID script - stavros - 10.06.2022

(12.12.2019, 21:22)josdegroot Wrote: Hopefully someone can help me with the following "problem".

I would like to use the PID script to control the temperature in one of the bedrooms.

The temperature have to be set to 18 degrees... if under setpoint set groupadress ON, if setpoint reached set groupadress OFF.

I saw this example script, but the setpoint must not be an groupadress but a fixed value..


  1. -- init pid algorithm

  2. if not p then

  3.   p = PID:init({

  4.     current = '1/1/1',

  5.     setpoint = '18',

  6.     output = '1/1/3'

  7.   })

  8. end

  9.  

  10. -- run algorithm

  11. p:run()


This script gives the following error:

Resident script:3: attempt to index global 'PID' (a nil value)
stack traceback:

So I think I made a mistake in the setpoint notation... can somebody help me?

Best,

Jos

hello,
I have the same problem with you and i don't know what to do to solve it.
Do you remember how you solved it?
Thank you


RE: PID script - admin - 10.06.2022

Add this to Common functions:
Code:
PID = {
  -- default params
  defaults = {
    -- invert algorithm, used for cooling
    inverted = false,
    -- minimum output value
    min = 0,
    -- maximum output value
    max = 100,
    -- proportional gain
    kp = 1,
    -- integral gain
    ki = 1,
    -- derivative gain
    kd = 1,
  }
}

-- PID init, returns new PID object
function PID:init(params)
  local n = setmetatable({}, { __index = PID })
  local k, v

  -- set user parameters
  n.params = params

  -- copy parameters that are set by user
  for k, v in pairs(PID.defaults) do
    if n.params[ k ] == nil then
      n.params[ k ] = v
    end
  end

  -- reverse gains in inverted mode
  if n.params.inverted then
    n.params.kp = -n.params.kp
    n.params.ki = -n.params.ki
    n.params.kd = -n.params.kd
  end

  return n
end

-- resets algorithm on init or a switch back from manual mode
function PID:reset()
  -- previous value
  self.previous = grp.getvalue(self.params.current)
  -- reset iterm
  self.iterm = 0
  -- last running time
  self.lasttime = os.time()

  -- clamp iterm
  self:clampiterm()
end

-- clamps iterm value
function PID:clampiterm()
  self.iterm = math.max(self.iterm, self.params.min)
  self.iterm = math.min(self.iterm, self.params.max)
end

-- clamp and set new output value
function PID:setoutput()
  local t, object, value

  self.output = math.max(self.output, self.params.min)
  self.output = math.min(self.output, self.params.max)

  value = math.floor(self.output)
  local t = type(self.params.output)

  -- write to output if object is set
  if t == 'string' or t == 'table' then
    if t == 'string' then
      self.params.output = { self.params.output }
    end

    for _, output in ipairs(self.params.output) do
      grp.write(output, value, dt.scale)
    end
  end
end

-- algorithm step, returns nil when disabled or no action is required, output value otherwise
function PID:run()
  local result

  -- get manual mode status
  local manual = self.params.manual and grp.getvalue(self.params.manual) or false

  -- in manual mode, do nothing
  if manual then
    self.running = false
  -- not in manual, check if reset is required after switching on
  elseif not self.running then
    self:reset()
    self.running = true
  end

  -- compute new value if not in manual mode
  if self.running then
    -- get time between previous and current call
    local now = os.time()
    self.deltatime = now - self.lasttime
    self.lasttime = now

    -- run if previous call was at least 1 second ago
    if self.deltatime > 0 then
      result = self:compute()
    end
  end

  return result
end

-- computes new output value
function PID:compute()
  local current, setpoint, deltasc, deltain, output

  -- get input values
  current = grp.getvalue(self.params.current)
  setpoint = grp.getvalue(self.params.setpoint)

  -- delta between setpoint and current
  deltasc = setpoint - current

  -- calculate new iterm
  self.iterm = self.iterm + self.params.ki * self.deltatime * deltasc
  self:clampiterm()

  -- delta between current and previous value
  deltain = current - self.previous

  -- calculate output value
  self.output = self.params.kp * deltasc + self.iterm
  self.output = self.output - self.params.kd / self.deltatime * deltain

  -- write to output
  self:setoutput()

  -- save previous value
  self.previous = current

  return self.output
end



RE: PID script - stavros - 10.06.2022

(10.06.2022, 12:54)admin Wrote: Add this to Common functions:
Code:
PID = {
  -- default params
  defaults = {
    -- invert algorithm, used for cooling
    inverted = false,
    -- minimum output value
    min = 0,
    -- maximum output value
    max = 100,
    -- proportional gain
    kp = 1,
    -- integral gain
    ki = 1,
    -- derivative gain
    kd = 1,
  }
}

-- PID init, returns new PID object
function PID:init(params)
  local n = setmetatable({}, { __index = PID })
  local k, v

  -- set user parameters
  n.params = params

  -- copy parameters that are set by user
  for k, v in pairs(PID.defaults) do
    if n.params[ k ] == nil then
      n.params[ k ] = v
    end
  end

  -- reverse gains in inverted mode
  if n.params.inverted then
    n.params.kp = -n.params.kp
    n.params.ki = -n.params.ki
    n.params.kd = -n.params.kd
  end

  return n
end

-- resets algorithm on init or a switch back from manual mode
function PID:reset()
  -- previous value
  self.previous = grp.getvalue(self.params.current)
  -- reset iterm
  self.iterm = 0
  -- last running time
  self.lasttime = os.time()

  -- clamp iterm
  self:clampiterm()
end

-- clamps iterm value
function PID:clampiterm()
  self.iterm = math.max(self.iterm, self.params.min)
  self.iterm = math.min(self.iterm, self.params.max)
end

-- clamp and set new output value
function PID:setoutput()
  local t, object, value

  self.output = math.max(self.output, self.params.min)
  self.output = math.min(self.output, self.params.max)

  value = math.floor(self.output)
  local t = type(self.params.output)

  -- write to output if object is set
  if t == 'string' or t == 'table' then
    if t == 'string' then
      self.params.output = { self.params.output }
    end

    for _, output in ipairs(self.params.output) do
      grp.write(output, value, dt.scale)
    end
  end
end

-- algorithm step, returns nil when disabled or no action is required, output value otherwise
function PID:run()
  local result

  -- get manual mode status
  local manual = self.params.manual and grp.getvalue(self.params.manual) or false

  -- in manual mode, do nothing
  if manual then
    self.running = false
  -- not in manual, check if reset is required after switching on
  elseif not self.running then
    self:reset()
    self.running = true
  end

  -- compute new value if not in manual mode
  if self.running then
    -- get time between previous and current call
    local now = os.time()
    self.deltatime = now - self.lasttime
    self.lasttime = now

    -- run if previous call was at least 1 second ago
    if self.deltatime > 0 then
      result = self:compute()
    end
  end

  return result
end

-- computes new output value
function PID:compute()
  local current, setpoint, deltasc, deltain, output

  -- get input values
  current = grp.getvalue(self.params.current)
  setpoint = grp.getvalue(self.params.setpoint)

  -- delta between setpoint and current
  deltasc = setpoint - current

  -- calculate new iterm
  self.iterm = self.iterm + self.params.ki * self.deltatime * deltasc
  self:clampiterm()

  -- delta between current and previous value
  deltain = current - self.previous

  -- calculate output value
  self.output = self.params.kp * deltasc + self.iterm
  self.output = self.output - self.params.kd / self.deltatime * deltain

  -- write to output
  self:setoutput()

  -- save previous value
  self.previous = current

  return self.output
end
thank you for your answer,
after this i have the following error

"Common functions:331: attempt to perform arithmetic on local 'setpoint' (a nil value)
stack traceback:
Common functions:331: in function 'compute'
Common functions:315: in function 'run'
"


RE: PID script - admin - 10.06.2022

Check that setpoint object address is valid in the pid script.


RE: PID script - stavros - 10.06.2022

(10.06.2022, 13:58)admin Wrote: Check that setpoint object address is valid in the pid script.

here is the script

-- init pid algorithm
  if not p then
    p = PID:init({
      current = '32/1/7',
      setpoint = '32/1/5',
      manual = '32/1/8',
      inverted = 0,
      output = '32/1/6',
      min=0,
      max=100,
      kp=1,
      ki=1,
      kd=1
    })
  end
   
  -- run algorithm
  p:run()

and the objects are shown in the attached


RE: PID script - admin - 10.06.2022

Restart the script via disable/enable. Also inverted should be false not 0. Because 0 evaluates as "true".


RE: PID script - stavros - 10.06.2022

(10.06.2022, 14:12)admin Wrote: Restart the script via disable/enable. Also inverted should be false not 0. Because 0 evaluates as "true".

thank you very much,


RE: PID script - manos@dynamitec - 12.02.2024

Hello Admin,

I am trying to realize a constant light control loop with the PID script you have on the website. 
I've tried to adjust the kp, ki, kd parameters to make the system stable but there are some issues.
Since this not about temperature control and fluctuations to the measured brightness value in the room can be pretty high due to sunny/cloudy day, the system is unstable. I would like to be able to set a min/max step (like +/- 3%) for the value that is written to the bus each control loop even if the PID calculation is resulting to a big difference from the previous value. In this way I would like to avoid positive or negative jumps in the written value. Another thing is to be able to add something like a deadband or hysteresis to the system so if the actual value = setpoint +/- 20 lux for exmple then don't calculate anything. Something similar of what is done now via the manual variable.
As my lua scripting skills are basic could you or anyone in this forum please help me figure this out and how the new script will be? Below I am attaching the script I am using now:
Code:
PID = {
  -- default params
  defaults = {
    -- invert algorithm, used for cooling
    inverted = false,
    -- minimum output value
    min = 10,
    -- maximum output value
    max = 100,
    -- proportional gain
    kp = 0.25,
    -- integral gain
    ki = 0.01,
    -- derivative gain
    kd = 0.1,
  }
}

-- PID init, returns new PID object
function PID:init(params)
  local n = setmetatable({}, { __index = PID })
  local k, v

  -- set user parameters
  n.params = params

  -- copy parameters that are set by user
  for k, v in pairs(PID.defaults) do
    if n.params[ k ] == nil then
      n.params[ k ] = v
    end
  end

  -- reverse gains in inverted mode
  if n.params.inverted then
    n.params.kp = -n.params.kp
    n.params.ki = -n.params.ki
    n.params.kd = -n.params.kd
  end

  return n
end

-- resets algorithm on init or a switch back from manual mode
function PID:reset()
  -- previous value
  self.previous = grp.getvalue(self.params.current)
  -- reset iterm
  self.iterm = 0
  -- last running time
  self.lasttime = os.time()

  -- clamp iterm
  self:clampiterm()
end

-- clamps iterm value
function PID:clampiterm()
  self.iterm = math.max(self.iterm, self.params.min)
  self.iterm = math.min(self.iterm, self.params.max)
end

-- clamp and set new output value
function PID:setoutput()
  local t, object, value

  self.output = math.max(self.output, self.params.min)
  self.output = math.min(self.output, self.params.max)

  value = math.floor(self.output)
  local t = type(self.params.output)

  -- write to output if object is set
  if t == 'string' or t == 'table' then
    if t == 'string' then
      self.params.output = { self.params.output }
    end

    for _, output in ipairs(self.params.output) do
      grp.write(output, value, dt.scale)
    end
  end
end

-- algorithm step, returns nil when disabled or no action is required,
-- output value otherwise
function PID:run()
  local result

  -- get manual mode status
  local manual = false
  if self.params.manual then
    manual = grp.getvalue(self.params.manual)
  end

  -- in manual mode, do nothing
  if manual then
    self.running = false
  -- not in manual, check if reset is required after switching on
  elseif not self.running then
    self:reset()
    self.running = true
  end

  -- compute new value if not in manual mode
  if self.running then
    -- get time between previous and current call
    local now = os.time()
    self.deltatime = now - self.lasttime
    self.lasttime = now

    -- run if previous call was at least 1 second ago
    if self.deltatime > 0 then
      result = self:compute()
    end
  end

  return result
end

-- computes new output value
function PID:compute()
  local current, setpoint, deltasc, deltain, output

  -- get input values
  current = grp.getvalue(self.params.current)
  setpoint = grp.getvalue(self.params.setpoint)

  -- delta between setpoint and current
  deltasc = setpoint - current

  -- calculate new iterm
  self.iterm = self.iterm + self.params.ki * self.deltatime * deltasc
  self:clampiterm()

  -- delta between current and previous value
  deltain = current - self.previous

  -- calculate output value
  self.output = self.params.kp * deltasc + self.iterm
  self.output = self.output - self.params.kd / self.deltatime * deltain

  -- write to output
  self:setoutput()

  -- save previous value
  self.previous = current

  return self.output
end

Thank you in advance.


RE: PID script - Daniel - 13.02.2024

Long time ago I worked on similar solution (not on LM) and to solve this we had to make lux more stable by making an avg form last few values. Try using something like this
https://forum.logicmachine.net/showthread.php?tid=4244&pid=27441#pid27441