Coverage for D:\Ralf Gerlich\git\modypy\modypy\model\ports.py: 29%

# Coverage for D:\Ralf Gerlich\git\modypy\modypy\model\ports.py : 29%

## 115 statements

Hot-keys on this page

r m x p toggle line displays

j k next/prev highlighted chunk

0 (zero) top of page

1 (one) first highlighted chunk

1"""

2 Values in a model are transported by signals. In MoDyPy signals are always

3 real-valued, and may be multi-dimensional.

4

5 The value of a signal is defined by a value function, which may depend on the

6 value of any system state or signal, which are made accessible by the

7:class:`DataProvider <modypy.model.evaluation.DataProvider>` object passed to

8 it.

9

10 Signals differ from states in that they do not have their own memory - although

11 they may be based on the values of states, which represent memory.

12

13 Ports serve as symbolic placeholders for signals, and may be connected to each

14 other and to signals using the ``connect`` method. In fact, each signal is also

15 a port.

16"""

17 import functools

18 import numpy as np

19 import operator

20 from typing import Optional, Sequence, Tuple, Union

21

22

23 ShapeType = Union[int, Sequence[int], Tuple[int]]

24

25

26 class PortNotConnectedError(RuntimeError):

27 """This exception is raised when a port is evaluated that is not connected

28 to a signal"""

29

30

31 class MultipleSignalsError(RuntimeError):

32 """This exception is raised if two ports shall be connected to each other

33 that are already connected to different signals."""

34

35

36 class ShapeMismatchError(RuntimeError):

37 """This exception is raised if two ports with incompatible shapes shall be

38 connected to each other."""

39

40

41 class Port:

42 """A port is a structural element of a system that can be connected to a

43 signal."""

44

45 def __init__(self, shape: ShapeType = ()):

46 if isinstance(shape, int):

47 shape = (shape,)

48 self.shape = shape

49 self.size = functools.reduce(operator.mul, self.shape, 1)

50 self._reference = self

51

52 @property

53 def reference(self):

54 """The port referenced by this port"""

55 if self._reference is not self:

56 # Try to further shorten the reference path

57 self._reference = self._reference.reference

58 return self._reference

59

60 @reference.setter

61 def reference(self, value):

62 self._reference = value

63

64 @property

65 def signal(self) -> Optional['Port']:

66 """The signal referenced by this port or ``None`` if this port is not

67 connected to any signal."""

68 if self._reference is not self:

69 return self.reference.signal

70 return None

71

72 def connect(self, other):

73 """Connect this port to another port.

74

75 Args:

76 other: The other port to connect to

77

78 Raises:

79 ShapeMismatchError: if the shapes of the ports do not match

80 MultipleSignalsError: if both ports are already connected to

81 different signals

82 """

83 if self.shape != other.shape:

84 # It is an error if the shapes of the ports do not match.

85 raise ShapeMismatchError('Shape (%s) of left port does not match '

86 'shape (%s) of right port' % (

87 self.shape,

88 other.shape

89 ))

90 if self.signal is not None and other.signal is not None:

91 # Both ports are already connected to a signal.

92 # It is an error if they are not connected to the same signal.

93 if self.signal != other.signal:

94 raise MultipleSignalsError()

95 else:

96 if self.signal is None:

97 # We are not yet connected to a signal, so we take the reference

98 # from the other port.

99 self.reference.reference = other.reference

100 else:

101 # The other port is not yet connected to a signal, so we update

102 # the reference of the other port.

103 other.reference.reference = self.reference

104

105 def __call__(self, *args, **kwargs):

106 if self.size == 0:

107 return np.empty(self.shape)

108 if self.signal is None:

109 raise PortNotConnectedError()

110 return self.signal(*args, **kwargs)

111

112

113 class AbstractSignal(Port):

114 """An signal is a terminal port with a defined value.

115

116 It is connected to itself, i.e., it can only be connected to other,

117 unconnected ports or to ports that are already connected to itself."""

118

