• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

WISDEM / WEIS / 9244539224

26 May 2024 04:03PM UTC coverage: 80.482% (+9.6%) from 70.847%
9244539224

push

github

web-flow
Merge pull request #279 from WISDEM/wisdem_315

5 of 7 new or added lines in 2 files covered. (71.43%)

689 existing lines in 18 files now uncovered.

21615 of 26857 relevant lines covered (80.48%)

0.8 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.46
/weis/multifidelity/methods/base_method.py
1
import numpy as np
1✔
2
from scipy.interpolate import Rbf
1✔
3
import matplotlib.pyplot as plt
1✔
4
from collections import OrderedDict
1✔
5
import smt.surrogate_models as smt
1✔
6
from scipy import interpolate
1✔
7

8

9
class BaseMethod:
1✔
10
    """
11
    The base class that all multifidelity optimization methods inherit from.
12

13
    Attributes
14
    ----------
15
    model_low : BaseModel instance
16
        The low-fidelity model instance provided by the user.
17
    model_high : BaseModel instance
18
        The high-fidelity model instance provided by the user.
19
    bounds : dict
20
        A dictionary with keys for each design variable and the values of those
21
        keys correspond to the design variable bounds, e.g. [[0., 1.], ...].
22
    disp : bool, optional
23
        If True, the method will print out progress and results to the terminal.
24
    counter_plot : int
25
        Int for the number of plots that have been created so the filenames
26
        are saved correctly.
27
    objective : string
28
        Name of the objective function whose output is provided by the user-provided
29
        models.
30
    objective_scaler : float
31
        Multiplicative scaling factor applied to the objective function before
32
        passing it to the optimizer. Useful for mamximizing functions instead
33
        of minimizing them by providing a negative number.
34
    constraints : list of dicts
35
        Each dict contains the constraint output name, the constraint type (equality
36
        or inequality), and the constraint value. One dict for each set of constraints.
37
    list_of_constraints : list of dicts
38
        A list of converted constraints and callable functions ready to be used
39
        by Scipy optimize.
40
    n_dims : int
41
        Number of dimensions in the design space, i.e. number of design variables.
42
    design_vectors : array
43
        2-D array containing all of the queried design points. Rows (the 2nd dimension)
44
        contain the design vectors, whereas the number of columns (the 1st dimension)
45
        correspond to the number of design points.
46
    approximation_functions : dict of callables
47
        Dictionary whose keys are the string names of all functions of interest
48
        (objective and all constraints) and the corresponding values are callable
49
        funcs for an approximation of those values across the design space.    
50
    """
51

52
    def __init__(self, model_low, model_high, bounds, disp=True, num_initial_points=5):
1✔
53
        """
54
        Initialize the method by storing the models and populating the
55
        first points.
56
        
57
        Parameters
58
        ----------
59
        model_low : BaseModel instance
60
            The low-fidelity model instance provided by the user.
61
        model_high : BaseModel instance
62
            The high-fidelity model instance provided by the user.
63
        bounds : dict
64
            A dictionary with keys for each design variable and the values of those
65
            keys correspond to the design variable bounds, e.g. [[0., 1.], ...].
66
        disp : bool, optional
67
            If True, the method will print out progress and results to the terminal.
68
        num_initial_points : int
69
            The number of initial points to use to populate the surrogate-based
70
            approximations of the methods. In general, higher dimensional problems
71
            require more initial points to get a reasonable surrogate approximation.
72
        """
73

74
        self.model_low = model_low
1✔
75
        self.model_high = model_high
1✔
76
        
77
        self.bounds = self.flatten_bounds_dict(bounds)
1✔
78
        self.disp = disp
1✔
79

80
        self.initialize_points(num_initial_points)
1✔
81
        self.counter_plot = 0
1✔
82

83
        self.objective = None
1✔
84
        self.constraints = []
1✔
85
        
86
    def flatten_bounds_dict(self, bounds):
1✔
87
        """
88
        Given a dict of bounds, return an array of bound pairs.
89
        
90
        Parameters
91
        ----------
92
        bounds : dict
93
            Dict of bounds keys/values.
94
            
95
        Returns
96
        -------
97
        flattened_bounds : array
98
            Flattened array of bounds values.
99
        """
100
        flattened_bounds = []
1✔
101

102
        for key, value in bounds.items():
1✔
103
            if isinstance(value, (float, list)):
1✔
104
                value = np.array(value)
×
105
            flattened_value = np.squeeze(value.flatten()).reshape(-1, 2)
1✔
106
            flattened_bounds.extend(flattened_value)
1✔
107

