Commit 26ca9aa9 authored by GUILLOU Perceval's avatar GUILLOU Perceval Committed by Perceval Guillou
Browse files

modify pluggin, mockup, add mockup soft loop, clean everything

parent ce93e5bc
This diff is collapsed.
......@@ -7,9 +7,14 @@
import sys
import enum
import itertools
import weakref
from bliss.common.regulation import Input, Output, Loop
from bliss.common.regulation import (
Input,
Output,
Loop,
SoftLoop,
ExternalInput,
ExternalOutput,
)
from bliss.config.plugins.utils import find_class, replace_reference_by_object
......@@ -22,17 +27,46 @@ 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 )
# --- prepare dictionaries for cached object and instanciated objects
name2cacheditems = {}
name2items = {}
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
# --- dealing with a child of a controller (Input, Output, Loop) or an object defined outside of a controller (like ExternalIn/out or SoftLoop)
obj_name = node.get("name")
node = node.parent # <= step-up to controller node
upper_node = node.parent # <= check parent node and see if it is a controller
if (
"inputs" in upper_node
or "outputs" in upper_node
or "ctrl_loops" in upper_node
): # <= if True it is a contoller
node = upper_node
else: # <= else it is an object without a controller (like ExternalIn/out or SoftLoop)
replace_reference_by_object(config, node)
if node.get("class") in ["SoftLoop", "Loop"]:
new_obj = SoftLoop(node)
elif node.get("class") in ["Input", "ExternalInput"]:
new_obj = ExternalInput(node)
elif node.get("class") in ["Output", "ExternalOutput"]:
new_obj = ExternalOutput(node)
name2items[obj_name] = new_obj
config.update_items(name2items)
return name2items
# --- whatever the object kind, first of all we instanciate the controller
controller_name = node.get("name") # usually is None
......@@ -40,10 +74,6 @@ def create_objects_from_config_node(config, node):
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 (
......@@ -53,22 +83,7 @@ def create_objects_from_config_node(config, node):
):
# --- 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
......@@ -78,8 +93,7 @@ def create_objects_from_config_node(config, node):
# 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
# --- don't forget to instanciate the object for which this function has been called (if not a controller)
if obj_name is not None:
obj = config.get(obj_name)
yield {obj_name: obj}
......@@ -92,7 +106,6 @@ 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)
......@@ -104,5 +117,5 @@ def create_object_from_cache(config, name, object_info):
except AttributeError:
object_class = DEFAULT_CLASS[node_type]
new_object = controller.create_object(node_type.name, object_class, node)
new_object = controller.add_object(node_type.name, object_class, node)
return new_object
......@@ -5,37 +5,25 @@
# Copyright (c) 2015-2019 Beamline Control Unit, ESRF
# Distributed under the GNU LGPLv3. See LICENSE for more info.
from bliss.controllers.regulator import Controller, SoftController
from bliss.common.regulation import Input, Output, Loop
from bliss.controllers.regulator import Controller
from bliss.common.regulation import Input, Output
import random
import time
import math
import gevent
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 bliss.common.logtools import log_debug
from simple_pid import PID
# DEFAULT INITIAL PARAMETERS
DEGREE_PER_SECOND = 1.0
INITIAL_TEMP = 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.
INITIAL_OUTPUT_VALUE = 0.0
class MyDevice:
""" 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.
""" 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.
"""
def __init__(self, name="FakeDevice"):
......@@ -50,6 +38,8 @@ class MyDevice:
class MyCustomInput(Input):
""" Interface to handle an arbitrary device as a regulation Input """
def __init__(self, name, config):
super().__init__(None, config)
......@@ -59,7 +49,12 @@ class MyCustomInput(Input):
""" returns the input device value (in input unit) """
log_debug(self, "MyCustomInput:read")
return self.device.get_value()
# return self.device.get_value()
xmin, xmax = self.config["feedback"].limits
value = (self.config["feedback"].read() - xmin) / (xmax - xmin) * 100.
return value
def state(self):
""" returns the input device state """
......@@ -69,19 +64,21 @@ class MyCustomInput(Input):
class MyCustomOutput(Output):
""" Interface to handle an arbitrary device as a regulation Output """
def __init__(self, name, config):
super().__init__(None, config)
self.device = MyDevice()
def read(self):
""" returns the input device value (in input unit) """
""" returns the output device value (in output unit) """
log_debug(self, "MyCustomOutput:read")
return self.device.get_value()
def state(self):
""" returns the input device state """
""" returns the output device state """
log_debug(self, "MyCustomOutput:state")
return "READY"
......@@ -91,60 +88,80 @@ class MyCustomOutput(Output):
log_debug(self, "MyCustomOutput:_set_value %s" % 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(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)
class Mockup(Controller):
""" Simulate a regulation controller.
The PID regulation is handled by the controller hardware (simulated).
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'
"""
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].
__material = "Hg"
"""
def __init__(self, config, *args):
SoftController.__init__(self, config, *args)
super().__init__(config, *args)
# attributes to simulate the behaviour of the controller hardware
self._cool_down_tasks = {}
self._stop_cool_down_events = {}
self._cool_down_task_frequency = 20.0
self._pid_tasks = {}
self._stop_pid_events = {}
self.dummy_output_tasks = {}
self._stop_dummy_events = {} # gevent.event.Event()
self._stop_dummy_events = {}
self.dummy_output_task_frequency = 20.0
self.pids = {}
def __del__(self):
for sde in self._stop_dummy_events.values():
sde.set()
for spe in self._stop_pid_events.values():
spe.set()
# self._stop_dummy_event.set()
super().__del__()
for sde in self._stop_cool_down_events.values():
sde.set()
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_read_time"] = 0.0
tinput._attr_dict["last_cool_time"] = 0.0
tinput._attr_dict["cooling_rate"] = tinput.config.get("cooling_rate", 1.0)
def initialize_output(self, toutput):
log_debug(self, "mockup: initialize_output: %s" % (toutput))
toutput._attr_dict["value"] = INITIAL_POWER
toutput._attr_dict["equilibrium_value"] = toutput.config.get(
"equilibrium_value", 0.3
)
toutput._attr_dict["value"] = INITIAL_OUTPUT_VALUE
toutput._attr_dict["heating_rate"] = toutput.config.get(
"heating_rate", DEGREE_PER_SECOND
)
......@@ -152,8 +169,164 @@ class Mockup(SoftController):
def initialize_loop(self, tloop):
log_debug(self, "mockup: initialize_loop: %s" % (tloop))
self.dummy_output_tasks[tloop.name] = None
self._stop_dummy_events[tloop.name] = gevent.event.Event()
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)
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))
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()
def read_input(self, tinput):
"""Reading on a Input object"""
......@@ -170,55 +343,82 @@ class Mockup(SoftController):
log_debug(self, "mockup:set_output_value: %s %s" % (toutput, value))
toutput._attr_dict["value"] = value
def start_regulation(self, tloop):
""" Starts the regulation loop """
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))
if not self.dummy_output_tasks[tloop.name]:
self.dummy_output_tasks[tloop.name] = gevent.spawn(
self._do_dummy_output_task, tloop
)
self.pids[tloop.name].setpoint = sp
super().start_regulation(tloop)
def get_setpoint(self, tloop):
"""
Get the current setpoint (target value)
Raises NotImplementedError if not defined by inheriting class
Args:
tloop: Loop class type object
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.
Returns:
(float) setpoint value (in tloop.input unit).
"""
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):
while not self._stop_dummy_events[tloop.name].is_set():
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
tnow = time.time()
dt = tnow - tloop.input._attr_dict["last_read_time"]
tloop.input._attr_dict["last_read_time"] = tnow
dt = tnow - tloop.input._attr_dict["last_cool_time"]
tloop.input._attr_dict["last_cool_time"] = tnow
fac = (
tloop.output._attr_dict["value"]
- tloop.output._attr_dict["equilibrium_value"]
)
# convert output value (in device unit) to its corresponding power_value (which is in range [0,1])
# to apply a ponderation factor on heating/cooling.
fac = tloop.output.get_unit2power(fac)
dtemp = dt * tloop.output._attr_dict["heating_rate"] * fac
# compute how much the temperature has naturally decreased because of the physical system losses
cooling = dt * tloop.input._attr_dict["cooling_rate"]
tloop.input._attr_dict["value"] += dtemp
# 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"]
gevent.sleep(1. / self.dummy_output_task_frequency)
heating = dt * tloop.output._attr_dict["heating_rate"] * power
"""
Custom commands and Attributes
"""
# update temperature value
tloop.input._attr_dict["value"] += heating - cooling
tloop.input._attr_dict["value"] = max(-273, tloop.input._attr_dict["value"])
gevent.sleep(1. / self._cool_down_task_frequency)
def _pid_task(self, tloop):
# simulate the PID processes handled by the controller hardware
self._stop_pid_events[tloop.name].clear()
while not self._stop_pid_events[tloop.name].is_set():
input_value = tloop.input.read()
power_value = self.pids[tloop.name](input_value)
@object_method_type(types_info=("str", "str"), type=Input)
def get_double_str(self, tinput, value):
return value + "_" + value
output_value = tloop._get_power2unit(power_value)
# Custom Attribute
@object_attribute_type_get(type_info=("str"), type=Output)
def get_material(self, toutput):
return self.__material
if not tloop.is_in_idleband():