-
Notifications
You must be signed in to change notification settings - Fork 240
/
eq_constraint_comp.py
320 lines (270 loc) · 14.6 KB
/
eq_constraint_comp.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
"""Define the EQConstraintComp class."""
from __future__ import print_function, division, absolute_import
from numbers import Number
from six import iteritems
import numpy as np
from openmdao.core.explicitcomponent import ExplicitComponent
class EQConstraintComp(ExplicitComponent):
"""
A component that computes the difference between two inputs to test for equality.
Attributes
----------
_output_vars : dict
Cache the data provided during `add_eq_output`
so everything can be saved until setup is called.
"""
def __init__(self, name=None, eq_units=None, lhs_name=None, rhs_name=None, rhs_val=0.0,
use_mult=False, mult_name=None, mult_val=1.0, normalize=True, add_constraint=False,
ref=None, ref0=None, adder=None, scaler=None, **kwargs):
r"""
Initialize an EQConstraintComp, optionally add an output constraint to the model.
The EQConstraintComp allows for the creation of one or more output variables and
computes the values for those variables based on the following equation:
.. math::
name_{output} = \frac{name_{mult} \times name_{lhs} - name_{rhs} }{f_{norm}(name_{rhs})}
Where :math:`name_{lhs}` represents the left-hand-side of the equality,
:math:`name_{rhs}` represents the right-hand-side, and :math:`name_{mult}`
is an optional multiplier on the left hand side. If use_mult is True then
the default value of the multiplier is 1. The optional normalization function
:math:`f_{norm}` is computed as:
.. math::
f_{norm}(name_{rhs}) =
\begin{cases}
\left| name_{rhs} \right|, & \text{if normalize and } \left| name_{rhs} \right| \geq 2 \\
0.25 name_{rhs}^2 + 1, & \text{if normalize and } \left| name_{rhs} \right| < 2 \\
1, & \text{if not normalize}
\end{cases}
New output variables are created by calling `add_eq_output`.
Parameters
----------
name : str
The name of the output variable to be created.
eq_units : str or None
Units for the left-hand-side and right-hand-side of the difference equation.
lhs_name : str or None
Optional name for the LHS variable associated with the difference equation.
If None, the default will be used: 'lhs:{name}'.
rhs_name : str or None
Optional name for the RHS variable associated with the difference equation.
If None, the default will be used: 'rhs:{name}'.
rhs_val : int, float, or np.array
Default value for the RHS of the given output. Must be compatible
with the shape (optionally) given by the val or shape option in kwargs.
use_mult : bool
Specifies whether the LHS multiplier is to be used. If True, then an additional
input `mult_name` is created, with the default value given by `mult_val`, that
multiplies lhs. Default is False.
mult_name : str or None
Optional name for the LHS multiplier variable associated with the output
variable. If None, the default will be used: 'mult:{name}'.
mult_val : int, float, or np.array
Default value for the LHS multiplier of the given output. Must be compatible
with the shape (optionally) given by the val or shape option in kwargs.
normalize : bool
Specifies whether or not the resulting output should be normalized by the RHS. When
the RHS value is between [-2, 2], the normalization value is a quadratic function that
is close to one but still provides a C1 continuous function. When this option is True,
the user-provided ref/ref0 scaler/adder options below are typically unnecessary.
add_constraint : bool
Specifies whether to add an equality constraint.
ref : float or ndarray, optional
Value of response variable that scales to 1.0 in the driver. This option is only
meaningful when add_constraint=True.
ref0 : float or ndarray, optional
Value of response variable that scales to 0.0 in the driver. This option is only
meaningful when add_constraint=True.
adder : float or ndarray, optional
Value to add to the model value to get the scaled value for the driver. adder
is first in precedence. This option is only meaningful when add_constraint=True.
scaler : float or ndarray, optional
value to multiply the model value to get the scaled value for the driver. scaler
is second in precedence. This option is only meaningful when add_constraint=True.
**kwargs : dict
Additional arguments to be passed for the creation of the output variable.
(see `add_output` method).
"""
super(EQConstraintComp, self).__init__()
self._output_vars = {}
if name is not None:
self.add_eq_output(name, eq_units, lhs_name, rhs_name, rhs_val,
use_mult, mult_name, mult_val, normalize, add_constraint, ref, ref0,
adder, scaler, **kwargs)
def _post_configure(self):
"""
Define the independent variables, output variables, and partials.
"""
# set static mode to False because we are doing things that would normally be done in setup
self._static_mode = False
for name, options in iteritems(self._output_vars):
meta = self.add_output(name, **options['kwargs'])
shape = meta['shape']
for s in ('lhs', 'rhs', 'mult'):
if options['{0}_name'.format(s)] is None:
options['{0}_name'.format(s)] = '{0}:{1}'.format(s, name)
self.add_input(options['lhs_name'],
val=np.ones(shape),
units=options['eq_units'])
self.add_input(options['rhs_name'],
val=options['rhs_val'] * np.ones(shape),
units=options['eq_units'])
if options['use_mult']:
self.add_input(options['mult_name'],
val=options['mult_val'] * np.ones(shape),
units=None)
self._scale_factor = np.ones(shape)
self._dscale_drhs = np.ones(shape)
ar = np.arange(np.prod(shape))
self.declare_partials(of=name, wrt=options['lhs_name'], rows=ar, cols=ar, val=1.0)
self.declare_partials(of=name, wrt=options['rhs_name'], rows=ar, cols=ar, val=1.0)
if options['use_mult']:
self.declare_partials(of=name, wrt=options['mult_name'], rows=ar, cols=ar, val=1.0)
if options['add_constraint']:
self.add_constraint(name, equals=0., ref0=options['ref0'], ref=options['ref'],
adder=options['adder'], scaler=options['scaler'])
self._static_mode = True
super(EQConstraintComp, self)._post_configure()
def compute(self, inputs, outputs):
"""
Calculate the output for each equality constraint.
Parameters
----------
inputs : Vector
unscaled, dimensional input variables read via inputs[key]
outputs : Vector
unscaled, dimensional output variables read via outputs[key]
"""
if inputs._under_complex_step:
self._scale_factor = self._scale_factor.astype(np.complex)
else:
self._scale_factor = self._scale_factor.real
for name, options in iteritems(self._output_vars):
lhs = inputs[options['lhs_name']]
rhs = inputs[options['rhs_name']]
# Compute scaling factors
# scale factor that normalizes by the rhs, except near 0
if options['normalize']:
# Indices where the rhs is near zero or not near zero
idxs_nz = np.where(np.abs(rhs) < 2)[0]
idxs_nnz = np.where(np.abs(rhs) >= 2)[0]
self._scale_factor[idxs_nnz] = 1.0 / np.abs(rhs[idxs_nnz])
self._scale_factor[idxs_nz] = 1.0 / (.25 * rhs[idxs_nz] ** 2 + 1)
else:
self._scale_factor[:] = 1.0
if options['use_mult']:
outputs[name] = (inputs[options['mult_name']] * lhs - rhs) * self._scale_factor
else:
outputs[name] = (lhs - rhs) * self._scale_factor
def compute_partials(self, inputs, partials):
"""
Compute sub-jacobian parts. The model is assumed to be in an unscaled state.
Parameters
----------
inputs : Vector
unscaled, dimensional input variables read via inputs[key]
partials : Jacobian
sub-jac components written to partials[output_name, input_name]
"""
if inputs._under_complex_step:
self._dscale_drhs = self._dscale_drhs.astype(np.complex)
else:
self._dscale_drhs = self._dscale_drhs.real
for name, options in iteritems(self._output_vars):
lhs_name = options['lhs_name']
rhs_name = options['rhs_name']
lhs = inputs[lhs_name]
rhs = inputs[rhs_name]
if options['normalize']:
# Indices where the rhs is near zero or not near zero
idxs_nz = np.where(np.abs(rhs) < 2)[0]
idxs_nnz = np.where(np.abs(rhs) >= 2)[0]
# scale factor that normalizes by the rhs, except near 0
self._scale_factor[idxs_nnz] = 1.0 / np.abs(rhs[idxs_nnz])
self._scale_factor[idxs_nz] = 1.0 / (.25 * rhs[idxs_nz] ** 2 + 1)
self._dscale_drhs[idxs_nnz] = -np.sign(rhs[idxs_nnz]) / rhs[idxs_nnz]**2
self._dscale_drhs[idxs_nz] = -.5 * rhs[idxs_nz] / (.25 * rhs[idxs_nz] ** 2 + 1) ** 2
else:
self._scale_factor[:] = 1.0
self._dscale_drhs[:] = 0.0
if options['use_mult']:
mult_name = options['mult_name']
mult = inputs[mult_name]
# Partials of output wrt mult
deriv = lhs * self._scale_factor
partials[name, mult_name] = deriv.flatten()
else:
mult = 1.0
# Partials of output wrt rhs
deriv = (mult * lhs - rhs) * self._dscale_drhs - self._scale_factor
partials[name, rhs_name] = deriv.flatten()
# Partials of output wrt lhs
deriv = mult * self._scale_factor
partials[name, lhs_name] = deriv.flatten()
def add_eq_output(self, name, eq_units=None, lhs_name=None, rhs_name=None, rhs_val=0.0,
use_mult=False, mult_name=None, mult_val=1.0, normalize=True,
add_constraint=False, ref=None, ref0=None, adder=None, scaler=None, **kwargs):
"""
Add a new output variable computed via the difference equation.
This will create new inputs `lhs:name`, `rhs:name`, and `mult:name` that will
define the left and right sides of the difference equation, and a
multiplier for the left-hand-side.
Parameters
----------
name : str
The name of the output variable to be created.
eq_units : str or None
Units for the left-hand-side and right-hand-side of the difference equation.
lhs_name : str or None
Optional name for the LHS variable associated with the difference equation. If
None, the default will be used: 'lhs:{name}'.
rhs_name : str or None
Optional name for the RHS variable associated with the difference equation. If
None, the default will be used: 'rhs:{name}'.
rhs_val : int, float, or np.array
Default value for the RHS. Must be compatible with the shape (optionally)
given by the val or shape option in kwargs.
use_mult : bool
Specifies whether the LHS multiplier is to be used. If True, then an additional
input `mult_name` is created, with the default value given by `mult_val`, that
multiplies lhs. Default is False.
mult_name : str or None
Optional name for the LHS multiplier variable associated with the output
variable. If None, the default will be used: 'mult:{name}'.
mult_val : int, float, or np.array
Default value for the LHS multiplier. Must be compatible with the shape (optionally)
given by the val or shape option in kwargs.
normalize : bool
Specifies whether or not the resulting output should be normalized by a quadratic
function of the RHS. When this option is True, the user-provided ref/ref0 scaler/adder
options below are typically unnecessary.
add_constraint : bool
Specifies whether to add an equality constraint.
ref : float or ndarray, optional
Value of response variable that scales to 1.0 in the driver. This option is only
meaningful when add_constraint=True.
ref0 : float or ndarray, optional
Value of response variable that scales to 0.0 in the driver. This option is only
meaningful when add_constraint=True.
adder : float or ndarray, optional
Value to add to the model value to get the scaled value for the driver. adder
is first in precedence. This option is only meaningful when add_constraint=True.
scaler : float or ndarray, optional
Value to multiply the model value to get the scaled value for the driver. scaler
is second in precedence. This option is only meaningful when add_constraint=True.
**kwargs : dict
Additional arguments to be passed for the creation of the output variable.
(see `add_output` method).
"""
self._output_vars[name] = {'kwargs': kwargs,
'eq_units': eq_units,
'lhs_name': lhs_name,
'rhs_name': rhs_name,
'rhs_val': rhs_val,
'use_mult': use_mult,
'mult_name': mult_name,
'mult_val': mult_val,
'normalize': normalize,
'add_constraint': add_constraint,
'ref': ref,
'ref0': ref0,
'adder': adder,
'scaler': scaler}