Commit 5c4df333 authored by GUILLOU Perceval's avatar GUILLOU Perceval Committed by Perceval Guillou
Browse files

last commit before moving soft pid to the loop object

parent 1ecdca2f
This diff is collapsed.
# -*- coding: utf-8 -*-
#
# This file is part of the bliss project
#
# Copyright (c) 2015-2019 Beamline Control Unit, ESRF
# Distributed under the GNU LGPLv3. See LICENSE for more info.
import sys
import enum
import itertools
import weakref
from bliss.common.regulation import Input, Output, Loop
from bliss.config.plugins.utils import find_class, replace_reference_by_object
TYPE = enum.Enum("TYPE", "INPUT OUTPUT LOOP")
DEFAULT_CLASS_NAME = {TYPE.INPUT: "Input", TYPE.OUTPUT: "Output", TYPE.LOOP: "Loop"}
DEFAULT_CLASS = {TYPE.INPUT: Input, TYPE.OUTPUT: Output, TYPE.LOOP: Loop}
def create_objects_from_config_node(config, node):
# print("CREATE OBJECT FROM CONFIG", node.get('name'))
# --- for a better understanding of this function, see bliss.config.static => config.get( obj_name )
if "inputs" in node or "outputs" in node or "ctrl_loops" in node:
# --- dealing with a controller
obj_name = None
else:
# --- dealing with a child of a controller (Input, Output, Loop) or something else using regulation plugin
obj_name = node.get("name")
node = node.parent # <= step-up to controller node
# --- whatever the object kind, first of all we instanciate the controller
controller_name = node.get("name") # usually is None
controller_class = find_class(node, "bliss.controllers.temperature")
controller = controller_class(node)
controller.initialize()
# --- prepare dictionaries for cached object and instanciated objects
name2cacheditems = {}
name2items = {}
# --- store in cache the sub-objects of the controller for a later instanciation
# --- for each type of a controller sub-node (i.e. inputs, outputs, loops)
for node_type, child_nodes in (
(TYPE.INPUT, node.get("inputs", [])),
(TYPE.OUTPUT, node.get("outputs", [])),
(TYPE.LOOP, node.get("ctrl_loops", [])),
):
# --- for each subnode of a given type, store info in cache
# name2cacheditems.update( { nd['name'] : (node_type, nd.deep_copy(), controller) for nd in child_nodes } )
for nd in child_nodes:
# name = nd["name"]
# if name.startswith(
# "$"
# ): # --- handle custom inputs/outputs which are given with a name as a reference (i.e. $name)
# new_obj = config.get(name) # --- instanciate custom object now
# name2items[
# name
# ] = new_obj # --- register in instanciated object dict of the config
# new_obj._controller = controller # --- define the associated controller
# controller.register_object(
# new_obj
# ) # --- register the custom device in the controller
# else: # --- store in cache devices which are not custom
name2cacheditems[nd["name"]] = (node_type, nd.deep_copy(), controller)
# --- add the controller to stored items if it has a name
if controller_name:
name2items[controller_name] = controller
# update the config cache dict NOW to avoid cyclic instanciation (i.e. config.get => create_object_from_... => replace_reference_by_object => config.get )
yield name2items, name2cacheditems
# --- don't forget to instanciate the object for which this function has been called
# --- if it isn't a controller
if obj_name is not None:
obj = config.get(obj_name)
yield {obj_name: obj}
# --- NOW, any new object_name going through 'config.get( obj_name )' should call 'create_object_from_cache' only.
# --- 'create_objects_from_config_node' should never be called again for any object related to the controller instanciated here (see config.get code)
def create_object_from_cache(config, name, object_info):
# for a better understanding of this function, see bliss.config.static => config.get( obj_name )
# print(f"===== CREATE FROM CACHE: {name}")
node_type, node, controller = object_info
replace_reference_by_object(config, node)
controller_module = sys.modules[controller.__module__]
object_class_name = node.get("class", DEFAULT_CLASS_NAME[node_type])
try:
object_class = getattr(controller_module, object_class_name)
except AttributeError:
object_class = DEFAULT_CLASS[node_type]
new_object = controller.create_object(node_type.name, object_class, node)
return new_object
......@@ -5,86 +5,61 @@
# Copyright (c) 2015-2019 Beamline Control Unit, ESRF
# Distributed under the GNU LGPLv3. See LICENSE for more info.
from bliss.controllers.regulator import Controller
from bliss.common.regulation import ExternalInput, ExternalOutput
from bliss.controllers.regulator import Controller, SoftController
from bliss.common.regulation import Input, Output, Loop
import random
import time
import math
import gevent
from bliss.common.logtools import log_debug
from bliss.common.utils import object_method, object_method_type
from bliss.common.utils import object_attribute_get, object_attribute_type_get
from bliss.common.utils import object_attribute_set, object_attribute_type_set
from bliss.common.logtools import log_info, log_debug
from simple_pid import PID
# DEFAULT INITIAL PARAMETERS
DEGREE_PER_SECOND = 1.0
INITIAL_TEMP = 0.0
INITIAL_OUTPUT_VALUE = 0.0
INITIAL_POWER = 0.0
# ======= FOR TESTINGS =================
# from bliss.controllers.regulator import RegPlot
# s = sample_regulation
# plt = RegPlot(s)
# plt.start()
# s.setpoint = 20.
class MyDevice:
""" Fake device that simulates any device which is not a standard Bliss object.
This device will be used as a custom Input or Output by a SoftLoop.
""" Fake device that simulates any complex device which is not a standard Bliss object.
This device will be used as a Custom Input or Output by a SoftRegulation controller.
"""
def __init__(self, name="FakeDevice", config=None):
def __init__(self, name="FakeDevice"):
self.name = name
self.current_temp = 10. # deg
# live temp simulator attributes
self.cooling_rate = 1. # deg per sec
self.heating_rate = 0. # deg per sec
self._cool_down_tasks = None
self._stop_cool_down_events = None
self._cool_down_task_frequency = 20.0
if self._stop_cool_down_events is None:
self._stop_cool_down_events = gevent.event.Event()
if not self._cool_down_tasks:
self._cool_down_tasks = gevent.spawn(self._cooling_task)
def get_current_temp(self):
""" read the current temperature (like a sensor) """
return self.current_temp
def get_heating_rate(self):
return self.heating_rate
def set_heating_rate(self, heating_rate):
""" set the current heating rate (like an heater output) """
self.heating_rate = heating_rate
def _cooling_task(self):
self._stop_cool_down_events.clear()
last_time = time.time()
while not self._stop_cool_down_events.is_set():
gevent.sleep(1. / self._cool_down_task_frequency)
now = time.time()
dt = now - last_time
last_time = now
self.value = 0
self.current_temp = max(
0., self.current_temp + self.heating_rate * dt - self.cooling_rate * dt
)
def get_value(self):
return self.value
def set_value(self, value):
self.value = value
class MyCustomInput(ExternalInput):
""" Interface to handle an arbitrary device as a regulation Input """
class MyCustomInput(Input):
def __init__(self, name, config):
super().__init__(config)
super().__init__(None, config)
self.device = MyDevice()
def read(self):
""" returns the input device value (in input unit) """
log_debug(self, "MyCustomInput:read")
return self.device.get_current_temp()
return self.device.get_value()
def state(self):
""" returns the input device state """
......@@ -93,20 +68,20 @@ class MyCustomInput(ExternalInput):
return "READY"
class MyCustomOutput(ExternalOutput):
""" Interface to handle an arbitrary device as a regulation Output """
class MyCustomOutput(Output):
def __init__(self, name, config):
super().__init__(config)
super().__init__(None, config)
self.device = MyDevice()
def read(self):
""" returns the output device value (in output unit) """
""" returns the input device value (in input unit) """
log_debug(self, "MyCustomOutput:read")
return self.device.get_heating_rate()
return self.device.get_value()
def state(self):
""" returns the output device state """
""" returns the input device state """
log_debug(self, "MyCustomOutput:state")
return "READY"
......@@ -116,80 +91,60 @@ class MyCustomOutput(ExternalOutput):
log_debug(self, "MyCustomOutput:_set_value %s" % value)
self.device.set_heating_rate(value)
if None not in self._limits:
value = max(value, self._limits[0])
value = min(value, self._limits[1])
self.device.set_value(value)
class Mockup(Controller):
""" Simulate a regulation controller.
The PID regulation is handled by the controller hardware (simulated).
This mockup starts '_cool_down_tasks' that simulates a natural cool down of the temperatures measured by the inputs.
The cooling rate is defined by the special pararmeter 'cooling_rate' associated to the inputs.
Also the task simulates the effect of the output devices on the temperatures measured by the inputs.
The outputs have an 'heating_rate' parameter that defines how much the outputs will heat and make the temperatures rising.
The increase of temperatures is proportional to 'heating_rate * output_power' with output_power in range [0,1].
class Mockup(SoftController):
""" Simulate a soft controller.
The PID regulation is handled by the software (see 'SoftController' class)
The ramping cmds (on setpoint or output power) are handled by the software (see 'Ramp' and 'OutputRamp' classes)
This mockup starts a 'dummy_output_task' that simulates a natural cool down of the system if the current value of the output
is inferior to the 'equilibrium_value'. It means that:
- the value of the input device will increase if output value > 'equilibrium_value'
- the value of the input device will decrease if output value < 'equilibrium_value'
- the value of the input device will stay stable if output value = 'equilibrium_value'
"""
def __init__(self, config):
super().__init__(config)
__material = "Hg"
# attributes to simulate the behaviour of the controller hardware
def __init__(self, config, *args):
self._cool_down_tasks = {}
self._stop_cool_down_events = {}
self._cool_down_task_frequency = 20.0
self._pid_tasks = {}
self._stop_pid_events = {}
SoftController.__init__(self, config, *args)
self.dummy_output_tasks = {}
self._stop_dummy_events = {}
self._stop_dummy_events = {} # gevent.event.Event()
self.dummy_output_task_frequency = 20.0
self.pids = {}
def __del__(self):
for spe in self._stop_pid_events.values():
spe.set()
for sde in self._stop_cool_down_events.values():
for sde in self._stop_dummy_events.values():
sde.set()
def initialize_controller(self):
# self._stop_dummy_event.set()
super().__del__()
def initialize(self):
# host becomes mandatory
log_debug(self, "mockup: initialize ")
self.host = self.config.get("host", str)
# simulate the PID processes handled by the controller hardware
for loop_node in self.config.get("ctrl_loops", []):
loop_name = loop_node.get("name")
self.pids[loop_name] = PID(
Kp=1.0,
Ki=0.0,
Kd=0.0,
setpoint=0.0,
sample_time=0.01,
output_limits=(0.0, 1.0),
auto_mode=True,
proportional_on_measurement=False,
)
def initialize_input(self, tinput):
log_debug(self, "mockup: initialize_input: %s" % (tinput))
tinput._attr_dict["value"] = INITIAL_TEMP
tinput._attr_dict["last_cool_time"] = 0.0
tinput._attr_dict["cooling_rate"] = tinput.config.get("cooling_rate", 1.0)
tinput._attr_dict["last_read_time"] = 0.0
def initialize_output(self, toutput):
log_debug(self, "mockup: initialize_output: %s" % (toutput))
toutput._attr_dict["value"] = INITIAL_OUTPUT_VALUE
toutput._attr_dict["value"] = INITIAL_POWER
toutput._attr_dict["equilibrium_value"] = toutput.config.get(
"equilibrium_value", 0.3
)
toutput._attr_dict["heating_rate"] = toutput.config.get(
"heating_rate", DEGREE_PER_SECOND
)
......@@ -197,166 +152,8 @@ class Mockup(Controller):
def initialize_loop(self, tloop):
log_debug(self, "mockup: initialize_loop: %s" % (tloop))
def set_kp(self, tloop, kp):
"""
Set the PID P value
Raises NotImplementedError if not defined by inheriting class
Args:
tloop: Loop class type object
kp: the kp value
"""
log_debug(self, "Controller:set_kp: %s %s" % (tloop, kp))
self.pids[tloop.name].Kp = kp
def get_kp(self, tloop):
"""
Get the PID P value
Raises NotImplementedError if not defined by inheriting class
Args:
tloop: Loop class type object
Returns:
kp value
"""
log_debug(self, "Controller:get_kp: %s" % (tloop))
return self.pids[tloop.name].Kp
def set_ki(self, tloop, ki):
"""
Set the PID I value
Raises NotImplementedError if not defined by inheriting class
Args:
tloop: Loop class type object
ki: the ki value
"""
log_debug(self, "Controller:set_ki: %s %s" % (tloop, ki))
self.pids[tloop.name].Ki = ki
def get_ki(self, tloop):
"""
Get the PID I value
Raises NotImplementedError if not defined by inheriting class
Args:
tloop: Loop class type object
Returns:
ki value
"""
log_debug(self, "Controller:get_ki: %s" % (tloop))
return self.pids[tloop.name].Ki
def set_kd(self, tloop, kd):
"""
Set the PID D value
Raises NotImplementedError if not defined by inheriting class
Args:
tloop: Loop class type object
kd: the kd value
"""
log_debug(self, "Controller:set_kd: %s %s" % (tloop, kd))
self.pids[tloop.name].Kd = kd
def get_kd(self, tloop):
"""
Reads the PID D value
Raises NotImplementedError if not defined by inheriting class
Args:
tloop: Output class type object
Returns:
kd value
"""
log_debug(self, "Controller:get_kd: %s" % (tloop))
return self.pids[tloop.name].Kd
def get_sampling_frequency(self, tloop):
"""
Get the sampling frequency (PID)
Raises NotImplementedError if not defined by inheriting class
Args:
tloop: Loop class type object
"""
log_debug(self, "Controller:get_sampling_frequency: %s" % (tloop))
return 1. / self.pids[tloop.name].sample_time
def set_sampling_frequency(self, tloop, value):
"""
Set the sampling frequency (PID)
Raises NotImplementedError if not defined by inheriting class
Args:
tloop: Loop class type object
value: the sampling frequency [Hz]
"""
log_debug(self, "Controller:set_sampling_frequency: %s %s" % (tloop, value))
self.pids[tloop.name].sample_time = 1. / value
def get_pid_range(self, tloop):
"""
Get the PID range (PID output value limits)
"""
log_debug(self, "Controller:get_pid_range: %s" % (tloop))
return self.pids[tloop.name].output_limits
def set_pid_range(self, tloop, pid_range):
"""
Set the PID range (PID output value limits)
"""
log_debug(self, "Controller:set_pid_range: %s %s" % (tloop, pid_range))
self.pids[tloop.name].output_limits = pid_range
def start_regulation(self, tloop):
"""
Starts the regulation process.
Does NOT start the ramp, use 'start_ramp' to do so.
Raises NotImplementedError if not defined by inheriting class
Args:
tloop: Loop class type object
"""
log_debug(self, "Controller:start_regulation: %s" % (tloop))
if self._stop_cool_down_events.get(tloop.name) is None:
self._stop_cool_down_events[tloop.name] = gevent.event.Event()
if not self._cool_down_tasks.get(tloop.name):
self._cool_down_tasks[tloop.name] = gevent.spawn(self._cooling_task, tloop)
if self._stop_pid_events.get(tloop.name) is None:
self._stop_pid_events[tloop.name] = gevent.event.Event()
if not self._pid_tasks.get(tloop.name):
self._pid_tasks[tloop.name] = gevent.spawn(self._pid_task, tloop)
def stop_regulation(self, tloop):
"""
Stops the regulation process.
Does NOT stop the ramp, use 'stop_ramp' to do so.
Raises NotImplementedError if not defined by inheriting class
Args:
tloop: Loop class type object
"""
log_debug(self, "Controller:stop_regulation: %s" % (tloop))
self._stop_pid_events[tloop.name].set()
self.dummy_output_tasks[tloop.name] = None
self._stop_dummy_events[tloop.name] = gevent.event.Event()
def read_input(self, tinput):
"""Reading on a Input object"""
......@@ -373,82 +170,55 @@ class Mockup(Controller):
log_debug(self, "mockup:set_output_value: %s %s" % (toutput, value))
toutput._attr_dict["value"] = value
def set_setpoint(self, tloop, sp, **kwargs):
"""
Set the current setpoint (target value).
Does NOT start the PID process, use 'start_regulation' to do so.
Raises NotImplementedError if not defined by inheriting class
Args:
tloop: Loop class type object
sp: setpoint (in tloop.input unit)
**kwargs: auxilliary arguments
"""
log_debug(self, "Controller:set_setpoint: %s %s" % (tloop, sp))
self.pids[tloop.name].setpoint = sp
def start_regulation(self, tloop):
""" Starts the regulation loop """
def get_setpoint(self, tloop):
"""
Get the current setpoint (target value)
Raises NotImplementedError if not defined by inheriting class
if not self.dummy_output_tasks[tloop.name]:
self.dummy_output_tasks[tloop.name] = gevent.spawn(
self._do_dummy_output_task, tloop
)
Args:
tloop: Loop class type object
super().start_regulation(tloop)
Returns:
(float) setpoint value (in tloop.input unit).
def _do_dummy_output_task(self, tloop):
""" Simulates a system that naturally cool down if not heated by the output device.
The output device value must be > 'equilibrium_value' to compensate energy loss of the system.
"""
log_debug(self, "Controller:get_setpoint: %s" % (tloop))
return self.pids[tloop.name].setpoint
self._stop_dummy_events[tloop.name].clear()
tloop.input._attr_dict["last_read_time"] = time.time()
def _cooling_task(self, tloop):
self._stop_cool_down_events[tloop.name].clear()
tloop.input._attr_dict["last_cool_time"] = time.time()
while not self._stop_cool_down_events[tloop.name].is_set():
# compute elapsed time since last call
while not self._stop_dummy_events[tloop.name].is_set():
tnow = time.time()
dt = tnow - tloop.input._attr_dict["last_cool_time"]
tloop.input._attr_dict["last_cool_time"] = tnow
# compute how much the temperature has naturally decreased because of the physical system losses
cooling = dt * tloop.input._attr_dict["cooling_rate"]
# compute how much the temperature has increased because of the output device effect
if not None in tloop.output.limits:
power = (tloop.output._attr_dict["value"] - tloop.output.limits[0]) / (
tloop.output.limits[1] - tloop.output.limits[0]
)
else:
power = tloop.output._attr_dict["value"]