Hide keyboard shortcuts

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""" 

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

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

4 

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

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

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

8it. 

9 

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

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

12 

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

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

15a port. 

16""" 

17import functools 

18import numpy as np 

19import operator 

20from typing import Optional, Sequence, Tuple, Union 

21 

22 

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

24 

25 

26class PortNotConnectedError(RuntimeError): 

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

28 to a signal""" 

29 

30 

31class 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 

36class ShapeMismatchError(RuntimeError): 

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

38 connected to each other.""" 

39 

40 

41class 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 

113class 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 

126class 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 

140def 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 

163def 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 

175def 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 

213class 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)