Coverage for D:\Ralf Gerlich\git\modypy\modypy\blocks\linear.py : 0%

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"""Blocks for linear, time-invariant systems"""
2import numpy as np
3from functools import partial
4from modypy.model import Block, Port, Signal, SignalState, State
7class InvalidLTIException(RuntimeError):
8 """An exception which is raised when the specification of an LTI is invalid.
9 """
12class LTISystem(Block):
13 """Implementation of a linear, time-invariant block of the following format:
15 dx/dt = system_matrix * x + input_matrix * u
16 y = output_matrix * x + feed_through_matrix * u
18 The matrices ``system_matrix``, ``input_matrix``, ``output_matrix`` and
19 ``feed_through_matrix`` define the state and output behaviour of the block.
20 """
22 def __init__(self,
23 parent,
24 system_matrix,
25 input_matrix,
26 output_matrix,
27 feed_through_matrix,
28 initial_condition=None):
29 Block.__init__(self, parent)
31 # Determine the number of states and the shape of the state
32 if np.isscalar(system_matrix):
33 self.state_shape = ()
34 num_states = 1
35 else:
36 system_matrix = np.asarray(system_matrix)
37 if (system_matrix.ndim == 2 and
38 system_matrix.shape[0] > 0 and
39 system_matrix.shape[0] == system_matrix.shape[1]):
40 self.state_shape = system_matrix.shape[0]
41 num_states = self.state_shape
42 else:
43 raise InvalidLTIException('The system matrix must be a scalar '
44 'or a non-empty square matrix')
46 # Determine the number of inputs and the shape of the input signal
47 if np.isscalar(input_matrix):
48 if num_states > 1:
49 raise InvalidLTIException('There is more than one state, but '
50 'the input matrix is neither empty, '
51 'nor a vector or a matrix')
52 num_inputs = 1
53 self.input_shape = ()
54 else:
55 input_matrix = np.asarray(input_matrix)
56 if input_matrix.ndim == 1:
57 # The input matrix is a column vector => one input
58 if num_states != input_matrix.shape[0]:
59 raise InvalidLTIException('The height of the input matrix '
60 'must match the number of states')
61 num_inputs = 1
62 elif input_matrix.ndim == 2:
63 # The input matrix is a matrix
64 if num_states != input_matrix.shape[0]:
65 raise InvalidLTIException('The height of the input matrix '
66 'does not match the number of '
67 'states')
68 num_inputs = input_matrix.shape[1]
69 else:
70 raise InvalidLTIException('The input matrix must be empty,'
71 'a scalar, a vector or a matrix')
72 self.input_shape = num_inputs
74 # Determine the number of outputs and the shape of the output array
75 if np.isscalar(output_matrix):
76 if num_states > 1:
77 raise InvalidLTIException('There is more than one state, but '
78 'the output matrix is neither an '
79 'empty, a vector nor a matrix')
80 num_outputs = 1
81 self.output_shape = ()
82 else:
83 output_matrix = np.asarray(output_matrix)
84 if output_matrix.ndim == 1:
85 # The output matrix is a row vector => one output
86 if num_states != output_matrix.shape[0]:
87 raise InvalidLTIException('The width of the output matrix '
88 'does not match the number of '
89 'states')
90 num_outputs = 1
91 elif output_matrix.ndim == 2:
92 # The output matrix is a matrix
93 if num_states != output_matrix.shape[1]:
94 raise InvalidLTIException('The width of the output matrix '
95 'does not match the number of '
96 'states')
97 num_outputs = output_matrix.shape[0]
98 else:
99 raise InvalidLTIException('The output matrix must be empty, a'
100 'scalar, a vector or a matrix')
101 self.output_shape = num_outputs
103 if np.isscalar(feed_through_matrix):
104 if not (num_inputs == 1 and num_outputs == 1):
105 raise InvalidLTIException('A scalar feed-through matrix is '
106 'only allowed for systems with '
107 'exactly one input and one output')
108 else:
109 feed_through_matrix = np.asarray(feed_through_matrix)
110 if feed_through_matrix.ndim == 1:
111 # A vector feed_through_matrix is interpreted as row vector,
112 # so there must be exactly one output.
113 if num_outputs == 0:
114 raise InvalidLTIException('The feed-through matrix for a '
115 'system without outputs must be'
116 'empty')
117 elif num_outputs > 1:
118 raise InvalidLTIException('The feed-through matrix for a '
119 'system with more than one '
120 'output must be a matrix')
121 if feed_through_matrix.shape[0] != num_inputs:
122 raise InvalidLTIException('The width of the feed-through '
123 'matrix must match the number of '
124 'inputs')
125 elif feed_through_matrix.ndim == 2:
126 if feed_through_matrix.shape[0] != num_outputs:
127 raise InvalidLTIException('The height of the feed-through '
128 'matrix must match the number of '
129 'outputs')
130 if feed_through_matrix.shape[1] != num_inputs:
131 raise InvalidLTIException('The width of the feed-through '
132 'matrix must match the number of '
133 'inputs')
134 else:
135 raise InvalidLTIException('The feed-through matrix must be '
136 'empty, a scalar, a vector or a '
137 'matrix')
139 self.system_matrix = system_matrix
140 self.input_matrix = input_matrix
141 self.output_matrix = output_matrix
142 self.feed_through_matrix = feed_through_matrix
144 self.input = Port(shape=self.input_shape)
145 self.state = State(self,
146 shape=self.state_shape,
147 derivative_function=self.state_derivative,
148 initial_condition=initial_condition)
149 self.output = Signal(shape=self.output_shape,
150 value=self.output_function)
152 def state_derivative(self, data):
153 """Calculates the state derivative for the system"""
154 if self.state.shape == ():
155 derivative = self.system_matrix * self.state(data)
156 else:
157 derivative = np.matmul(self.system_matrix, self.state(data))
158 if self.input.shape == ():
159 derivative += self.input_matrix * self.input(data)
160 elif self.input.size > 0:
161 derivative += np.matmul(self.input_matrix, self.input(data))
162 return derivative
164 def output_function(self, data):
165 """Calculates the output for the system"""
166 if self.state.shape == ():
167 output = self.output_matrix * self.state(data)
168 else:
169 output = np.matmul(self.output_matrix, self.state(data))
170 if self.input.shape == ():
171 output += self.feed_through_matrix * self.input(data)
172 elif self.input.size > 0:
173 output += np.matmul(self.feed_through_matrix, self.input(data))
174 return output
177class Gain(Block):
178 """A simple linear gain block.
180 Provides the input scaled by the constant gain as output.
182 This class is deprecated. Use ``gain`` instead.
183 """
185 def __init__(self, parent, k):
186 Block.__init__(self, parent)
187 self.k = np.atleast_2d(k)
189 self.input = Port(shape=self.k.shape[0])
190 self.output = Signal(shape=self.k.shape[1],
191 value=self.output_function)
193 def output_function(self, data):
194 """Calculates the output for the system
196 Args:
197 data: The current time, states and signals for the system.
199 Returns: The input multiplied by the gain
200 """
201 return self.k @ self.input(data)
204def _gain_function(gain_matrix, input_signal, data):
205 """
206 Calculate the product of the given gain matrix and the value of the signal.
208 Args:
209 gain_matrix: The gain (matrix) to apply
210 input_signal: The input signal
211 data: The data provider
213 Returns:
214 The product of the gain matrix and the value of the signal
215 """
217 return np.matmul(gain_matrix, input_signal(data))
220def gain(gain_matrix, input_signal):
221 """
222 Create a signal that represents the product of the given gain matrix and the
223 value of the given input signal.
225 Args:
226 gain_matrix: The gain matrix
227 input_signal: The input signal to consider
229 Returns:
230 A signal that represents the product of the gain matrix and the value of
231 the input signal.
232 """
234 # Determine the shape of the output signal
235 output_shape = (gain_matrix @ np.zeros(input_signal.shape)).shape
236 return Signal(shape=output_shape,
237 value=partial(_gain_function,
238 gain_matrix,
239 input_signal))
242class Sum(Block):
243 """A linear weighted sum block.
245 This block may have a number of inputs which are interpreted as vectors of
246 common dimension. The output of the block is calculated as the weighted
247 sum of the inputs.
249 The ``channel_weights`` give the factors by which the individual channels
250 are weighted in the sum.
252 This class is deprecated. Use ``sum_signal`` instead.
253 """
255 def __init__(self,
256 parent,
257 channel_weights,
258 output_size=1):
259 Block.__init__(self, parent)
261 self.channel_weights = np.asarray(channel_weights)
262 self.output_size = output_size
264 self.inputs = [Port(shape=self.output_size)
265 for _ in range(self.channel_weights.shape[0])]
266 self.output = Signal(shape=self.output_size,
267 value=self.output_function)
269 def output_function(self, data):
270 """Calculates the output for the system
272 Args:
273 data: The time, states and signals of the system
275 Returns:
276 The sum of the input signals
277 """
278 inputs = np.empty((len(self.inputs), self.output_size))
279 for port_idx in range(len(self.inputs)):
280 inputs[port_idx] = self.inputs[port_idx](data)
281 return self.channel_weights @ inputs
284def _sum_function(signals, gains, data):
285 """
286 Calculate the sum of the values of the given signals multiplied by the
287 given gains.
289 Args:
290 signals: A tuple of signals
291 gains: A tuple of gains
292 data: The data provider
294 Returns:
295 The sum of the values of the given signals multiplied by the given
296 gains
297 """
299 signal_sum = 0
300 for signal, gain_value in zip(signals, gains):
301 signal_sum = signal_sum + np.dot(gain_value, signal(data))
302 return signal_sum
305def sum_signal(input_signals, gains=None):
306 """
307 Create a signal that represents the sum of the input signals multiplied by
308 the given gains.
310 The signals must have the same shape and there must be exactly as many
311 entries in the ``gains`` tuple as there are input signals.
313 Args:
314 input_signals: A tuple of input signals
315 gains: A tuple of gains for the input signals. Optional: Default value
316 is all ones.
318 Returns:
319 A signal that represents the sum of the input signals
320 """
322 if gains is None:
323 gains = np.ones(len(input_signals))
325 shape = input_signals[0].shape
326 if any(signal.shape != shape for signal in input_signals):
327 raise ValueError('The shapes of the input signals do not match')
328 if len(input_signals) != len(gains):
329 raise ValueError('There must be as many gains as there are '
330 'input signals')
332 return Signal(shape=shape,
333 value=partial(_sum_function,
334 input_signals,
335 gains))
338def integrator(owner, input_signal, initial_condition=None):
339 """
340 Create a state-signal that provides the integrated value of the input
341 callable.
343 The resulting signal will have the same shape as the input callable.
345 Args:
346 owner: The owner of the integrator
347 input_signal: A callable accepting an object implementing the system
348 object access protocol and providing the value of the derivative
349 initial_condition: The initial condition of the integrator
350 (default: ``None``)
352 Returns:
353 A state-signal that provides the integrated value of the input signal
354 """
356 return SignalState(owner,
357 shape=input_signal.shape,
358 derivative_function=input_signal,
359 initial_condition=initial_condition)