119 @property

120 def signal(self):

121 """The signal this signal is connected to, i.e., itself."""

122 # A signal is always connected to itself

123 return self

124

125

126 class Signal(AbstractSignal):

127 """A signal is a port for which the value is defined by a callable or a

128 constant."""

129

130 def __init__(self, value=0, *args, **kwargs):

131 super().__init__(*args, **kwargs)

132 self.value = value

133

134 def __call__(self, *args, **kwargs):

135 if callable(self.value):

136 return self.value(*args, **kwargs)

137 return self.value

138

139

140 def decorator(func):

141 """Helper function to create decorators with optional arguments"""

142

143 def _wrapper(*args, **kwargs):

144 if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):

145 # We only have the function as parameter, so directly call the

146 # decorator function

147 return func(*args, **kwargs)

148

149 # We have parameters, so we define a functor to use as decorator

150 def _functor(user_func):

151 return func(user_func, *args, **kwargs)

152

153 return _functor

154

155 # Be a well-behaved decorator by copying name, documentation and attributes

156 _wrapper.__name__ = func.__name__

157 _wrapper.__doc__ = func.__doc__

158 _wrapper.__dict__.update(func.__dict__)

159 return _wrapper

160

161

162@decorator

163 def signal_function(user_function, *args, **kwargs):

164 """Transform a function into a ``Signal``"""

165 the_signal = Signal(user_function, *args, **kwargs)

166

167 # Be a well-behaved decorator by copying name, documentation and attributes

168 the_signal.__doc__ = user_function.__doc__

169 the_signal.__name__ = user_function.__name__

170 the_signal.__dict__.update(user_function.__dict__)

171 return the_signal

172

173

174@decorator

175 def signal_method(user_function, *args, **kwargs):

176 """Transform a method into a ``Signal``

177

178 The return value is a descriptor object that creates a ``Signal`` instance

179 for each instance of the containing class."""

180

181 class _SignalDescriptor:

182 """Descriptor that will return itself when accessed on a class, but a

183 unique Signal instance when accessed on a class instance."""

184

185 def __init__(self, function):

186 self.name = None

187 self.function = function

188

189 def __set_name__(self, owner, name):

190 self.name = name

191

192 def __get__(self, instance, owner):

193 if instance is None:

194 return self

195 signal_name = '__signal_%s' % self.name

196 the_signal = getattr(instance, signal_name, None)

197 if the_signal is None:

198 the_signal = Signal(self.function.__get__(instance, owner),

199 *args, **kwargs)

200 the_signal.__name__ = self.function.__name__

201 the_signal.__doc__ = self.function.__doc__

202 the_signal.__dict__.update(self.function.__dict__)

203 setattr(instance, signal_name, the_signal)

204 return the_signal

205

206 descriptor = _SignalDescriptor(user_function)

207 descriptor.__name__ = user_function.__name__

208 descriptor.__doc__ = user_function.__doc__

209 descriptor.__dict__.update(user_function.__dict__)

210 return descriptor

211

212

213 class InputSignal(AbstractSignal):

214 """An ``InputSignal`` is a special kind of signal that is considered an

215 input into the system. In steady-state identification and linearization,

216 input signals play a special role."""

217

218 def __init__(self, owner, shape: ShapeType = (), value=None):

219 super().__init__(shape)

220 self.owner = owner

221 self.input_index = self.owner.system.allocate_input_lines(self.size)

222 self.owner.system.inputs.append(self)

223 if value is None:

224 value = np.zeros(shape)

225 self.value = value

226

227 @property

228 def input_slice(self):

229 """A slice object that represents the indices of this input in the

230 inputs vector."""

231

232 return slice(self.input_index,

233 self.input_index + self.size)

234

235 @property

236 def input_range(self):

237 """A range object that represents the indices of this input in the

238 inputs vector."""

239

240 return range(self.input_index,

241 self.input_index + self.size)

242

243 def __call__(self, system_state):

244 return system_state.get_input_value(self)