108
        return np.array(flattened_bounds)
1✔
109

110
    def initialize_points(self, num_initial_points):
1✔
111
        """
112
        Populate the design_vectors array with a set of initial points that will be used
113
        to create the initial surrogate models.
114
        
115
        Modifies `design_vectors` in-place.
116
        
117
        Parameters
118
        ----------
119
        num_initial_points : int
120
            Number of points used to populate the initial surrogate model creation.
121
            These points are called using both the low- and high-fidelity models.
122
        
123
        """
124
        self.n_dims = self.model_high.total_size
1✔
125
        x_init_raw = np.random.rand(num_initial_points, self.n_dims)
1✔
126
        self.design_vectors = (
1✔
127
            x_init_raw * (self.bounds[:, 1] - self.bounds[:, 0]) + self.bounds[:, 0]
128
        )
129

130
    def set_initial_point(self, initial_design):
1✔
131
        """
132
        Set the initial point for the optimization method.
133
        
134
        Modifies `design_vectors` in-place.
135
        
136
        Parameters
137
        ----------
138
        initial_design : array
139
            Initial design point for the optimization method.
140
        
141
        """
142
        if isinstance(initial_design, (float, list)):
1✔
143
            initial_design = np.array(initial_design)
1✔
144
        self.design_vectors = np.vstack((self.design_vectors, initial_design))
1✔
145

146
    def add_objective(self, objective_name, scaler=1.0):
1✔
147
        """
148
        Set the optimization objective string and scaler.
149
        
150
        Parameters
151
        ----------
152
        objective : string
153
            Name of the objective function whose output is provided by the user-provided
154
            models.
155
        scaler : float
156
            Multiplicative scaling factor applied to the objective function before
157
            passing it to the optimizer. Useful for mamximizing functions instead
158
            of minimizing them by providing a negative number.
159
        
160
        """
161
        self.objective = objective_name
1✔
162
        self.objective_scaler = scaler
1✔
163

164
    def add_constraint(self, constraint_name, equals=None, lower=None, upper=None):
1✔
165
        """
166
        Append user-defined constraints into a list of dicts with all constraint
167
        info to be used later.
168
        
169
        Modifies `constraints` in-place.
170
        
171
        Parameters
172
        ----------
173
        constraint_name : string
174
            Name of the output value to constrain.
175
        equals : float or None
176
            If a float, the value at which to constrain the output.
177
        lower : float or None
178
            If a float, the value of the lower bound for the constraint.
179
        upper : float or None
180
            If a float, the value of the upper bound for the constraint.
181
        
182
        """
183
        self.constraints.append(
1✔
184
            {"name": constraint_name, "equals": equals, "lower": lower, "upper": upper,}
185
        )
186

187
    def process_constraints(self):
1✔
188
        """
189
        Convert the list of user-defined constraint dicts into constraint functions
190
        compatible with Scipy optimize.
191
        
192
        Modifies `list_of_constraints` in-place.
193
        
194
        """
195
        list_of_constraints = []
1✔
196
        for constraint in self.constraints:
1✔
197
            scipy_constraint = {}
1✔
198

199
            func = self.approximation_functions[constraint["name"]]
1✔
200
            if constraint["equals"] is not None:
1✔
201
                scipy_constraint["type"] = "eq"
1✔
202
                scipy_constraint["fun"] = lambda x: np.squeeze(
1✔
203
                    func(x) - constraint["equals"]
204
                )
205

206
            if constraint["upper"] is not None:
1✔
207
                scipy_constraint["type"] = "ineq"
1✔
208
                scipy_constraint["fun"] = lambda x: np.squeeze(
1✔
209
                    constraint["upper"] - func(x)
210
                )
211

212
            if constraint["lower"] is not None:
1✔
213
                scipy_constraint["type"] = "ineq"
1✔
214
                scipy_constraint["fun"] = lambda x: np.squeeze(
1✔
215
                    func(x) - constraint["lower"]
216
                )
217

218
            list_of_constraints.append(scipy_constraint)
1✔
219
            
220
        self.list_of_constraints = list_of_constraints
1✔
221

222
    def construct_approximations(self, interp_method="smt"):
