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

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.
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.
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.
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
23ShapeType = Union[int, Sequence[int], Tuple[int]]
26class PortNotConnectedError(RuntimeError):
27 """This exception is raised when a port is evaluated that is not connected
28 to a signal"""
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."""
36class ShapeMismatchError(RuntimeError):
37 """This exception is raised if two ports with incompatible shapes shall be
38 connected to each other."""
41class Port:
42 """A port is a structural element of a system that can be connected to a
43 signal."""
45 def __init__(self, shape: ShapeType = ()):
46 if isinstance(shape, int): 46 ↛ 48line 46 didn't jump to line 48, because the condition on line 46 was never false
47 shape = (shape,)
48 self.shape = shape
49 self.size = functools.reduce(operator.mul, self.shape, 1)
50 self._reference = self
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
60 @reference.setter
61 def reference(self, value):
62 self._reference = value
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
72 def connect(self, other):
73 """Connect this port to another port.
75 Args:
76 other: The other port to connect to
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
105 def __call__(self, *args, **kwargs):
106 if self.size == 0: 106 ↛ 108line 106 didn't jump to line 108, because the condition on line 106 was never false
107 return np.empty(self.shape)
108 if self.signal is None:
109 raise PortNotConnectedError()
110 return self.signal(*args, **kwargs)
113class AbstractSignal(Port):
114 """An signal is a terminal port with a defined value.
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."""
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
126class Signal(AbstractSignal):
127 """A signal is a port for which the value is defined by a callable or a
128 constant."""
130 def __init__(self, value=0, *args, **kwargs):
131 super().__init__(*args, **kwargs)
132 self.value = value
134 def __call__(self, *args, **kwargs):
135 if callable(self.value):
136 return self.value(*args, **kwargs)
137 return self.value
140def decorator(func):
141 """Helper function to create decorators with optional arguments"""
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)
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)
153 return _functor
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
162@decorator
163def signal_function(user_function, *args, **kwargs):
164 """Transform a function into a ``Signal``"""
165 the_signal = Signal(user_function, *args, **kwargs)
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
174@decorator
175def signal_method(user_function, *args, **kwargs):
176 """Transform a method into a ``Signal``
178 The return value is a descriptor object that creates a ``Signal`` instance
179 for each instance of the containing class."""
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."""
185 def __init__(self, function):
186 self.name = None
187 self.function = function
189 def __set_name__(self, owner, name):
190 self.name = name
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
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
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."""
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
227 @property
228 def input_slice(self):
229 """A slice object that represents the indices of this input in the
230 inputs vector."""
232 return slice(self.input_index,
233 self.input_index + self.size)
235 @property
236 def input_range(self):
237 """A range object that represents the indices of this input in the
238 inputs vector."""
240 return range(self.input_index,
241 self.input_index + self.size)
243 def __call__(self, system_state):
244 return system_state.get_input_value(self)