Coverage for D:\Ralf Gerlich\git\modypy\modypy\steady_state.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"""
2Functions and classes for finding the steady state of a system.
4To determine a steady state, set up a :class:`SteadyStateConfiguration` object
5and pass it to :func:`find_steady_state`.
6"""
7from itertools import accumulate
9import numpy as np
10from collections.abc import Mapping
11from functools import partial
12from modypy.model import InputSignal, Port, State, System, SystemState
13from scipy import optimize as opt
14from typing import Union
17class SteadyStateConfiguration:
18 """Represents the configuration for the steady state determination
20 Attributes:
21 system
22 The system for which a steady state analysis shall be carried out
23 time
24 The system time for which the steady state analysis shall be carried
25 out.
26 objective
27 An objective to minimize. This is either `None` (default), a
28 callable or a `Port`. If no objective is specified, any steady state
29 that satisfies the other constraints may be returned.
30 states
31 A read-only dictionary mapping states to :class:`StateConstraint`
32 instances for the respective state. The state constraint can be
33 configured by modifying its properties.
34 ports
35 A read-only dictionary mapping ports to :class:`PortConstraint`
36 instances for the respective port. The port constraint can be
37 configured by modifying its properties.
38 inputs
39 A read-only dictionary mapping ports to :class:`InputConstraint`
40 instances for the respective port. The port constraint can be
41 configured by modifying its properties.
42 initial_condition
43 The initial estimate for the states. The default is the initial
44 condition of the system.
45 initial_input
46 The initial estimate for the inputs. The default is the initial
47 values specified for the inputs in the system.
48 input_bounds
49 An array of shape (n,2), where n is the number of inputs for the
50 system. Each entry ``input_bounds[k]`` is a tuple ``(lb, ub)``, with
51 ``lb`` giving the lower and ``ub`` giving the upper bound for the
52 respective input line. The initial value for all bounds is
53 ``(-np.inf, np.inf)``, representing an unconstrained input line.
54 state_bounds
55 An array of shape (n,2), where n is the number of states for the
56 system. The format is the same as for `input_bounds`.
57 steady_states
58 An array of shape (n,), where n is the number of states for the
59 system. The entry ``steady_states[k]`` is a boolean indicating
60 whether the state ``k`` shall be steady, i.e. whether its derivative
61 shall be zero. By default, all states are set to be steady.
62 solver_options
63 Dictionary with additional keyword options for the solver.
64 """
66 def __init__(self,
67 system: System,
68 time: float = 0,
69 objective: Union[callable, Port] = None):
70 self.system = system
71 self.time = time
73 self.objective = objective
75 # Set up the initial state estimates
76 self.initial_condition = self.system.initial_condition
77 # Set up the initial state bounds
78 self.state_bounds = np.full(shape=(self.system.num_states, 2),
79 fill_value=(-np.inf, np.inf))
80 # Set up the set of steady states
81 self.steady_states = np.full(shape=self.system.num_states,
82 fill_value=True)
84 # Set up the initial input estimates
85 self.initial_input = self.system.initial_input
86 # Set up the initial input bounds
87 self.input_bounds = np.full(shape=(self.system.num_inputs, 2),
88 fill_value=(-np.inf, np.inf))
90 # Set up the dictionary for solver options
91 self.solver_options = dict()
93 # Set up the dictionaries for the specific constraints
94 self.ports = _ConstraintDictionary(PortConstraint, self)
95 self.states = _ConstraintDictionary(StateConstraint, self)
96 self.inputs = _ConstraintDictionary(InputConstraint, self)
99class PortConstraint(opt.NonlinearConstraint):
100 """A ``PortConstraint`` represents constraints on a single port.
102 Properties:
103 port
104 The port to be constrained
105 lower_bounds
106 A numerical value or an array representing the lower limit for
107 the port. The default is negative infinity, i.e., no lower limit.
108 upper_bounds
109 A numerical value or an array representing the upper limit for
110 the port. The default is positive infinity, i.e., no upper limit.
111 """
113 def __init__(self,
114 config: SteadyStateConfiguration,
115 port: Port,
116 lower_limit=-np.inf,
117 upper_limit=np.inf):
118 self.config = config
119 self.port = port
121 super().__init__(fun=self._evaluate,
122 lb=np.ravel(np.full(self.port.shape, lower_limit)),
123 ub=np.ravel(np.full(self.port.shape, upper_limit)))
125 @property
126 def lower_bounds(self):
127 """Return the lower bounds currently set for this port"""
128 return self.lb.reshape(self.port.shape)
130 @lower_bounds.setter
131 def lower_bounds(self, value):
132 self.lb[:] = np.ravel(value)
134 @property
135 def upper_bounds(self):
136 return self.ub.reshape(self.port.shape)
138 @upper_bounds.setter
139 def upper_bounds(self, value):
140 self.ub[:] = np.ravel(value)
142 def _evaluate(self, x):
143 """Calculate the value vector of the port"""
145 state = x[:self.config.system.num_states]
146 inputs = x[self.config.system.num_states:]
147 system_state = SystemState(time=self.config.time,
148 system=self.config.system,
149 state=state,
150 inputs=inputs)
151 return np.ravel(self.port(system_state))
154class StateConstraint:
155 """A ``StateConstraint`` object represents the constraints on a state.
157 Properties:
158 lower_bounds:
159 A matrix with the shape of the state, representing the lower bound
160 for each component of the state (default: -inf)
161 upper_bounds:
162 A matrix with the shape of the state, representing the upper bound
163 for each component of the state (default: +inf)
164 steady_state:
165 A matrix of booleans with the shape of the state, indicating whether
166 the respective component of the state shall be a steady-state, i.e.,
167 whether its derivative shall be constrained to zero (default: True)
168 initial_condition:
169 A matrix with the shape of the state, representing the initial
170 steady state guess for each component of the state
171 (default: initial condition of the state)"""
173 def __init__(self,
174 config: SteadyStateConfiguration,
175 state: State):
176 self.config = config
177 self.state = state
179 flat_lower_bounds = self.config.state_bounds[self.state.state_slice, 0]
180 flat_upper_bounds = self.config.state_bounds[self.state.state_slice, 1]
181 self._lower_bounds = flat_lower_bounds.reshape(self.state.shape)
182 self._upper_bounds = flat_upper_bounds.reshape(self.state.shape)
184 flat_steady_states = self.config.steady_states[self.state.state_slice]
185 self._steady_states = flat_steady_states.reshape(self.state.shape)
187 flat_initial_condition = \
188 self.config.initial_condition[self.state.state_slice]
189 self._initial_condition = \
190 flat_initial_condition.reshape(self.state.shape)
192 @property
193 def lower_bounds(self):
194 return self._lower_bounds
196 @lower_bounds.setter
197 def lower_bounds(self, value):
198 self.config.state_bounds[self.state.state_slice, 0] = np.ravel(value)
200 @property
201 def upper_bounds(self):
202 return self._upper_bounds
204 @upper_bounds.setter
205 def upper_bounds(self, value):
206 self.config.state_bounds[self.state.state_slice, 1] = np.ravel(value)
208 @property
209 def steady_state(self):
210 return self._steady_states
212 @steady_state.setter
213 def steady_state(self, value):
214 self.config.steady_states[self.state.state_slice] = np.ravel(value)
216 @property
217 def initial_condition(self):
218 return self._initial_condition
220 @initial_condition.setter
221 def initial_condition(self, value):
222 self.config.initial_condition[self.state.state_slice] = np.ravel(value)
225class InputConstraint:
226 """A ``InputConstraint`` object represents the constraints on an input port.
228 Properties:
229 lower_bounds:
230 A matrix with the shape of the input, representing the lower bound
231 for each component of the port (default: -inf)
232 upper_bounds:
233 A matrix with the shape of the input, representing the upper bound
234 for each component of the port (default: +inf)
235 initial_guess:
236 A matrix with the shape of the input, representing the initial
237 steady state guess for each component of the input
238 (default: value of the input at the time of creation of the
239 :class:`SteadyStateConfiguration` object)"""
241 def __init__(self,
242 config: SteadyStateConfiguration,
243 input_signal: InputSignal):
244 self.config = config
245 self.input_signal = input_signal
247 flat_lower_bounds = \
248 self.config.input_bounds[self.input_signal.input_slice, 0]
249 flat_upper_bounds = \
250 self.config.input_bounds[self.input_signal.input_slice, 1]
251 self._lower_bounds = flat_lower_bounds.reshape(self.input_signal.shape)
252 self._upper_bounds = flat_upper_bounds.reshape(self.input_signal.shape)
254 flat_initial_guess = \
255 self.config.initial_input[self.input_signal.input_slice]
256 self._initial_guess = \
257 flat_initial_guess.reshape(self.input_signal.shape)
259 @property
260 def lower_bounds(self):
261 return self._lower_bounds
263 @lower_bounds.setter
264 def lower_bounds(self, value):
265 self.config.input_bounds[self.input_signal.input_slice, 0] = \
266 np.ravel(value)
268 @property
269 def upper_bounds(self):
270 return self._upper_bounds
272 @upper_bounds.setter
273 def upper_bounds(self, value):
274 self.config.input_bounds[self.input_signal.input_slice, 1] = \
275 np.ravel(value)
277 @property
278 def initial_guess(self):
279 return self._initial_guess
281 @initial_guess.setter
282 def initial_guess(self, value):
283 self.config.initial_input[self.input_signal.input_slice] = \
284 np.ravel(value)
287def find_steady_state(config: SteadyStateConfiguration):
288 """Run the steady-state determination
290 Args:
291 config: The configuration for the steady-state analysis
293 Returns:
294 An :class:`OptimizeResult <scipy.optimize.OptimizeResult>` object with
295 additional fields.
297 state: ndarray
298 The state part of the solution
299 inputs: ndarray
300 The input part of the solution
301 system_state: modypy.model.evaluation.SystemState
302 An :class:`SystemState <modypy.model.evaluation.SystemState>`
303 object, configured to evaluate the system at the determined
304 steady-state
305 """
307 # Set up the initial estimate
308 x0 = np.concatenate((config.initial_condition, config.initial_input))
309 # Set up the bounds
310 bounds = np.concatenate((config.state_bounds, config.input_bounds))
312 # Set up the constraints
313 constraints = list()
315 constraints += config.ports.values()
317 if config.objective is not None:
318 # We have an actual objective function, so we can use the steady-state
319 # constraint as actual constraint.
320 if any(config.steady_states):
321 # Set up the state derivative constraint
322 steady_state_constraint = _StateDerivativeConstraint(config)
323 constraints.append(steady_state_constraint)
325 # Translate the objective function
326 if callable(config.objective):
327 objective_function = partial(_general_objective_function, config)
328 else:
329 raise ValueError('The objective function must be either a Port or '
330 'a callable')
331 elif any(config.steady_states):
332 # No objective function was specified, but we can use the steady-state
333 # constraint function. The value of this function is intended to be
334 # zero, so the minimum value of its square is zero.
335 steady_state_constraint = _StateDerivativeConstraint(config)
336 constraints.append(steady_state_constraint)
337 objective_function = steady_state_constraint.evaluate_squared
338 else:
339 # We have neither an objective function to minimize nor do we have any
340 # state that is intended to be steady. We cannot use the signal
341 # constraints to minimize, as these may specify ranges instead of a
342 # target value. We cannot do anything about this.
343 raise ValueError('Either an objective function or at least one steady '
344 'state is required')
346 result = opt.minimize(fun=objective_function,
347 x0=x0,
348 method='trust-constr',
349 bounds=bounds,
350 constraints=constraints,
351 options=config.solver_options)
353 result.config = config
354 result.state = result.x[:config.system.num_states]
355 result.inputs = result.x[config.system.num_states:]
356 result.system_state = SystemState(time=config.time,
357 system=config.system,
358 state=result.state,
359 inputs=result.inputs)
361 return result
364class _StateDerivativeConstraint(opt.NonlinearConstraint):
365 """Represents the steady-state constraints on the state derivatives"""
367 def __init__(self, config: SteadyStateConfiguration):
368 self.config = config
369 # Steady-state constraints can be defined on the level of individual
370 # state components. To optimize evaluation, we only evaluate derivatives
371 # of those states that have at least one of their components
372 # constrained.
373 self.constrained_states = [
374 state for state in self.config.system.states
375 if any(self.config.steady_states[state.state_slice])]
377 # We will build a vector of the constrained derivatives, and for that
378 # we assign offsets for the states in that vector
379 self.state_offsets = \
380 [0] + list(accumulate(state.size
381 for state in self.constrained_states))
383 num_states = self.state_offsets[-1]
385 # Now we set up the bounds for each of these
386 ub = np.full(num_states, np.inf)
387 ub[self.config.steady_states] = 0
388 lb = -ub
390 opt.NonlinearConstraint.__init__(self,
391 fun=self.evaluate,
392 lb=lb,
393 ub=ub)
395 def evaluate(self, x):
396 """Determine the value of the derivatives of the vector of constrained
397 states"""
399 state = x[:self.config.system.num_states]
400 inputs = x[self.config.system.num_states:]
401 system_state = SystemState(time=self.config.time,
402 system=self.config.system,
403 state=state,
404 inputs=inputs)
405 derivative_vector = self.config.system.state_derivative(system_state)
406 return derivative_vector
408 def evaluate_squared(self, x):
409 """Determine the 2-norm of the derivatives vector of constrained
410 states"""
412 return np.sum(np.square(self.evaluate(x)))
415def _general_objective_function(config: SteadyStateConfiguration, x):
416 """Implementation of the general objective function
418 This calls the objective function with a `DataProvider` as single parameter.
420 Args:
421 config: The configuration for the steady-state determination
422 x: The vector of the current values of states and input
424 Returns:
425 The current value of the objective function
426 """
428 state = x[:config.system.num_states]
429 inputs = x[config.system.num_states:]
430 system_state = SystemState(time=config.time,
431 system=config.system,
432 state=state,
433 inputs=inputs)
434 return config.objective(system_state)
437class _ConstraintDictionary(Mapping):
438 """Dictionary to hold constraints
440 When a key is requested for which there is no entry yet, the given
441 constructor is called with the args, the key and the keyword args to
442 create a new entry."""
444 def __init__(self, constructor, *args, **kwargs):
445 self.data = dict()
446 self.constructor = constructor
447 self.args = args
448 self.kwargs = kwargs
450 def __getitem__(self, key):
451 try:
452 return self.data[key]
453 except KeyError:
454 new_item = self.constructor(*self.args, key, **self.kwargs)
455 self.data[key] = new_item
456 return new_item
458 def __len__(self):
459 return len(self.data)
461 def __iter__(self):
462 return iter(self.data)