1✔
223
        """
224
        Create callable functions for each of the corrected low-fidelity
225
        models by constructing surrogate models for the error between
226
        low- and high-fidelity results.
227
        
228
        Follows the process laid out by multiple trust-region methods presented
229
        in the literature where we construct a corrected low-fidelity model.
230
        This correction comes from a surrogate model trained by the error between
231
        the low- and high-fidelity models. Each call to one of these callable
232
        funcs runs the low-fidelity model and queries the surrogate model at
233
        that design point to obtain the corrected output.
234
        
235
        This method create approximation functions for the objective function and
236
        all constraints in the same way.
237
        
238
        Depending on the smoothness of the underlying functions, certain surrogate
239
        models may be better suited to model the error between the models.
240
        Future studies could focus on when to use which surrogate model.
241
        
242
        Modifies `approximation_functions` in-place.
243
        
244
        Parameters
245
        ----------
246
        interp_method : string
247
            Set the type of surrogate model method to use, valid options are 'rbf'
248
            and 'smt' for now. 
249
        
250
        """
251
        outputs_low = self.model_low.run_vec(self.design_vectors)
1✔
252
        outputs_high = self.model_high.run_vec(self.design_vectors)
1✔
253

254
        approximation_functions = {}
1✔
255
        outputs_to_approximate = [self.objective]
1✔
256

257
        if len(self.constraints) > 0:
1✔
258
            for constraint in self.constraints:
1✔
259
                outputs_to_approximate.append(constraint["name"])
1✔
260

261
        for output_name in outputs_to_approximate:
1✔
262
            differences = outputs_high[output_name] - outputs_low[output_name]
1✔
263

264
            if interp_method == "rbf":
1✔
265
                input_arrays = np.split(self.design_vectors, self.design_vectors.shape[1], axis=1)
×
266
                input_arrays = [x.flatten() for x in input_arrays]
×
267

268
                # Construct RBF interpolater for error function
269
                e = Rbf(*input_arrays, differences)
×
270

271
                # Create m_k = lofi + RBF
272
                def approximation_function(x):
×
273
                    return self.model_low.run(x)[output_name] + e(*x)
×
274
                    
275
            if interp_method in ["linear", "quadratic", "cubic"]:
1✔
276
                sm = interpolate.interp1d(np.squeeze(self.design_vectors), differences, kind=interp_method, fill_value="extrapolate")
×
277

278
                def approximation_function(x, output_name=output_name, sm=sm):
×
UNCOV
279
                    return self.model_low.run(x)[output_name] + sm(
×
280
                        np.atleast_2d(x)
281
                    )
282

283
            elif interp_method == "smt":
1✔
284
                # If there's no difference between the high- and low-fidelity values,
285
                # we don't need to construct a surrogate. This is useful for
286
                # outputs that don't vary between fidelities, like geometric properties.
287
                if np.all(differences == 0.):
1✔
288
                    def approximation_function(x, output_name=output_name):
×
289
                        return self.model_low.run(x)[output_name]
×
290
                        
291
                else:
292
                    # sm = smt.RBF(print_global=False, d0=5)
293
                    # sm = smt.IDW(print_global=False, p=2)
294
                    # sm = smt.KRG(theta0=[1e-2], print_global=False)
295
                    # sm = smt.RMTB(
296
                    #     num_ctrl_pts=12,
297
                    #     xlimits=self.bounds,
298
                    #     nonlinear_maxiter=20,
299
                    #     solver_tolerance=1e-16,
300
                    #     energy_weight=1e-6,
301
                    #     regularization_weight=0.0,
302
                    #     print_global=False
303
                    # )
304
                    sm = smt.KPLS(print_global=False, theta0=[1e-1])
1✔
305

306
                    sm.set_training_values(self.design_vectors, differences)
1✔
307
                    sm.train()
1✔
308

309
                    def approximation_function(x, output_name=output_name, sm=sm):
1✔
310
                        return self.model_low.run(x)[output_name] + sm.predict_values(
1✔
311
                            np.atleast_2d(x)
312
                        )
313

314
            # Create m_k = lofi + RBF
315
            approximation_functions[output_name] = approximation_function
1✔
316

317
        self.approximation_functions = approximation_functions
1✔
318

319
    def process_results(self):
1✔
320
        """
321
        Store results from the optimization into a dictionary and return those results.
322
        """
323
        
324
        results = {}
1✔
325
        results["optimal_design"] = self.design_vectors[-1, :]
1✔
326
        results["high_fidelity_func_value"] = self.model_high.run(
1✔
327
            self.design_vectors[-1, :]
328
        )[self.objective]
329
        results["number_high_fidelity_calls"] = len(self.design_vectors[:, 0])
1✔
330
        results["design_vectors"] = self.design_vectors
1✔
331
        outputs = self.model_high.run_vec(np.atleast_2d(self.design_vectors[-1, :]))
1✔
332
        results["outputs"] = outputs        
1✔
333

334
        if self.disp:
1✔
335
            print()
×
336
            print(results)
×
337

338
        return results
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc