Commit 24b008a1 authored by Perceval Guillou's avatar Perceval Guillou
Browse files

Regulator as BlissController

parent 31029613
......@@ -170,6 +170,8 @@ import gevent
import gevent.event
import enum
from bliss.common.protocols import CounterContainer
from bliss.common.logtools import log_debug, disable_user_output
from bliss.common.utils import with_custom_members, autocomplete_property
from bliss.common.counter import SamplingCounter
......@@ -200,38 +202,38 @@ def _get_external_device_name(device):
return device
class SCC(SamplingCounterController):
def __init__(self, name, boss):
super().__init__(name)
self.boss = boss
def read_all(self, *counters):
return [self.boss.read()]
@with_custom_members
class Input(SamplingCounterController):
class Input(CounterContainer):
"""Implements the access to an input device which is accessed via the
regulation controller (like a sensor plugged on a channel of the controller)
"""
def __init__(self, controller, config):
""" Constructor """
super().__init__(name=config["name"])
self._name = config["name"]
self._controller = controller
self._config = config
self._channel = self._config.get("channel")
self.max_sampling_frequency = config.get("max_sampling_frequency", 5)
# useful attribute for a temperature controller writer
self._attr_dict = {}
self._build_counters()
def _build_counters(self):
self.create_counter(
SamplingCounter,
self.name + "_counter",
unit=self._config.get("unit", "N/A"),
mode=self._config.get("sampling-counter-mode", "SINGLE"),
)
@property
def name(self):
return self._name
def read_all(self, *counters):
return [self.read()]
@autocomplete_property
def counters(self):
return counter_namespace({self.name: self._controller.counters[self.name]})
# ----------- BASE METHODS -----------------------------------------
......@@ -316,7 +318,13 @@ class ExternalInput(Input):
self.device = config.get("device")
self.load_base_config()
# ----------- METHODS THAT A CHILD CLASS MAY CUSTOMIZE ------------------
self._controller = SCC(self.name, self)
self._controller.create_counter(
SamplingCounter,
self.name,
unit=self._config.get("unit"),
mode=self._config.get("mode", "SINGLE"),
)
def __info__(self):
lines = ["\n"]
......@@ -358,7 +366,7 @@ class ExternalInput(Input):
@with_custom_members
class Output(SamplingCounterController):
class Output(CounterContainer):
""" Implements the access to an output device which is accessed via the regulation controller (like an heater plugged on a channel of the controller)
The Output has a ramp object.
......@@ -370,7 +378,7 @@ class Output(SamplingCounterController):
def __init__(self, controller, config):
""" Constructor """
super().__init__(name=config["name"])
self._name = config["name"]
self._controller = controller
self._config = config
......@@ -387,20 +395,13 @@ class Output(SamplingCounterController):
# useful attribute for a temperature controller writer
self._attr_dict = {}
self.max_sampling_frequency = config.get("max_sampling_frequency", 5)
self._build_counters()
def _build_counters(self):
self.create_counter(
SamplingCounter,
self.name + "_counter",
unit=self._config.get("unit", "N/A"),
mode=self._config.get("sampling-counter-mode", "SINGLE"),
)
@property
def name(self):
return self._name
def read_all(self, *counters):
return [self.read()]
@autocomplete_property
def counters(self):
return counter_namespace({self.name: self._controller.counters[self.name]})
# ----------- BASE METHODS -----------------------------------------
......@@ -608,6 +609,14 @@ class ExternalOutput(Output):
self.mode = config.get("mode", "relative")
self.load_base_config()
self._controller = SCC(self.name, self)
self._controller.create_counter(
SamplingCounter,
self.name,
unit=self._config.get("unit"),
mode=self._config.get("mode", "SINGLE"),
)
# ----------- BASE METHODS -----------------------------------------
@property
......@@ -700,7 +709,7 @@ class ExternalOutput(Output):
@with_custom_members
class Loop(SamplingCounterController):
class Loop(CounterContainer):
""" Implements the access to the regulation loop
The regulation is the PID process that:
......@@ -733,7 +742,7 @@ class Loop(SamplingCounterController):
def __init__(self, controller, config):
""" Constructor """
super().__init__(name=config["name"])
self._name = config["name"]
self._controller = controller
self._config = config
......@@ -761,20 +770,8 @@ class Loop(SamplingCounterController):
self.reg_plot = None
self.max_sampling_frequency = config.get("max_sampling_frequency", 5)
self._build_counters()
self._create_soft_axis()
def _build_counters(self):
self.create_counter(
SamplingCounter,
self.name + "_setpoint",
unit=self.input.config.get("unit", "N/A"),
mode="SINGLE",
)
def __del__(self):
self.close()
......@@ -787,8 +784,16 @@ class Loop(SamplingCounterController):
# ----------- BASE METHODS -----------------------------------------
def read_all(self, *counters):
return [self._get_working_setpoint()]
@lazy_init
def read(self):
""" Return the current working setpoint """
log_debug(self, "Loop:read")
return self._get_working_setpoint()
@property
def name(self):
return self._name
##--- CONFIG METHODS
def load_base_config(self):
......@@ -847,7 +852,7 @@ class Loop(SamplingCounterController):
all_counters = (
list(self.input.counters)
+ list(self.output.counters)
+ list(self._counters.values())
+ [self._controller.counters[self.name]]
)
return counter_namespace(all_counters)
......@@ -1484,6 +1489,14 @@ class SoftLoop(Loop):
self.load_base_config()
self.max_attempts_before_failure = config.get("max_attempts_before_failure", 3)
self._controller = SCC(self.name, self)
self._controller.create_counter(
SamplingCounter,
self.name,
unit=self._config.get("unit"),
mode=self._config.get("mode", "SINGLE"),
)
def __info__(self):
lines = ["\n"]
lines.append(f"=== SoftLoop: {self.name} ===")
......@@ -1517,6 +1530,12 @@ class SoftLoop(Loop):
self._ramp.stop()
self._stop_regulation()
def read(self):
""" Return the current working setpoint """
log_debug(self, "SoftLoop:read")
return self._get_working_setpoint()
@property
def max_attempts_before_failure(self):
"""
......
......@@ -6,10 +6,22 @@
# Distributed under the GNU LGPLv3. See LICENSE for more info.
from re import subn
# ================ IMPORTANT NOTE_ ABOUT PLUGIN CYCLIC IMPORT =============
#
# The plugin prevent cyclic import thanks to the yield name2cacheditems tricks
# in create_objects_from_config_node() below.
#
# however the best way to avoid this problem would be to NOT allow references ($name) of
# a bliss_controller item within another item of the same bliss_controller.
# In other words, in a bliss_controller config, only references to external objects should be allowed.
# (external = not owned by the bliss controller itself)
# If a BC item needs to reference another item of the same BC, then just using the item name (without '$')
# should be enough, as the BC knows its items and associated names.
from bliss.config.plugins.utils import find_class_and_node
# from bliss.config.static import ConfigNode, ConfigReference
from bliss.config.static import ConfigNode, ConfigList
def find_sub_names_config(
......@@ -34,13 +46,18 @@ def find_sub_names_config(
if not exclude_ref or not config.get("name").startswith("$"):
selection[level].append((config, parent_key))
for k, v in config.items():
if isinstance(v, dict):
for (
k,
v,
) in (
config.raw_items()
): # !!! raw_items to avoid cyclic import while resloving reference !!!
if isinstance(v, ConfigNode):
find_sub_names_config(v, selection, level + 1, k)
elif isinstance(v, list):
elif isinstance(v, ConfigList):
for i in v:
if isinstance(i, dict):
if isinstance(i, ConfigNode):
find_sub_names_config(i, selection, level + 1, k)
return selection
......@@ -71,6 +88,7 @@ def create_objects_from_config_node(cfg_obj, cfg_node):
# search the 'class' key in cfg_node or at a upper node level
# return the class and the associated config node
# upper_node = cfg_node.parent ??
klass, ctrl_node = find_class_and_node(cfg_node)
# print("=== FOUND BLISS CONTROLLER CLASS", klass, "WITH NODE", ctrl_node)
......@@ -78,18 +96,18 @@ def create_objects_from_config_node(cfg_obj, cfg_node):
item_name = cfg_node["name"] # name of the item that should be created and returned
# always create the bliss controller first
bctrl = klass(ctrl_name, ctrl_node.clone())
bctrl = klass(ctrl_node.clone())
# find all sub objects with a name in controller config
sub_cfgs = find_sub_names_config(ctrl_node.to_dict())
sub_cfgs = find_sub_names_config(ctrl_node) # .to_dict(resolve_references=False))
for level in sorted(sub_cfgs.keys()):
if level != 0: # ignore the controller itself
for cfg, pkey in sub_cfgs[level]:
subname = cfg["name"]
if subname == item_name: # this is the sub-object to return
name2items[item_name] = bctrl._create_sub_item(item_name, cfg, pkey)
else: # store sub-object info for later instantiation
name2cacheditems[subname] = (bctrl, cfg, pkey)
# if subname == item_name: # this is the sub-object to return
# name2items[item_name] = bctrl._create_sub_item(item_name, cfg, pkey)
# else: # store sub-object info for later instantiation
name2cacheditems[subname] = (bctrl, cfg, pkey)
# --- add the controller to stored items if it has a name
if ctrl_name:
......@@ -98,6 +116,11 @@ def create_objects_from_config_node(cfg_obj, cfg_node):
# update the config cache dict NOW to avoid cyclic instanciation (i.e. config.get => create_object_from_... => config.get )
yield name2items, name2cacheditems
# --- don't forget to instanciate the object for which this function has been called (if not a controller)
if item_name != ctrl_name:
obj = cfg_obj.get(item_name)
yield {item_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)
......@@ -105,4 +128,5 @@ def create_objects_from_config_node(cfg_obj, cfg_node):
def create_object_from_cache(config, name, cached_object_info):
print("===== REGULATION FROM CACHE", name) # config, name, object_info)
bctrl, cfg, pkey = cached_object_info
return bctrl._create_sub_item(name, cfg, pkey)
new_object = bctrl._create_sub_item(name, cfg, pkey)
return new_object
......@@ -5,11 +5,9 @@
# Copyright (c) 2015-2020 Beamline Control Unit, ESRF
# Distributed under the GNU LGPLv3. See LICENSE for more info.
import enum
from time import perf_counter, sleep
from itertools import chain
from collections import ChainMap
from gevent import Timeout, event, sleep as gsleep
from gevent import event, sleep as gsleep
from bliss import global_map
from bliss.common.protocols import CounterContainer
......@@ -30,7 +28,7 @@ from bliss.controllers.counter import (
)
from bliss.scanning.acquisition.counter import BaseCounterAcquisitionSlave
from bliss.config.beacon_object import BeaconObject
# from bliss.config.beacon_object import BeaconObject
from bliss.common.logtools import log_info, log_debug, log_debug_data, log_warning
......@@ -157,10 +155,10 @@ class BlissController(CounterContainer):
_COUNTER_TAGS = {}
def __init__(self, name, config):
def __init__(self, config):
self._name = name
self._config = config
self._name = config.get("name")
self._counter_controllers = {}
self._hw_controller = None
......@@ -217,6 +215,13 @@ class BlissController(CounterContainer):
def _load_config(self):
""" Read and apply the YML configuration """
# for k in self.config.keys():
# if k in self._SUB_CLASS:
# for cfg in self.config[k]:
# if cfg.get('name'):
# self._objects[cfg.get('name')] = self._SUB_CLASS[k](self, cfg)
raise NotImplementedError
def _build_counters(self):
......
......@@ -55,36 +55,67 @@ with the mandatory fields:
"""
from gevent import lock
from itertools import chain
from bliss.common.regulation import Input, Output, Loop
from bliss.common.utils import set_custom_members
from bliss.common.logtools import log_info
from bliss.common.protocols import counter_namespace
from bliss.common.utils import autocomplete_property
import time
from bliss.controllers.bliss_controller import BlissController
from bliss.common.counter import SamplingCounter
from bliss.controllers.counter import SamplingCounterController
class Controller:
class Controller(BlissController):
"""
Regulation controller base class
Regulation controller base class
The 'Controller' class should be inherited by controller classes that are linked to an hardware
which has internal PID regulation functionnalities and optionally ramping functionnalities (on setpoint or output value) .
If controller hardware does not have ramping capabilities, the Loop objects associated to the controller will automatically use a SoftRamp.
The 'Controller' class should be inherited by controller classes that are linked to an hardware
which has internal PID regulation functionnalities and optionally ramping functionnalities (on setpoint or output value) .
If controller hardware does not have ramping capabilities, the Loop objects associated to the controller will automatically use a SoftRamp.
"""
class SCC(SamplingCounterController):
def __init__(self, name, bctrl):
super().__init__(name)
self.bctrl = bctrl
def read_all(self, *counters):
values = []
for cnt in counters:
item = self.bctrl.get_object(cnt.name)
if item is not None:
values.append(item.read())
return values
_SUB_CLASS = {"inputs": Input, "outputs": Output, "ctrl_loops": Loop}
def __init__(self, config):
self.__config = config
self.__name = config.get("name")
self._objects = {}
self.__lock = lock.RLock()
self.__initialized_obj = {}
self.__hw_controller_initialized = False
def add_object(self, node_type_name, object_class, cfg):
""" creates an instance of the object and add it to the controller. Called by regulation plugin. """
super().__init__(config)
new_obj = object_class(self, cfg)
def _create_sub_item(self, name, cfg, parent_key):
""" Create/get and return an object which has a config name and which is owned by this controller
This method is called by the Bliss Controller Plugin and is called after the controller __init__().
This method is called only once per item on the first config.get('item_name') call (see plugin).
args:
'name': sub item name
'cfg' : sub item config
'parent_key': the config key under which the sub item was found (ex: 'counters').
return: the sub item object
"""
new_obj = self._SUB_CLASS[parent_key](self, cfg)
# --- store the new object
self._objects[new_obj.name] = new_obj
......@@ -94,6 +125,44 @@ class Controller:
return new_obj
def _load_config(self):
""" Read and apply the YML configuration """
print("=== _load_config")
pass
def _build_counters(self):
""" Build the CounterControllers and associated Counters"""
print("=== _build_counters")
self._counter_controllers["scc"] = self.SCC("scc", self)
self._counter_controllers["scc"].max_sampling_frequency = self.config.get(
"max_sampling_frequency", 1
)
for k in self._SUB_CLASS:
for cfg in self.config.get(k, []):
name = cfg["name"]
mode = cfg.get("mode", "SINGLE")
unit = cfg.get("unit")
self._counter_controllers["scc"].create_counter(
SamplingCounter, name, unit=unit, mode=mode
)
def _build_axes(self):
""" Build the Axes (real and pseudo) """
print("=== _build_axes")
pass
@autocomplete_property
def counters(self):
cnts = [ctrl.counters for ctrl in self._counter_controllers.values()]
return counter_namespace(chain(*cnts))
@autocomplete_property
def axes(self):
# return counter_namespace(self._motor_controller.axes)
pass
def init_obj(self, obj):
""" Initialize objects under the controller. Called by @lazy_init. """
......@@ -103,6 +172,7 @@ class Controller:
if not self.__hw_controller_initialized:
self.initialize_controller()
print("=== initialize_controller")
self.__hw_controller_initialized = True
if self.__initialized_obj.get(obj):
......@@ -116,33 +186,27 @@ class Controller:
self.__initialized_obj[obj.input] = True
obj.input.load_base_config()
self.initialize_input(obj.input)
print("=== initialize_input")
if not self.__initialized_obj.get(obj.output):
self.__initialized_obj[obj.output] = True
obj.output.load_base_config()
self.initialize_output(obj.output)
print("=== initialize_output")
obj.load_base_config()
self.initialize_loop(obj)
print("=== initialize_loop")
else:
self.__initialized_obj[obj] = True
obj.load_base_config()
if isinstance(obj, Input):
self.initialize_input(obj)
print("=== initialize_input")
elif isinstance(obj, Output):
self.initialize_output(obj)
@property
def name(self):
return self.__name
@property
def config(self):
"""
returns the config node
"""
return self.__config
print("=== initialize_output")
def get_object(self, name):
"""
......
plugin: regulation
plugin: bliss_controller #regulation
......@@ -42,6 +42,7 @@
-
class: ExternalInput # <== declare an 'ExternalInput' object
package: bliss.common.regulation
name: diode_input
device: $diode # <== a SamplingCounter object reference (pointing to a counter declared somewhere else in a YML config file)
unit: N/A
......@@ -49,6 +50,7 @@
-
class: ExternalOutput # <== declare an 'ExternalOutput' object
package: bliss.common.regulation
name: robz_output
device: $robz # <== an axis object reference (pointing to an object declared somewhere else in a YML config file)
unit: mm
......@@ -60,6 +62,7 @@
-
class: SoftLoop # <== declare a 'SoftLoop' object
package: bliss.common.regulation
name: soft_regul
input: $custom_input
output: $custom_output
......@@ -77,6 +80,7 @@
-
class: SoftLoop # <== declare a 'SoftLoop' object
package: bliss.common.regulation
name: soft_regul2
input: $bound_input
output: $robz_output
......@@ -96,7 +100,8 @@
-
class: Mockup
module: temperature.mockup
# plugin: bliss_controller
module: regulation.temperature.mockup
host: lid42
inputs:
-
......@@ -138,4 +143,4 @@
ramprate: 1.0 # <== ramprate to reach the setpoint value [input_unit/s]
wait_mode: deadband
tango_server: temp1
\ No newline at end of file
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment