closed_loop.py 3.33 KB
Newer Older
Lucas Felix's avatar
Lucas Felix committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# -*- coding: utf-8 -*-
#
# This file is part of the bliss project
#
# Copyright (c) 2015-2022 Beamline Control Unit, ESRF
# Distributed under the GNU LGPLv3. See LICENSE for more info.

import enum
from functools import partial

from bliss.config.beacon_object import BeaconObject, EnumProperty


class ClosedLoopState(enum.Enum):
    UNKNOWN = enum.auto()
    ON = enum.auto()
    OFF = enum.auto()


def fget_gen(key):
    def f(key, self):
        return self.axis.controller.get_closed_loop_param(self.axis, key)

    fget = partial(f, key)
    fget.__name__ = key
    return fget


def fset_gen(key):
    def f(key, self, value):
        if self._setters_on:
            return self.axis.controller.set_closed_loop_param(self.axis, key, value)

    fset = partial(f, key)
    fset.__name__ = key
    return fset


class ClosedLoop(BeaconObject):
    """
    config example:
    - name: m1
      steps_per_unit: 1000
      velocity: 50
      acceleration: 1
      encoder: $m1enc
      closed_loop:
          state: on
          kp: 1
          ki: 2
          kd: 3
          settling_window: 0.1
          settling_time: 3
    """

    def __new__(cls, axis):
        """Make a class copy per instance to allow closed loop objects to own different properties"""
        cls = type(cls.__name__, (cls,), {})
        return object.__new__(cls)

    def __init__(self, axis):
        self._axis = axis
        name = f"{axis.name}:closed_loop"
        config = axis.config.config_dict
        if isinstance(config, dict):
            super().__init__(config.get("closed_loop"), name=name)
        else:
            super().__init__(config, name=name, path=["closed_loop"])

        setattr(
            self.__class__,
            "_state",
            EnumProperty("state", ClosedLoopState, must_be_in_config=True),
        )

        self._setters_on = False
        self._init_properties()

    def _init_properties(self):
        """Instantiate properties depending on the controller requirements"""
        reqs = self.axis.controller.get_closed_loop_requirements()
        for key in reqs:
            if hasattr(self, key):
                raise Exception(
                    f"Cannot create closed loop property '{key}', name already exists"
                )
            setattr(
                self.__class__,
                key,
                BeaconObject.property(
                    fget=fget_gen(key), fset=fset_gen(key), must_be_in_config=True
                ),
            )

    def __info__(self):
        info_str = "CLOSED LOOP:\n"
        info_str += f"     state: {self.state.name}\n"
        for key in self.axis.controller.get_closed_loop_requirements():
            info_str += f"     {key}: {getattr(self, key)}\n"
        return info_str

    @property
    def axis(self):
        return self._axis

    @property
    def state(self):
        return self._state

    def _activate(self, onoff):
        new_state = self.axis.controller.activate_closed_loop(self.axis, onoff)
        if not isinstance(new_state, ClosedLoopState):
            raise ValueError(
                f"Controller expected to return ClosedLoopState, got {new_state}"
            )
        else:
            self._state = new_state

    def on(self):
        self._activate(True)

    def off(self):
        self._activate(False)

    def _activate_setters(self):
        self._setters_on = True