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

2Provides functions to determine the jacobi matrix for linearizing the system 

3around a given state with specified inputs. 

4""" 

5from typing import List 

6 

7import numpy as np 

8from scipy.misc import central_diff_weights 

9 

10from modypy.model import Port, System, SystemState 

11 

12 

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

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

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

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

189def _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 

205def _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))