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

# Coverage for D:\Ralf Gerlich\git\modypy\modypy\linearization.py : 0%

## 79 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 Provides functions to determine the jacobi matrix for linearizing the system

3 around a given state with specified inputs.

4"""

5 from typing import List

6

7 import numpy as np

8 from scipy.misc import central_diff_weights

9

10 from modypy.model import Port, System, SystemState

11

12

13 class LinearizationConfiguration:

14 """

15 Represents the configuration for the determination of the system jacobian

16

17

18 Attributes:

19 system

20 The system for which the jacobian shall be determined

21 time

22 The system time for which the jacobian shall be determined

23 (default: 0)

24 state

25 The state around which the jacobian shall be determined (default: 0)

26 inputs

27 The input values around which the jacobian shall be determined

28 (default: 0)

29 outputs

30 List of :class:`OutputDescriptor` instances for the signals to be

31 considered as outputs (default: representations of all output ports

32 in the system)

33 num_outputs

34 The sum of sizes of all signals to be considered as outputs

35 default_step_size

36 The default step size to use for the numerical differentiation

37 (default: 0.1)

38 interpolation_order

39 The interpolation order to use for numerical differentiation

40 (default: 3)

41

42 """

43

44 def __init__(self,

45 system: System,

46 time=0,

47 state=None,

48 inputs=None):

49 self.system = system

50 self.time = time

51

52 if state is None:

53 self.state = np.zeros(self.system.num_states)

54 else:

55 self.state = state

56

57 if inputs is None:

58 self.inputs = np.zeros(self.system.num_inputs)

59 else:

60 self.inputs = inputs

61

62 self.outputs: List[OutputDescriptor] = list()

63 self.num_outputs = 0

64

65 self.default_step_size = 0.1

66 self.interpolation_order = 3

67

68

69 class OutputDescriptor:

70 """

71 Represents information about an output signal for the determination of the

72 system jacobian

73

74 Attributes:

75 config

76 The :class:`LinearizationConfiguration` instance to which this

77 output descriptor belongs

78 port

79 The :class:`port <modypy.model.Port>` object that is used as output

80 output_index

81 The index of the first line allocated to this signal in the

82 output and feed-through matrices

83 """

84

85 def __init__(self, config: LinearizationConfiguration, port: Port):

86 self.config = config

87 self.port = port

88

89 # Assign an output index

90 self.output_index = self.config.num_outputs

91 self.config.num_outputs += self.port.size

92 self.config.outputs.append(self)

93

94 @property

95 def output_slice(self):

96 """A slice representing the index range of this output"""

97

98 return slice(self.output_index, self.port.size)

99

100

101 def system_jacobian(config: LinearizationConfiguration,

102 single_matrix=False):

103 """Numerically determine the jacobian of the system at the given state and

104 input setting.

105

106 The function uses polygonal interpolation of the given order on the

107 components of the system derivative and output function, choosing

108 interpolation points at a given distance from the given state and input

109 values.

110

111 This can be used in conjunction with `find_steady_state` to determine

112 an LTI approximating the behaviour around the steady state.

113

114 NOTE: This function currently does not honor clocks.

115

116 Args:

117 config

118 The :class:`LinearizationConfiguration` instance describing the

119 linearization to be performed

120 single_matrix

121 Flag indicating whether a single matrix shall be returned. The

122 default is `False`.

123

124 Returns:

125 the jacobian, if `single_matrix` is `True`, and a tuple of

126 system matrix, input matrix, output matrix and feed-through matrix, if

127 `single_matrix` is `False`.

128 Raises:

129 ValueError if the system does not have states or inputs

130 """

131

132 if config.system.num_states + config.system.num_inputs == 0:

133 raise ValueError("Cannot linearize system without states and inputs")

134

135 num_invars = config.system.num_states + config.system.num_inputs

136 num_outvars = config.system.num_states + config.num_outputs

137 half_offset = config.interpolation_order >> 1

138 weights = _get_central_diff_weights(config.interpolation_order)

139

140 jac = np.zeros((num_outvars, num_invars))

141 x_ref0 = np.concatenate((config.state, config.inputs), axis=None)

142

143 for var_ind in range(num_invars):

144 x_step = config.default_step_size * np.eye(N=1,

145 M=num_invars,

146 k=var_ind).ravel()

147 for k in range(config.interpolation_order):

148 x_k = x_ref0 + (k - half_offset) * x_step

149 y_k = _system_function(config, x_k)

150 jac[:, var_ind] += weights[k] * y_k

151 jac[:, var_ind] /= config.default_step_size

152

153 if single_matrix == "struct":

154 system_matrix = jac[:config.system.num_states,

155 :config.system.num_states]

156 input_matrix = jac[:config.system.num_states,

157 config.system.num_states:]

158 output_matrix = jac[config.system.num_states:,

159 :config.system.num_states]

160 feed_through_matrix = jac[config.system.num_states:,

161 config.system.num_states:]

162 return SystemJacobian(config=config,

163 system_matrix=system_matrix,

164 input_matrix=input_matrix,

165 output_matrix=output_matrix,

166 feed_through_matrix=feed_through_matrix)

167 if single_matrix:

168 return jac

169 return jac[:config.system.num_states, :config.system.num_states], \

170 jac[:config.system.num_states, config.system.num_states:], \

171 jac[config.system.num_states:, :config.system.num_states], \

172 jac[config.system.num_states:, config.system.num_states:]

173

174

175 class SystemJacobian:

176 def __init__(self,

177 config: LinearizationConfiguration,

178 system_matrix,

179 input_matrix,

180 output_matrix,

181 feed_through_matrix):

182 self.config = config

183 self.system_matrix = system_matrix

184 self.input_matrix = input_matrix

185 self.output_matrix = output_matrix

186 self.feed_through_matrix = feed_through_matrix

187

188

189 def _get_central_diff_weights(order):

190 """Determine the weights for central differentiation"""

191

192 if order == 3:

193 weights = np.array([-1, 0, 1]) / 2.0

194 elif order == 5:

195 weights = np.array([1, -8, 0, 8, -1]) / 12.0

196 elif order == 7:

197 weights = np.array([-1, 9, -45, 0, 45, -9, 1]) / 60.0

198 elif order == 9:

199 weights = np.array([3, -32, 168, -672, 0, 672, -168, 32, -3]) / 840.0

200 else:

201 weights = central_diff_weights(order, 1)

202 return weights

203

204

205 def _system_function(config: LinearizationConfiguration, x_ref):

206 """

207 Determine the value of the vector of state derivatives and outputs

208 given the vector of states and inputs

209

210 Args:

211 config

212 The :class:`LinearizationConfiguration` instance describing the

213 linearization to be performed

214 x_ref

215 The vector of states and inputs

216

217 Returns:

218 The vector of state derivatives and outputs

219 """

220 state = x_ref[:config.system.num_states]

221 inputs = x_ref[config.system.num_states:]

222

223 system_state = SystemState(time=config.time,

224 system=config.system,

225 state=state,

226 inputs=inputs)

227

228 outputs = np.zeros(config.num_outputs)

229 for output in config.outputs:

230 outputs[output.output_index: output.output_index + output.port.size] = \

231 np.ravel(output.port(system_state))

232

233 return np.concatenate((config.system.state_derivative(system_state),

234 outputs))