def _save_history(self): # type: () -> None """Save the solvers history to a collection of dictionaries.""" # Load optimization problem prob = self.system_optimizer_prob # Save design variables and bounds local_bounds = self.LOCAL_BOUNDS opt_dv_values = self.OPT_DV_VALUES ref0_values = self.REF0_VALUES ref_values = self.REF_VALUES for var_name, attrbs in prob.model._design_vars.items(): # Initialize variables val_lb, val_ub = attrbs['lower'], attrbs['upper'] val_ref0, val_ref = attrbs['ref0'], attrbs['ref'] adder, scaler = attrbs['adder'], attrbs['scaler'] def scaled(x): return scale_value(x, adder, scaler) val_opt = scaled( format_as_float_or_array('optimum', copy.deepcopy(prob[var_name]))) local_bounds.setdefault(var_name, []).append((val_lb, val_ub)) ref0_values.setdefault(var_name, []).append(val_ref0) ref_values.setdefault(var_name, []).append(val_ref) opt_dv_values.setdefault(var_name, []).append(val_opt) # Save objective value var_name = prob.model.objective opt_obj_values = self.OPT_OBJ_VALUES val_obj = copy.deepcopy(prob[var_name]) opt_obj_values.setdefault(var_name, []).append(val_obj) # Save constraint values opt_con_values = self.OPT_CON_VALUES attrbs_con_values = self.ATTRBS_CON_VALUES for var_name, attrbs in prob.model.constraints.items(): val_con = copy.deepcopy(prob[var_name]) opt_con_values.setdefault(var_name, []).append(val_con) attrbs_con_values[ var_name] = attrbs # Overwrite allowed: constant for each iteration # Pickle all output history = dict(local_bounds=local_bounds, opt_dv_values=opt_dv_values, ref0_values=ref0_values, ref_values=ref_values, opt_obj_values=opt_obj_values, opt_con_values=opt_con_values, attrbs_con_values=attrbs_con_values) output_folder = prob.model.data_folder output_case_str = prob.output_case_string output_file_path = os.path.join( output_folder, 'b2k_history_{}.p'.format(output_case_str)) pickle.dump(history, open(output_file_path, 'wb'))
def add_output(self, name, val=1.0, shape=None, units=None, res_units=None, desc='', lower=None, upper=None, ref=1.0, ref0=0.0, res_ref=1.0, var_set=0): """ Add an output variable to the component. Parameters ---------- name : str name of the variable in this component's namespace. val : float or list or tuple or ndarray The initial value of the variable being added in user-defined units. Default is 1.0. shape : int or tuple or list or None Shape of this variable, only required if val is not an array. Default is None. units : str or None Units in which the output variables will be provided to the component during execution. Default is None, which means it has no units. res_units : str or None Units in which the residuals of this output will be given to the user when requested. Default is None, which means it has no units. desc : str description of the variable. lower : float or list or tuple or ndarray or Iterable or None lower bound(s) in user-defined units. It can be (1) a float, (2) an array_like consistent with the shape arg (if given), or (3) an array_like matching the shape of val, if val is array_like. A value of None means this output has no lower bound. Default is None. upper : float or list or tuple or ndarray or or Iterable None upper bound(s) in user-defined units. It can be (1) a float, (2) an array_like consistent with the shape arg (if given), or (3) an array_like matching the shape of val, if val is array_like. A value of None means this output has no upper bound. Default is None. ref : float or ndarray Scaling parameter. The value in the user-defined units of this output variable when the scaled value is 1. Default is 1. ref0 : float or ndarray Scaling parameter. The value in the user-defined units of this output variable when the scaled value is 0. Default is 0. res_ref : float or ndarray Scaling parameter. The value in the user-defined res_units of this output's residual when the scaled value is 1. Default is 1. var_set : hashable object For advanced users only. ID or color for this variable, relevant for reconfigurability. Default is 0. Returns ------- dict metadata for added variable """ if units == 'unitless': warn_deprecation("Output '%s' has units='unitless' but 'unitless' " "has been deprecated. Use " "units=None instead. Note that connecting a " "unitless variable to one with units is no longer " "an error, but will issue a warning instead." % name) units = None # First, type check all arguments if not isinstance(name, str): raise TypeError('The name argument should be a string') if not np.isscalar(val) and not isinstance(val, (list, tuple, np.ndarray, Iterable)): raise TypeError('The val argument should be a float, list, tuple, or ndarray') if not np.isscalar(ref) and not isinstance(val, (list, tuple, np.ndarray, Iterable)): raise TypeError('The ref argument should be a float, list, tuple, or ndarray') if not np.isscalar(ref0) and not isinstance(val, (list, tuple, np.ndarray, Iterable)): raise TypeError('The ref0 argument should be a float, list, tuple, or ndarray') if not np.isscalar(res_ref) and not isinstance(val, (list, tuple, np.ndarray, Iterable)): raise TypeError('The res_ref argument should be a float, list, tuple, or ndarray') if shape is not None and not isinstance(shape, (int, tuple, list, np.integer)): raise TypeError("The shape argument should be an int, tuple, or list but " "a '%s' was given" % type(shape)) if units is not None and not isinstance(units, str): raise TypeError('The units argument should be a str or None') if res_units is not None and not isinstance(res_units, str): raise TypeError('The res_units argument should be a str or None') # Check that units are valid if units is not None and not valid_units(units): raise ValueError("The units '%s' are invalid" % units) metadata = {} # value, shape: based on args, making sure they are compatible metadata['value'], metadata['shape'] = ensure_compatible(name, val, shape) metadata['size'] = np.prod(metadata['shape']) # units, res_units: taken as is metadata['units'] = units metadata['res_units'] = res_units # desc: taken as is metadata['desc'] = desc if lower is not None: lower = ensure_compatible(name, lower, metadata['shape'])[0] if upper is not None: upper = ensure_compatible(name, upper, metadata['shape'])[0] metadata['lower'] = lower metadata['upper'] = upper # All refs: check the shape if necessary for item, item_name in zip([ref, ref0, res_ref], ['ref', 'ref0', 'res_ref']): if not np.isscalar(item): if np.atleast_1d(item).shape != metadata['shape']: raise ValueError('The %s argument has the wrong shape' % item_name) if np.isscalar(ref): self._has_output_scaling |= ref != 1.0 else: self._has_output_scaling |= np.any(ref != 1.0) if np.isscalar(ref0): self._has_output_scaling |= ref0 != 0.0 else: self._has_output_scaling |= np.any(ref0) if np.isscalar(res_ref): self._has_resid_scaling |= res_ref != 1.0 else: self._has_resid_scaling |= np.any(res_ref != 1.0) ref = format_as_float_or_array('ref', ref, flatten=True) ref0 = format_as_float_or_array('ref0', ref0, flatten=True) res_ref = format_as_float_or_array('res_ref', res_ref, flatten=True) # ref, ref0, res_ref: taken as is metadata['ref'] = ref metadata['ref0'] = ref0 metadata['res_ref'] = res_ref # var_set: taken as is metadata['var_set'] = var_set metadata['distributed'] = self.distributed # We may not know the pathname yet, so we have to use name for now, instead of abs_name. if self._static_mode: var_rel2data_io = self._static_var_rel2data_io var_rel_names = self._static_var_rel_names else: var_rel2data_io = self._var_rel2data_io var_rel_names = self._var_rel_names # Disallow dupes if name in var_rel2data_io: msg = "Variable name '{}' already exists.".format(name) raise ValueError(msg) var_rel2data_io[name] = { 'prom': name, 'rel': name, 'my_idx': len(self._var_rel_names['output']), 'type': 'output', 'metadata': metadata} var_rel_names['output'].append(name) return metadata
def add_output(self, name, val=1.0, shape=None, units=None, res_units=None, desc='', lower=None, upper=None, ref=1.0, ref0=0.0, res_ref=1.0): """ Add an output variable to the component. Parameters ---------- name : str name of the variable in this component's namespace. val : float or list or tuple or ndarray The initial value of the variable being added in user-defined units. Default is 1.0. shape : int or tuple or list or None Shape of this variable, only required if val is not an array. Default is None. units : str or None Units in which the output variables will be provided to the component during execution. Default is None, which means it has no units. res_units : str or None Units in which the residuals of this output will be given to the user when requested. Default is None, which means it has no units. desc : str description of the variable. lower : float or list or tuple or ndarray or Iterable or None lower bound(s) in user-defined units. It can be (1) a float, (2) an array_like consistent with the shape arg (if given), or (3) an array_like matching the shape of val, if val is array_like. A value of None means this output has no lower bound. Default is None. upper : float or list or tuple or ndarray or or Iterable None upper bound(s) in user-defined units. It can be (1) a float, (2) an array_like consistent with the shape arg (if given), or (3) an array_like matching the shape of val, if val is array_like. A value of None means this output has no upper bound. Default is None. ref : float or ndarray Scaling parameter. The value in the user-defined units of this output variable when the scaled value is 1. Default is 1. ref0 : float or ndarray Scaling parameter. The value in the user-defined units of this output variable when the scaled value is 0. Default is 0. res_ref : float or ndarray Scaling parameter. The value in the user-defined res_units of this output's residual when the scaled value is 1. Default is 1. Returns ------- dict metadata for added variable """ if units == 'unitless': warn_deprecation("Output '%s' has units='unitless' but 'unitless' " "has been deprecated. Use " "units=None instead. Note that connecting a " "unitless variable to one with units is no longer " "an error, but will issue a warning instead." % name) units = None if not isinstance(name, str): raise TypeError('The name argument should be a string') if not _valid_var_name(name): raise NameError("'%s' is not a valid output name." % name) if not isscalar(val) and not isinstance(val, (list, tuple, ndarray, Iterable)): msg = 'The val argument should be a float, list, tuple, ndarray or Iterable' raise TypeError(msg) if not isscalar(ref) and not isinstance(val, (list, tuple, ndarray, Iterable)): msg = 'The ref argument should be a float, list, tuple, ndarray or Iterable' raise TypeError(msg) if not isscalar(ref0) and not isinstance(val, (list, tuple, ndarray, Iterable)): msg = 'The ref0 argument should be a float, list, tuple, ndarray or Iterable' raise TypeError(msg) if not isscalar(res_ref) and not isinstance(val, (list, tuple, ndarray, Iterable)): msg = 'The res_ref argument should be a float, list, tuple, ndarray or Iterable' raise TypeError(msg) if shape is not None and not isinstance(shape, (int, tuple, list, np.integer)): raise TypeError("The shape argument should be an int, tuple, or list but " "a '%s' was given" % type(shape)) if units is not None and not isinstance(units, str): raise TypeError('The units argument should be a str or None') if res_units is not None and not isinstance(res_units, str): raise TypeError('The res_units argument should be a str or None') # Check that units are valid if units is not None and not valid_units(units): raise ValueError("The units '%s' are invalid" % units) metadata = {} # value, shape: based on args, making sure they are compatible metadata['value'], metadata['shape'], _ = ensure_compatible(name, val, shape) metadata['size'] = np.prod(metadata['shape']) # units, res_units: taken as is metadata['units'] = units metadata['res_units'] = res_units # desc: taken as is metadata['desc'] = desc if lower is not None: lower = ensure_compatible(name, lower, metadata['shape'])[0] if upper is not None: upper = ensure_compatible(name, upper, metadata['shape'])[0] metadata['lower'] = lower metadata['upper'] = upper # All refs: check the shape if necessary for item, item_name in zip([ref, ref0, res_ref], ['ref', 'ref0', 'res_ref']): if not isscalar(item): it = atleast_1d(item) if it.shape != metadata['shape']: raise ValueError("'{}': When adding output '{}', expected shape {} but got " "shape {} for argument '{}'.".format(self.name, name, metadata['shape'], it.shape, item_name)) if isscalar(ref): self._has_output_scaling |= ref != 1.0 else: self._has_output_scaling |= np.any(ref != 1.0) if isscalar(ref0): self._has_output_scaling |= ref0 != 0.0 else: self._has_output_scaling |= np.any(ref0) if isscalar(res_ref): self._has_resid_scaling |= res_ref != 1.0 else: self._has_resid_scaling |= np.any(res_ref != 1.0) ref = format_as_float_or_array('ref', ref, flatten=True) ref0 = format_as_float_or_array('ref0', ref0, flatten=True) res_ref = format_as_float_or_array('res_ref', res_ref, flatten=True) metadata['ref'] = ref metadata['ref0'] = ref0 metadata['res_ref'] = res_ref metadata['distributed'] = self.options['distributed'] # We may not know the pathname yet, so we have to use name for now, instead of abs_name. if self._static_mode: var_rel2meta = self._static_var_rel2meta var_rel_names = self._static_var_rel_names else: var_rel2meta = self._var_rel2meta var_rel_names = self._var_rel_names # Disallow dupes if name in var_rel2meta: msg = "Variable name '{}' already exists.".format(name) raise ValueError(msg) var_rel2meta[name] = metadata var_rel_names['output'].append(name) return metadata
def _get_new_bounds(self, var_name, attrbs): # type: (str, dict) -> dict """Method that determines new bounds for the design variables for the next BLISS loop. Bounds are initially reduced, but will be increased if bounds are hit or if the system-level optimization failed. Parameters ---------- var_name : str Design variable name of which new bounds should be determined attrbs : dict Attributes of the design variable (ref0, ref, lower, upper, adder, scaler) Returns ------- attrbs_new : dict Dictionary specifying the new bounds. """ options = self.options f_k_red = options['f_k_red'] # K-factor reduction f_int_inc = options[ 'f_int_inc'] # fraction of interval increase if bound is hit f_int_inc_abs = options[ 'f_int_inc_abs'] # absolute interval increase if fraction too small f_int_range = options[ 'f_int_range'] # minimum range of design variable interval btol = 1e-2 # Tolerance for boundary hit determination prob = self.system_optimizer_prob n_loop = self._iter_count - 1 opt_failed = prob.driver.fail # Initialize variables val_lb, val_ub = attrbs['lower'], attrbs['upper'] val_ref0, val_ref = attrbs['ref0'], attrbs['ref'] adder, scaler = attrbs['adder'], attrbs['scaler'] def unscaled(x): return unscale_value(x, val_ref0, val_ref) def scaled(x): return scale_value(x, adder, scaler) val_opt = scaled( format_as_float_or_array('optimum', copy.deepcopy(prob[var_name]))) if isinstance(val_opt, np.ndarray) and val_opt.size != 1: raise AssertionError( 'Design vectors are not (yet) supported for the B2k solver.') else: val_opt = val_opt[0] if n_loop == 0: local_bounds_pr = None else: local_bounds_pr = self.LOCAL_BOUNDS[var_name][n_loop - 1] # Get and/or store global bounds if n_loop == 0: model_des_var = prob.model.design_vars[var_name] val_min, val_max = scaled(model_des_var['global_lower']), \ scaled(model_des_var['global_upper']) self.GLOBAL_BOUNDS[var_name] = (val_min, val_max, val_ref0, val_ref) else: val_min_sc, val_max_sc, val_ref0_gb, val_ref_gb = self.GLOBAL_BOUNDS[ var_name] val_min_us = unscale_value(val_min_sc, val_ref0_gb, val_ref_gb) val_max_us = unscale_value(val_max_sc, val_ref0_gb, val_ref_gb) val_min, val_max = scaled(val_min_us), scaled(val_max_us) val_interval = val_ub - val_lb # If optimum is outside of bounds, then set the bound to the out_of_bounds = val_opt < val_lb or val_opt > val_ub val_lb_new = val_lb val_ub_new = val_ub if val_opt < val_lb: val_lb_new = val_opt val_opt = val_lb elif val_opt > val_ub: val_ub_new = val_opt val_opt = val_ub # Reduce bounds based on K-factor reduction if not opt_failed and not out_of_bounds: adjust = abs((val_ub + val_lb) / 2 - val_opt) / ( (val_ub + val_lb) / 2 - val_lb) reduce_val = adjust + (1 - adjust) * f_k_red val_lb_new = val_opt - (val_interval / reduce_val) / 2 val_ub_new = val_opt + (val_interval / reduce_val) / 2 elif n_loop == 0 and opt_failed: raise NotImplementedError( 'First system-level optimization needs to be successful ' 'for the BLISS solver to work.') # If bound has been hit (twice ==> increase) if (n_loop > 0 or opt_failed) and not out_of_bounds: val_opt_pr = float(self.OPT_DV_VALUES[var_name][n_loop - 1]) # lower bound hit twice or optimization failed lower_bound_hit = (val_opt - btol <= val_lb and val_opt_pr - btol <= local_bounds_pr[0]) or opt_failed dist_lb = abs(val_opt - val_lb) # upper bound hit twice or optimization failed upper_bound_hit = (val_opt + btol >= val_ub and val_opt_pr + btol >= local_bounds_pr[1]) or opt_failed dist_ub = abs(val_opt - val_ub) change_bound = [] if lower_bound_hit and upper_bound_hit: if dist_lb < dist_ub: change_bound = ['lb'] elif dist_ub < dist_lb: change_bound = ['ub'] else: change_bound = ['lb', 'ub'] elif lower_bound_hit or upper_bound_hit: if upper_bound_hit: change_bound = ['ub'] else: change_bound = ['lb'] incr = abs(val_interval * f_int_inc / 2) if 'lb' in change_bound: if incr >= f_int_inc_abs: val_lb_new = val_lb - val_interval * f_int_inc / 2 else: val_lb_new = val_lb - f_int_inc_abs elif 'ub' in change_bound: if incr >= f_int_inc_abs: val_ub_new = val_ub + val_interval * f_int_inc / 2 else: val_ub_new = val_ub + f_int_inc_abs # Check if bounds are not reversed -> otherwise set equal with minimal range if val_lb_new > val_ub_new: val_lb_new = val_opt - .5 * f_int_range val_ub_new = val_opt + .5 * f_int_range # If interval range is smaller than the minimum range -> adjust accordingly if abs(val_ub_new - val_lb_new) < f_int_range: # First consider upper bound dist_ub = abs(val_opt - val_max) if dist_ub < .5 * f_int_range: val_ub_new = val_max rest_range_ub = .5 * f_int_range - dist_ub else: val_ub_new = val_opt + .5 * f_int_range rest_range_ub = 0. # Then adjust lower bound accordingly dist_lb = abs(val_opt - val_min) if dist_lb < .5 * f_int_range: val_lb_new = val_min rest_range_lb = .5 * f_int_range - dist_lb else: val_lb_new = val_opt - .5 * f_int_range - rest_range_ub rest_range_lb = 0. # Add lower bound rest range to the upper bound val_ub_new += rest_range_lb # If interval is outside maximum bounds -> set equal to appropriate extremum if val_lb_new < val_min: val_lb_new = val_min if val_ub_new > val_max: val_ub_new = val_max # Save new bounds and nominal values in attribute dictionary attrbs_new = {} if opt_failed: attrbs_new['initial'] = np.array( [unscaled((val_ub_new + val_lb_new) / 2.)]) else: attrbs_new['initial'] = np.array([unscaled(val_opt)]) attrbs_new['lower'], attrbs_new['ref0'] = unscaled( val_lb_new), unscaled(val_lb_new) attrbs_new['upper'], attrbs_new['ref'] = unscaled( val_ub_new), unscaled(val_ub_new) return attrbs_new