Ejemplo n.º 1
0
    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'))
Ejemplo n.º 2
0
    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
Ejemplo n.º 3
0
    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
Ejemplo n.º 4
0
    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