Esempio n. 1
0
def get_configuration(key):
    config = ConfigDict()
    config.declare('print_callback_message', 
                   ConfigValue(domain=bool,
                               description='Print a message when callback is called',
                               default=False)).declare_as_argument(f'--{key}.print-callback-message')
    return config
Esempio n. 2
0
    def test_config_integration(self):
        c = ConfigList()
        c.add(1)
        c.add(3)
        c.add(5)
        a = Initializer(c)
        self.assertIs(type(a), ItemInitializer)
        self.assertTrue(a.contains_indices())
        self.assertEqual(list(a.indices()), [0, 1, 2])
        self.assertEqual(a(None, 0), 1)
        self.assertEqual(a(None, 1), 3)
        self.assertEqual(a(None, 2), 5)

        c = ConfigDict()
        c.declare('opt_1', ConfigValue(default=1))
        c.declare('opt_3', ConfigValue(default=3))
        c.declare('opt_5', ConfigValue(default=5))
        a = Initializer(c)
        self.assertIs(type(a), ItemInitializer)
        self.assertTrue(a.contains_indices())
        self.assertEqual(list(a.indices()), ['opt_1', 'opt_3', 'opt_5'])
        self.assertEqual(a(None, 'opt_1'), 1)
        self.assertEqual(a(None, 'opt_3'), 3)
        self.assertEqual(a(None, 'opt_5'), 5)
Esempio n. 3
0
def get_configuration(config):
    ans = ConfigDict()
    ans.declare('key1', ConfigValue(default=0, domain=int))
    ans.declare('key2', ConfigValue(default=5, domain=str))
    return ans(config)
Esempio n. 4
0
def _trf_config():
    """
    Generate the configuration dictionary.
    The user may change the configuration options during the instantiation
    of the trustregion solver:
        >>> optTRF = SolverFactory('trustregion',
        ...                        solver='ipopt',
        ...                        maximum_iterations=50,
        ...                        minimum_radius=1e-5,
        ...                        verbose=True)

    The user may also update the configuration after instantiation:
        >>> optTRF = SolverFactory('trustregion')
        >>> optTRF._CONFIG.trust_radius = 0.5

    The user may also update the configuration as part of the solve call:
        >>> optTRF = SolverFactory('trustregion')
        >>> optTRF.solve(model, decision_variables, trust_radius=0.5)
    Returns
    -------
    CONFIG : ConfigDict
        This holds all configuration options to be passed to the TRF solver.

    """
    CONFIG = ConfigDict('TrustRegion')

    ### Solver options
    CONFIG.declare(
        'solver',
        ConfigValue(default='ipopt',
                    description='Solver to use. Default = ``ipopt``.'))
    CONFIG.declare(
        'keepfiles',
        ConfigValue(default=False,
                    domain=Bool,
                    description="Optional. Whether or not to "
                    "write files of sub-problems for use in debugging. "
                    "Default = False."))
    CONFIG.declare(
        'tee',
        ConfigValue(default=False,
                    domain=Bool,
                    description="Optional. Sets the ``tee`` "
                    "for sub-solver(s) utilized. "
                    "Default = False."))

    ### Trust Region specific options
    CONFIG.declare(
        'verbose',
        ConfigValue(default=False,
                    domain=Bool,
                    description="Optional. When True, print each "
                    "iteration's relevant information to the console "
                    "as well as to the log. "
                    "Default = False."))
    CONFIG.declare(
        'trust_radius',
        ConfigValue(default=1.0,
                    domain=PositiveFloat,
                    description="Initial trust region radius ``delta_0``. "
                    "Default = 1.0."))
    CONFIG.declare(
        'minimum_radius',
        ConfigValue(
            default=1e-6,
            domain=PositiveFloat,
            description="Minimum allowed trust region radius ``delta_min``. "
            "Default = 1e-6."))
    CONFIG.declare(
        'maximum_radius',
        ConfigValue(
            default=CONFIG.trust_radius * 100,
            domain=PositiveFloat,
            description="Maximum allowed trust region radius. If trust region "
            "radius reaches maximum allowed, solver will exit. "
            "Default = 100 * trust_radius."))
    CONFIG.declare(
        'maximum_iterations',
        ConfigValue(default=50,
                    domain=PositiveInt,
                    description="Maximum allowed number of iterations. "
                    "Default = 50."))
    ### Termination options
    CONFIG.declare(
        'feasibility_termination',
        ConfigValue(
            default=1e-5,
            domain=PositiveFloat,
            description=
            "Feasibility measure termination tolerance ``epsilon_theta``. "
            "Default = 1e-5."))
    CONFIG.declare(
        'step_size_termination',
        ConfigValue(
            default=CONFIG.feasibility_termination,
            domain=PositiveFloat,
            description="Step size termination tolerance ``epsilon_s``. "
            "Matches the feasibility termination tolerance by default."))
    ### Switching Condition options
    CONFIG.declare(
        'minimum_feasibility',
        ConfigValue(default=1e-4,
                    domain=PositiveFloat,
                    description="Minimum feasibility measure ``theta_min``. "
                    "Default = 1e-4."))
    CONFIG.declare(
        'switch_condition_kappa_theta',
        ConfigValue(
            default=0.1,
            domain=In(NumericRange(0, 1, 0, (False, False))),
            description="Switching condition parameter ``kappa_theta``. "
            "Contained in open set (0, 1). "
            "Default = 0.1."))
    CONFIG.declare(
        'switch_condition_gamma_s',
        ConfigValue(default=2.0,
                    domain=PositiveFloat,
                    description="Switching condition parameter ``gamma_s``. "
                    "Must satisfy: ``gamma_s > 1/(1+mu)`` where ``mu`` "
                    "is contained in set (0, 1]. "
                    "Default = 2.0."))
    ### Trust region update/ratio test parameters
    CONFIG.declare(
        'radius_update_param_gamma_c',
        ConfigValue(
            default=0.5,
            domain=In(NumericRange(0, 1, 0, (False, False))),
            description="Lower trust region update parameter ``gamma_c``. "
            "Default = 0.5."))
    CONFIG.declare(
        'radius_update_param_gamma_e',
        ConfigValue(
            default=2.5,
            domain=In(NumericRange(1, None, 0)),
            description="Upper trust region update parameter ``gamma_e``. "
            "Default = 2.5."))
    CONFIG.declare(
        'ratio_test_param_eta_1',
        ConfigValue(default=0.05,
                    domain=In(NumericRange(0, 1, 0, (False, False))),
                    description="Lower ratio test parameter ``eta_1``. "
                    "Must satisfy: ``0 < eta_1 <= eta_2 < 1``. "
                    "Default = 0.05."))
    CONFIG.declare(
        'ratio_test_param_eta_2',
        ConfigValue(default=0.2,
                    domain=In(NumericRange(0, 1, 0, (False, False))),
                    description="Lower ratio test parameter ``eta_2``. "
                    "Must satisfy: ``0 < eta_1 <= eta_2 < 1``. "
                    "Default = 0.2."))
    ### Filter
    CONFIG.declare(
        'maximum_feasibility',
        ConfigValue(
            default=50.0,
            domain=PositiveFloat,
            description="Maximum allowable feasibility measure ``theta_max``. "
            "Parameter for use in filter method."
            "Default = 50.0."))
    CONFIG.declare(
        'param_filter_gamma_theta',
        ConfigValue(
            default=0.01,
            domain=In(NumericRange(0, 1, 0, (False, False))),
            description="Fixed filter parameter ``gamma_theta`` within (0, 1). "
            "Default = 0.01"))
    CONFIG.declare(
        'param_filter_gamma_f',
        ConfigValue(
            default=0.01,
            domain=In(NumericRange(0, 1, 0, (False, False))),
            description="Fixed filter parameter ``gamma_f`` within (0, 1). "
            "Default = 0.01"))

    return CONFIG
Esempio n. 5
0
def pyros_config():
    CONFIG = ConfigDict('PyROS')

    # ================================================
    # === Options common to all solvers
    # ================================================
    CONFIG.declare(
        'time_limit',
        ConfigValue(
            default=None,
            domain=NonNegativeFloat,
            description="Optional. Default = None. "
            "Total allotted time for the execution of the PyROS solver in seconds "
            "(includes time spent in sub-solvers). 'None' is no time limit."))
    CONFIG.declare(
        'keepfiles',
        ConfigValue(
            default=False,
            domain=bool,
            description=
            "Optional. Default = False. Whether or not to write files of sub-problems for use in debugging. "
            "Must be paired with a writable directory supplied via ``subproblem_file_directory``."
        ))
    CONFIG.declare(
        'tee',
        ConfigValue(
            default=False,
            domain=bool,
            description=
            "Optional. Default = False. Sets the ``tee`` for all sub-solvers utilized."
        ))
    CONFIG.declare(
        'load_solution',
        ConfigValue(
            default=True,
            domain=bool,
            description="Optional. Default = True. "
            "Whether or not to load the final solution of PyROS into the model object."
        ))

    # ================================================
    # === Required User Inputs
    # ================================================
    CONFIG.declare(
        "first_stage_variables",
        ConfigValue(
            default=[],
            domain=InputDataStandardizer(Var, _VarData),
            description=
            "Required. List of ``Var`` objects referenced in ``model`` representing the design variables."
        ))
    CONFIG.declare(
        "second_stage_variables",
        ConfigValue(
            default=[],
            domain=InputDataStandardizer(Var, _VarData),
            description=
            "Required. List of ``Var`` referenced in ``model`` representing the control variables."
        ))
    CONFIG.declare(
        "uncertain_params",
        ConfigValue(
            default=[],
            domain=InputDataStandardizer(Param, _ParamData),
            description=
            "Required. List of ``Param`` referenced in ``model`` representing the uncertain parameters. MUST be ``mutable``. "
            "Assumes entries are provided in consistent order with the entries of 'nominal_uncertain_param_vals' input."
        ))
    CONFIG.declare(
        "uncertainty_set",
        ConfigValue(
            default=None,
            domain=uncertainty_sets,
            description=
            "Required. ``UncertaintySet`` object representing the uncertainty space "
            "that the final solutions will be robust against."))
    CONFIG.declare(
        "local_solver",
        ConfigValue(
            default=None,
            domain=SolverResolvable(),
            description=
            "Required. ``Solver`` object to utilize as the primary local NLP solver."
        ))
    CONFIG.declare(
        "global_solver",
        ConfigValue(
            default=None,
            domain=SolverResolvable(),
            description=
            "Required. ``Solver`` object to utilize as the primary global NLP solver."
        ))
    # ================================================
    # === Optional User Inputs
    # ================================================
    CONFIG.declare(
        "objective_focus",
        ConfigValue(
            default=ObjectiveType.nominal,
            domain=ValidEnum(ObjectiveType),
            description=
            "Optional. Default = ``ObjectiveType.nominal``. Choice of objective function to optimize in the master problems. "
            "Choices are: ``ObjectiveType.worst_case``, ``ObjectiveType.nominal``. See Note for details."
        ))
    CONFIG.declare(
        "nominal_uncertain_param_vals",
        ConfigValue(
            default=[],
            domain=list,
            description=
            "Optional. Default = deterministic model ``Param`` values. List of nominal values for all uncertain parameters. "
            "Assumes entries are provided in consistent order with the entries of ``uncertain_params`` input."
        ))
    CONFIG.declare(
        "decision_rule_order",
        ConfigValue(
            default=0,
            domain=In([0, 1, 2]),
            description=
            "Optional. Default = 0. Order of decision rule functions for handling second-stage variable recourse. "
            "Choices are: '0' for constant recourse (a.k.a. static approximation), '1' for affine recourse "
            "(a.k.a. affine decision rules), '2' for quadratic recourse."))
    CONFIG.declare(
        "solve_master_globally",
        ConfigValue(
            default=False,
            domain=bool,
            description=
            "Optional. Default = False. 'True' for the master problems to be solved with the user-supplied global solver(s); "
            "or 'False' for the master problems to be solved with the user-supplied local solver(s). "
        ))
    CONFIG.declare(
        "max_iter",
        ConfigValue(
            default=-1,
            domain=PositiveIntOrMinusOne,
            description=
            "Optional. Default = -1. Iteration limit for the GRCS algorithm. '-1' is no iteration limit."
        ))
    CONFIG.declare(
        "robust_feasibility_tolerance",
        ConfigValue(
            default=1e-4,
            domain=NonNegativeFloat,
            description=
            "Optional. Default = 1e-4. Relative tolerance for assessing robust feasibility violation during separation phase."
        ))
    CONFIG.declare(
        "separation_priority_order",
        ConfigValue(
            default={},
            domain=dict,
            description=
            "Optional. Default = {}. Dictionary mapping inequality constraint names to positive integer priorities for separation. "
            "Constraints not referenced in the dictionary assume a priority of 0 (lowest priority)."
        ))
    CONFIG.declare(
        "progress_logger",
        ConfigValue(
            default="pyomo.contrib.pyros",
            domain=a_logger,
            description=
            "Optional. Default = \"pyomo.contrib.pyros\". The logger object to use for reporting."
        ))
    CONFIG.declare(
        "backup_local_solvers",
        ConfigValue(
            default=[],
            domain=SolverResolvable(),
            description=
            "Optional. Default = []. List of additional ``Solver`` objects to utilize as backup "
            "whenever primary local NLP solver fails to identify solution to a sub-problem."
        ))
    CONFIG.declare(
        "backup_global_solvers",
        ConfigValue(
            default=[],
            domain=SolverResolvable(),
            description=
            "Optional. Default = []. List of additional ``Solver`` objects to utilize as backup "
            "whenever primary global NLP solver fails to identify solution to a sub-problem."
        ))
    CONFIG.declare(
        "subproblem_file_directory",
        ConfigValue(
            default=None,
            domain=str,
            description=
            "Optional. Path to a directory where subproblem files and "
            "logs will be written in the case that a subproblem fails to solve."
        ))
    # ================================================
    # === Advanced Options
    # ================================================
    CONFIG.declare(
        "bypass_local_separation",
        ConfigValue(
            default=False,
            domain=bool,
            description=
            "This is an advanced option. Default = False. 'True' to only use global solver(s) during separation; "
            "'False' to use local solver(s) at intermediate separations, "
            "using global solver(s) only before termination to certify robust feasibility. "
        ))
    CONFIG.declare(
        "bypass_global_separation",
        ConfigValue(
            default=False,
            domain=bool,
            description=
            "This is an advanced option. Default = False. 'True' to only use local solver(s) during separation; "
            "however, robustness of the final result will not be guaranteed. Use to expedite PyROS run when "
            "global solver(s) cannot (efficiently) solve separation problems.")
    )
    CONFIG.declare(
        "p_robustness",
        ConfigValue(
            default={},
            domain=dict,
            description=
            "This is an advanced option. Default = {}. Whether or not to add p-robustness constraints to the master problems. "
            "If the dictionary is empty (default), then p-robustness constraints are not added. "
            "See Note for how to specify arguments."))

    return CONFIG
Esempio n. 6
0
class _DynamicBlockData(_BlockData):
    """ This class adds methods and data structures that are useful
    for working with dynamic models. These include methods for
    initialization and references to time-indexed variables.
    """
    # TODO: This class should probably give the option to clone
    # the user's model.

    logger = idaeslog.getLogger('nmpc')

    CONFIG = ConfigDict()

    CONFIG.declare(
        'tee',
        ConfigValue(
            default=True,
            domain=bool,
            doc="tee option for embedded solver calls",
        ))

    CONFIG.declare(
        'outlvl',
        ConfigValue(
            default=idaeslog.INFO,
            doc="Output level for IDAES logger",
        ))

    def _construct(self):
        """ Generates time-indexed references and categorizes them. """
        model = self.mod
        time = self.time
        inputs = self._inputs
        try:
            measurements = self._measurements
        except AttributeError:
            measurements = self._measurements = None

        # TODO: Give the user the option to provide their own
        # category_dict (they know the structure of their model
        # better than I do...)
        scalar_vars, dae_vars = flatten_dae_components(
            model,
            time,
            ctype=Var,
        )
        self.scalar_vars = scalar_vars
        self.dae_vars = dae_vars
        category_dict = categorize_dae_variables(
            dae_vars,
            time,
            inputs,
            measurements=measurements,
        )
        self.category_dict = category_dict

        self._add_category_blocks()
        self._add_category_references()

        self.differential_vars = category_dict[VC.DIFFERENTIAL]
        self.algebraic_vars = category_dict[VC.ALGEBRAIC]
        self.derivative_vars = category_dict[VC.DERIVATIVE]
        self.input_vars = category_dict[VC.INPUT]
        self.fixed_vars = category_dict[VC.FIXED]

        self.measurement_vars = category_dict.pop(VC.MEASUREMENT)
        # The categories in category_dict now form a partition of the
        # time-indexed variables. This is necessary to have a well-defined
        # vardata map, which maps each vardata to a unique component indexed
        # only by time.

        # Maps each vardata (of a time-indexed var) to the NmpcVar
        # that contains it.
        self.vardata_map = ComponentMap((var[t], var)
                                        for varlist in category_dict.values()
                                        for var in varlist for t in time)
        # NOTE: looking up var[t] instead of iterating over values()
        # appears to be ~ 5x faster

        # These should be overridden by a call to `set_sample_time`
        # The defaults assume that the entire model is one sample.
        self.sample_points = [time.first(), time.last()]
        self.sample_point_indices = [1, len(time)]

    _var_name = 'var'
    _block_suffix = '_BLOCK'
    _set_suffix = '_SET'

    @classmethod
    def get_category_block_name(cls, categ):
        """ Gets block name from name of enum entry """
        categ_name = str(categ).split('.')[1]
        return categ_name + cls._block_suffix

    @classmethod
    def get_category_set_name(cls, categ):
        """ Gets set name from name of enum entry """
        categ_name = str(categ).split('.')[1]
        return categ_name + cls._set_suffix

    def _add_category_blocks(self):
        """ Adds an indexed block for each category of variable and
        attach a reference to each variable to one of the BlockDatas.
        """
        category_dict = self.category_dict
        var_name = self._var_name
        for categ, varlist in category_dict.items():
            # These names are e.g. 'DIFFERENTIAL_BLOCK', 'DIFFERENTIAL_SET'
            # They serve as a way to access all the "differential variables"
            block_name = self.get_category_block_name(categ)
            set_name = self.get_category_set_name(categ)
            set_range = range(len(varlist))

            # Construct a set that indexes, eg, the "differential variables"
            category_set = Set(initialize=set_range)
            self.add_component(set_name, category_set)

            # Construct an IndexedBlock, each data object of which
            # will contain a single reference-to-timeslice of that
            # category, and with the corresponding custom ctype
            category_block = Block(category_set)
            self.add_component(block_name, category_block)

            # Don't want these blocks sent to any solver.
            category_block.deactivate()

            for i, var in enumerate(varlist):
                # Add reference-to-timeslices to new blocks:
                category_block[i].add_component(var_name, var)
                # These vars were created by the categorizer
                # and have custom ctypes.

    _vectors_name = 'vectors'

    def _add_category_references(self):
        """ Create a "time-indexed vector" for each category of variables. """
        category_dict = self.category_dict

        # Add a deactivated block to store all my `_NmpcVector`s
        # These be will vars, named by category, indexed by the index
        # into the list of that category and by time. E.g.
        # self.vectors.differential
        self.add_component(self._vectors_name, Block())
        self.vectors.deactivate()

        for categ in category_dict:
            ctype = CATEGORY_TYPE_MAP[categ]
            # Get the block that holds this category of var,
            # and the name of the attribute that holds the
            # custom-ctype var (this attribute is the same
            # for all blocks).
            block_name = self.get_category_block_name(categ)
            var_name = self._var_name

            # Get a slice of the block, e.g. self.DIFFERENTIAL_BLOCK[:]
            _slice = getattr(self, block_name)[:]
            #_slice = self.__getattribute__(block_name)[:]
            # Why does this work when self.__getattr__(block_name) does not?
            # __getattribute__ appears to work just fine...

            # Get a slice of the block and var, e.g.
            # self.DIFFERENTIAL_BLOCK[:].var[:]
            _slice = getattr(_slice, var_name)[:]

            # Add a reference to this slice to the `vectors` block.
            # This will be, e.g. `self.vectors.differential` and
            # can be accessed with its two indices, e.g.
            # `self.vectors.differential[i,t0]`
            # to get the "ith coordinate" of the vector of differential
            # variables at time t0.
            self.vectors.add_component(
                ctype._attr,
                # ^ I store the name I want this attribute to have,
                # e.g. 'differential', on the custom ctype.
                Reference(_slice, ctype=_NmpcVector),
            )

    # Time is added in DynamicBlock.construct but this is nice if the user wants
    # to add time in a rule without a long messy line of super().__setattr__.
    def add_time(self):
        # Do this because I can't add a reference to a set
        super(_BlockData, self).__setattr__('time', self.time)

    def set_sample_time(self, sample_time, tolerance=1e-8):
        """ Validates and sets sample time """
        self.validate_sample_time(sample_time, tolerance)
        self.sample_time = sample_time

    def validate_sample_time(self, sample_time, tolerance=1e-8):
        """Makes sure sample points, or integer multiple of sample time-offsets
        from time.first(), lie on finite element boundaries, and that the 
        horizon of each model is an integer multiple of sample time. Assembles 
        a list of sample points and a dictionary mapping sample points to the 
        number of finite elements in the preceding sampling period, and adds 
        them as attributes to _NMPC_NAMESPACE.

        Args:
            sample_time: Sample time to check
            tolerance: Tolerance within which time points must be integer
                       multiples of sample time

        """
        time = self.time
        horizon_length = time.last() - time.first()
        n_t = len(time)

        # TODO: This should probably be a DAE utility
        min_spacing = horizon_length
        for t in time:
            if t == time.first():
                continue
            prev = time.prev(t)
            if t - prev < min_spacing:
                min_spacing = t - prev
        # Sanity check:
        assert min_spacing > 0
        # Required so only one point can satisfy equality to tolerance
        if tolerance >= min_spacing / 2:
            raise ValueError(
                'ContinuousSet tolerance is larger than half the minimum '
                'spacing. An element of this set will not necessarily be '
                'unique within this tolerance.')

        off_by = abs(remainder(horizon_length, sample_time))
        if off_by > tolerance:
            raise ValueError('Sampling time must be an integer divider of '
                             'horizon length within tolerance %f' % tolerance)
        n_samples = round(horizon_length / sample_time)
        self.samples_per_horizon = n_samples

        finite_elements = time.get_finite_elements()
        fe_set = set(finite_elements)
        finite_element_indices = [
            i for i in range(1, n_t + 1) if time[i] in fe_set
        ]
        sample_points = [time.first()]
        sample_indices = [1]  # Indices of sample points with in time set
        sample_no = 1
        fe_per = 0
        fe_per_sample_dict = {}
        for i, t in zip(finite_element_indices, finite_elements):
            if t == time.first():
                continue
            fe_per += 1
            time_since = t - time.first()
            sp = sample_no * sample_time
            diff = abs(sp - time_since)
            if diff < tolerance:
                sample_points.append(t)
                sample_indices.append(i)
                sample_no += 1
                fe_per_sample_dict[sample_no] = fe_per
                fe_per = 0
            if time_since > sp:
                raise ValueError('Could not find a time point for the %ith '
                                 'sample point' % sample_no)
        assert len(sample_points) == n_samples + 1
        self.fe_per_sample = fe_per_sample_dict
        self.sample_points = sample_points
        self.sample_point_indices = sample_indices

    def initialize_sample_to_setpoint(
            self,
            sample_idx,
            ctype=(DiffVar, AlgVar, InputVar, DerivVar),
    ):
        """ Set values to setpoint values for variables of the
        specified variable ctypes in the specified sample.
        """
        time = self.time
        sample_point_indices = self.sample_point_indices
        i_0 = sample_point_indices[sample_idx - 1]
        i_s = sample_point_indices[sample_idx]
        for var in self.component_objects(ctype):
            # `type(var)` is a subclass of `NmpcVar`, so I can
            # access the `setpoint` attribute.
            #
            # Would like:
            # var[t1:ts].set_value(var.setpoint)
            for i in range(i_0 + 1, i_s + 1):
                # Want to exclude first time point of sample,
                # but include last time point of sample.
                t = time[i]
                var[t].set_value(var.setpoint)

    def initialize_sample_to_initial(
            self,
            sample_idx,
            ctype=(DiffVar, AlgVar, DerivVar),
    ):
        """ Set values to initial values for variables of the
        specified variable ctypes in the specified sample.
        """
        time = self.time
        sample_point_indices = self.sample_point_indices
        i_0 = sample_point_indices[sample_idx - 1]
        i_s = sample_point_indices[sample_idx]
        t0 = time[i_0]
        for var in self.component_objects(ctype):
            # Would be nice if I could use a slice with
            # start/stop indices to make this more concise.
            for i in range(i_0 + 1, i_s + 1):
                t = time[i]
                var[t].set_value(var[t0].value)

    def initialize_to_setpoint(
            self,
            ctype=(DiffVar, AlgVar, InputVar, DerivVar),
    ):
        """ Sets values to setpoint values for specified variable
        ctypes for all time points.
        """
        # There should be negligible overhead to initializing
        # in many small loops as opposed to one big loop here.
        for i in range(len(self.sample_points)):
            self.initialize_sample_to_setpoint(i, ctype=ctype)

    def initialize_to_initial_conditions(
            self,
            ctype=(DiffVar, AlgVar, DerivVar),
    ):
        """ Sets values to initial values for specified variable
        ctypes for all time points.
        """
        # There should be negligible overhead to initializing
        # in many small loops as opposed to one big loop here.
        for i in range(len(self.sample_points)):
            self.initialize_sample_to_initial(i, ctype=ctype)

    def initialize_by_solving_elements(self, solver, **kwargs):
        """ Solve the square problem with fixed inputs in each
        of the time finite elements individually. This can be
        thought of as a time integration.
        """
        strip_var_bounds = kwargs.pop('strip_var_bounds', True)
        input_option = kwargs.pop('input_option', InputOption.CURRENT)
        config = self.CONFIG(kwargs)
        square_solve_context = SquareSolveContext(
            self,
            strip_var_bounds=strip_var_bounds,
            input_option=input_option,
        )
        model = self.mod
        time = self.time
        # There is a significant amount of overhead when calling
        # initialize_by_element_in_range multiple times, so
        # this method does not call `initialize_samples_by_element`
        # in a loop.
        with square_solve_context as sqs:
            initialize_by_element_in_range(
                model,
                time,
                time.first(),
                time.last(),
                dae_vars=self.dae_vars,
                time_linking_vars=list(self.differential_vars[:]),
                outlvl=config.outlvl,
                solver=solver,
            )

    def initialize_samples_by_element(self, samples, solver, **kwargs):
        """ Solve the square problem with fixed inputs for the specified samples
        """
        # TODO: ConfigBlock for this class
        strip_var_bounds = kwargs.pop('strip_var_bounds', True)
        input_option = kwargs.pop('input_option', InputOption.CURRENT)
        config = self.CONFIG(kwargs)

        if type(samples) not in {list, tuple}:
            samples = (samples, )

        # Create a context manager that will temporarily strip bounds
        # and fix inputs, preparing the model for a "square solve."
        square_solve_context = SquareSolveContext(
            self,
            samples=samples,
            strip_var_bounds=strip_var_bounds,
            input_option=input_option,
        )
        sample_points = self.sample_points
        model = self.mod
        time = self.time
        with square_solve_context as sqs:
            for s in samples:
                t0 = sample_points[s - 1]
                t1 = sample_points[s]
                # Really I would like an `ElementInitializer` context manager
                # class that deactivates the model once, then allows me to
                # activate the elements I want to solve one at a time.
                # This would allow me to not repeat so much work when
                # initializing multiple samples.
                initialize_by_element_in_range(
                    model,
                    time,
                    t0,
                    t1,
                    dae_vars=self.dae_vars,
                    time_linking_vars=list(self.differential_vars[:]),
                    outlvl=config.outlvl,
                    solver=solver,
                )

    def set_variance(self, variance_list):
        """ Set variance for corresponding NmpcVars to the values provided

        Arguments:
            variance_list: List of vardata, value tuples. The vardatas
                           correspond to time-indexed references, and values
                           are the variances.
        """
        t0 = self.time.first()
        variance_map = ComponentMap(variance_list)
        for var, val in variance_list:
            nmpc_var = self.vardata_map[var]
            nmpc_var.variance = val
        # MeasurementVars will not have their variance set since they are
        # not mapped to in vardata_map
        for var in self.measurement_vars:
            if var[t0] in variance_map:
                var.variance = variance_map[var[t0]]

    def generate_inputs_at_time(self, t):
        for val in self.vectors.input[:, t].value:
            yield val

    def generate_measurements_at_time(self, t):
        for var in self.measurement_vars:
            yield var[t].value

    def inject_inputs(self, inputs):
        # To simulate computational delay, this function would
        # need an argument for the start time of inputs.
        for var, val in zip(self.input_vars, inputs):
            # Would like:
            # self.input_vars[:,:].fix(inputs)
            # This is an example of setting a matrix from a vector.
            # Could even aspire towards:
            # self.input_vars[:,t0:t1].fix(inputs[t1])
            var[:].fix(val)

    def load_measurements(self, measured):
        t0 = self.time.first()
        # Want: self.measured_vars[:,t0].fix(measured)
        for var, val in zip(self.measurement_vars, measured):
            var[t0].fix(val)

    def advance_by_time(
        self,
        t_shift,
        ctype=(DiffVar, DerivVar, AlgVar, InputVar, FixedVar),
        # Fixed variables are included as I expect disturbances
        # should shift in time as well.
        tolerance=1e-8,
    ):
        """ Set values for the variables of the specified ctypes
        to their values `t_shift` in the future.
        """
        time = self.time
        # The outer loop is over time so we don't have to call
        # `find_nearest_index` for every variable.
        # I am assuming that `find_nearest_index` is slower than
        # accessing `component_objects`
        for t in time:
            ts = t + t_shift
            idx = time.find_nearest_index(ts, tolerance)
            if idx is None:
                # t + sample_time is outside the model's "horizon"
                continue
            ts = time[idx]
            for var in self.component_objects(ctype):
                var[t].set_value(var[ts].value)

    def advance_one_sample(
        self,
        ctype=(DiffVar, DerivVar, AlgVar, InputVar, FixedVar),
        tolerance=1e-8,
    ):
        """ Set values for the variables of the specified ctypes
        to their values one sample time in the future.
        """
        sample_time = self.sample_time
        self.advance_by_time(
            sample_time,
            ctype=ctype,
            tolerance=tolerance,
        )

    def generate_time_in_sample(
        self,
        ts,
        t0=None,
        include_t0=False,
        tolerance=1e-8,
    ):
        """ Generate time points between the provided time point
        and one sample time in the past.
        """
        # TODO: Need to address the question of whether I want users
        # passing around time points or the integer index of samples.
        time = self.time
        idx_s = time.find_nearest_index(ts, tolerance=tolerance)
        ts = time[idx_s]
        if t0 is None:
            t0 = ts - self.sample_time
        idx_0 = time.find_nearest_index(t0, tolerance=tolerance)
        idx_start = idx_0 if include_t0 else idx_0 + 1
        for i in range(idx_start, idx_s + 1):
            # Don't want to include first point in sample
            yield time[i]

    def get_data_from_sample(
        self,
        ts,
        variables=(
            VC.DIFFERENTIAL,
            VC.INPUT,
        ),
        tolerance=1e-8,
        include_t0=False,
    ):
        """ Creates an `OrderedDict` that maps the time-indexed reference
        of each variable provided to a list of its values over the sample
        preceding the specified time point.
        """
        time = self.time
        sample_time = self.sample_time
        category_dict = self.category_dict
        vardata_map = self.vardata_map

        data = OrderedDict()
        queue = list(variables)
        for var in queue:
            if type(var) is VC:
                category = var
                varlist = category_dict[category]
                queue.extend(var[ts] for var in varlist)
                continue
            _slice = vardata_map[var]
            cuid = ComponentUID(_slice.referent)
            if include_t0:
                i0 = time.find_nearest_index(ts - sample_time,
                                             tolerance=tolerance)
                t0 = time[i0]
                data[cuid] = [_slice[t0].value]
            else:
                data[cuid] = []
            data[cuid].extend(
                _slice[t].value
                for t in self.generate_time_in_sample(ts, tolerance=tolerance))

        return data

    def add_ipopt_suffixes(self):
        """ Adds suffixes for communicating dual variables with IPOPT """
        # Maybe there should be some helper class to do solver-specific
        # stuff like this...
        self.ipopt_zL_out = Suffix(direction=Suffix.IMPORT)
        self.ipopt_zU_out = Suffix(direction=Suffix.IMPORT)

        self.ipopt_zL_in = Suffix(direction=Suffix.EXPORT)
        self.ipopt_zU_in = Suffix(direction=Suffix.EXPORT)

        self.dual = Suffix(direction=Suffix.IMPORT_EXPORT)

    def update_ipopt_multipliers(self):
        self.ipopt_zL_in.update(self.ipopt_zL_out)
        self.ipopt_zU_in.update(self.ipopt_zU_out)

    def advance_ipopt_multipliers(
            self,
            t_shift,
            ctype=(
                DiffVar,
                AlgVar,
                InputVar,
            ),
            tolerance=1e-8,
    ):
        """ Set the values of bound multipliers to the corresponding
        values a time `t_shift` in the future.
        """
        zL = self.ipopt_zL_in
        zU = self.ipopt_zU_in
        time = self.time
        # The outer loop is over time so we don't have to call
        # `find_nearest_index` for every variable.
        # I am assuming that `find_nearest_index` is slower than
        # accessing `component_objects`
        for t in time:
            ts = t + t_shift
            idx = time.find_nearest_index(ts, tolerance)
            if idx is None:
                # t + sample_time is outside the model's "horizon"
                continue
            ts = time[idx]
            for var in self.component_objects(ctype):
                if var[t] in zL and var[ts] in zL:
                    zL[var[t]] = zL[var[ts]]
                if var[t] in zU and var[ts] in zU:
                    zU[var[t]] = zU[var[ts]]

    def advance_ipopt_multipliers_one_sample(
            self,
            ctype=(
                DiffVar,
                AlgVar,
                InputVar,
            ),
            tolerance=1e-8,
    ):
        """ Set the values of bound multipliers to the corresponding
        values one sample time in the future.
        """
        sample_time = self.sample_time
        self.advance_ipopt_multipliers(
            sample_time,
            ctype=ctype,
            tolerance=tolerance,
        )
Esempio n. 7
0
 def __setattr__(self, name, value):
     if name in PrescientConfig.__slots__:
         super(ConfigDict, self).__setattr__(name, value)
     else:
         ConfigDict.__setattr__(self, name, value)
Esempio n. 8
0
    def __init__(self):
        ##########################
        #   CHAIN ONLY OPTIONS   #
        ##########################
        super().__init__()

        self.plugin_context = PluginRegistrationContext()

        def register_plugin(key, value):
            ''' Handle intial plugin setup
            Arguments
            ---------
            key - str
                The alias for this plugin in the configuration
            value - str, module, or dict
                If a string, the name of the python module or the python file for this plugin.
                If a module, the plugin's python module.
                If a dict, the initial values for any properties listed in the dict. One of the
                dict's keys MUST be 'module', and must be either a module, a string identifying
                the module, or a string identifying the module's *.py file.
            '''
            # Defaults, if value is not a dict
            mod_spec = value
            init_values = {}

            # Override defaults if value is a dict
            if isinstance(value, dict):
                if 'module' not in value:
                    raise RuntimeError(
                        f"Attempt to register '{key}' plugin without a module attribute"
                    )
                mod_spec = value['module']
                init_values = value.copy()
                del (init_values['module'])

            domain = Module()
            module = domain(mod_spec)

            c = module.get_configuration(key)
            c.declare('module', ConfigValue(module, domain=domain))
            MarkImmutable(c.get('module'))
            c.set_value(init_values)
            return c

        # We put this first so that plugins will be registered before any other
        # options are applied, which lets them add custom command line options
        # before they are potentially used.
        self.declare(
            "plugin",
            ConfigDict(
                implicit=True,
                implicit_domain=DynamicImplicitDomain(register_plugin),
                description=
                "Settings for python modules that extends prescient behavior",
            ))

        self.declare(
            "config_file",
            ConfigValue(
                domain=Path(),
                description="A file holding configuration options. If specified,"
                " the options in the config file are applied first, then"
                " overridden by any matching command line arguments.")
        ).declare_as_argument(metavar="<filename>")

        self.declare(
            "start_date",
            ConfigValue(
                domain=_StartDate,
                default="01-01-2020",
                description=
                "The start date for the simulation - specified in MM-DD-YYYY format. "
                "Defaults to 01-01-2020.",
            )).declare_as_argument()

        self.declare(
            "num_days",
            ConfigValue(
                domain=PositiveInt,
                default=7,
                description="The number of days to simulate",
            )).declare_as_argument()

        self.declare(
            "output_directory",
            ConfigValue(
                domain=Path(),
                default="outdir",
                description=
                "The root directory to which all of the generated simulation files and "
                "associated data are written.",
            )).declare_as_argument()

        self.declare(
            "data_provider",
            ConfigValue(
                domain=Module(),
                default=data_provider_factory,
                description=
                "Python module that supplies a data provider implementation")
        ).declare_as_argument()

        #############################
        #  PRESCIENT ONLY OPTIONS   #
        #############################

        # # PRESCIENT_INPUT_OPTIONS

        self.declare(
            "data_path",
            ConfigValue(
                domain=Path(),
                default="input_data",
                description="Specifies the file or directory to pull data from",
            )).declare_as_argument('--data-path', '--data-directory')

        self.declare(
            "input_format",
            ConfigValue(
                domain=_InEnumStr(InputFormats),
                default="dat",
                description="Indicate the format input data is in",
            )).declare_as_argument()

        self.declare(
            "simulator_plugin",
            ConfigValue(
                domain=Path(),
                default=None,
                description=
                "If the user has an alternative methods for the various simulator functions,"
                " they should be specified here, e.g., my_special_plugin.py.",
            )).declare_as_argument()

        self.declare(
            "deterministic_ruc_solver_plugin",
            ConfigValue(
                domain=Path(),
                default=None,
                description=
                "If the user has an alternative method to solve the deterministic RUCs,"
                " it should be specified here, e.g., my_special_plugin.py."
                " NOTE: This option is ignored if --simulator-plugin is used.")
        ).declare_as_argument()

        self.declare(
            "run_ruc_with_next_day_data",
            ConfigValue(
                domain=bool,
                default=False,
                description=
                "When running the RUC, use the data for the next day "
                "for tailing hours.",
            )).declare_as_argument()

        self.declare(
            "run_sced_with_persistent_forecast_errors",
            ConfigValue(
                domain=bool,
                default=False,
                description=
                "Create all SCED instances assuming persistent forecast error, "
                "instead of the default prescience.",
            )).declare_as_argument()

        self.declare(
            "ruc_prescience_hour",
            ConfigValue(
                domain=NonNegativeInt,
                default=0,
                description=
                "Hour before which linear blending of forecast and actuals "
                "takes place when running deterministic ruc. A value of "
                "0 indicates we always take the forecast. Default is 0.",
            )).declare_as_argument()

        self.declare(
            "ruc_execution_hour",
            ConfigValue(
                domain=int,
                default=16,
                description="Specifies when the the RUC process is executed. "
                "Negative values indicate time before horizon, positive after.",
            )).declare_as_argument()

        self.declare(
            "ruc_every_hours",
            ConfigValue(
                domain=PositiveInt,
                default=24,
                description=
                "Specifies at which hourly interval the RUC process is executed. "
                "Default is 24. Should be a divisor of 24.",
            )).declare_as_argument()

        self.declare(
            "ruc_network_type",
            ConfigValue(
                domain=_InEnumStr(NetworkType),
                default="ptdf",
                description=
                "Specifies the type of network representation to use in RUC processes. Choices are "
                "ptdf   -- power transfer distribution factor representation."
                "btheta -- b-theta representation."
                "Default is ptdf.",
            )).declare_as_argument()

        self.declare(
            "ruc_slack_type",
            ConfigValue(
                domain=_InEnumStr(SlackType),
                default="every-bus",
                description=
                "Specifies the type of slack variables to use in RUC processes. Choices are "
                "every-bus            -- slack variables at every system bus."
                "ref-bus-and-branches -- slack variables at only reference bus and each system branch."
                "Default is every-bus.",
            )).declare_as_argument()

        self.declare(
            "ruc_horizon",
            ConfigValue(
                domain=PositiveInt,
                default=48,
                description=
                "The number of hours for which the reliability unit commitment is executed. "
                "Must be <= 48 hours and >= --ruc-every-hours. "
                "Default is 48.",
            )).declare_as_argument()

        self.declare(
            "sced_horizon",
            ConfigValue(
                domain=PositiveInt,
                default=1,
                description="Specifies the number of time periods "
                "in the look-ahead horizon for each SCED. "
                "Must be at least 1.",
            )).declare_as_argument()

        self.declare(
            "sced_frequency_minutes",
            ConfigValue(
                domain=PositiveInt,
                default=60,
                description=
                "Specifies how often a SCED will be run, in minutes. "
                "Must divide evenly into 60, or be a multiple of 60.",
            )).declare_as_argument()

        self.declare(
            "sced_network_type",
            ConfigValue(
                domain=_InEnumStr(NetworkType),
                default="ptdf",
                description=
                "Specifies the type of network representation to use in SCED processes. Choices are "
                "ptdf   -- power transfer distribution factor representation."
                "btheta -- b-theta representation."
                "Default is ptdf.",
            )).declare_as_argument()

        self.declare(
            "sced_slack_type",
            ConfigValue(
                domain=_InEnumStr(SlackType),
                default="every-bus",
                description=
                "Specifies the type of slack variables to use in SCED processes. Choices are "
                "every-bus            -- slack variables at every system bus."
                "ref-bus-and-branches -- slack variables at only reference bus and each system branch."
                "Default is every-bus.",
            )).declare_as_argument()

        self.declare(
            "enforce_sced_shutdown_ramprate",
            ConfigValue(
                domain=bool,
                default=False,
                description=
                "Enforces shutdown ramp-rate constraints in the SCED. "
                "Enabling this options requires a long SCED look-ahead "
                "(at least an hour) to ensure the shutdown ramp-rate "
                "constraints can be statisfied.",
            )).declare_as_argument()

        self.declare(
            "no_startup_shutdown_curves",
            ConfigValue(
                domain=bool,
                default=False,
                description=
                "For thermal generators, do not infer startup/shutdown "
                "ramping curves when starting-up and shutting-down.",
            )).declare_as_argument()

        self.declare(
            "simulate_out_of_sample",
            ConfigValue(
                domain=bool,
                default=False,
                description=
                "Execute the simulation using an out-of-sample scenario, "
                "specified in Scenario_actuals.dat files in the daily input directories. "
                "Defaults to False, "
                "indicating that either the expected-value scenario will be used "
                "(for deterministic RUC) or a random scenario sample will be used "
                "(for stochastic RUC).",
            )).declare_as_argument()

        self.declare(
            "reserve_factor",
            ConfigValue(
                domain=NonNegativeFloat,
                default=0.0,
                description=
                "The reserve factor, expressed as a constant fraction of demand, "
                "for spinning reserves at each time period of the simulation. "
                "Applies to both stochastic RUC and deterministic SCED models.",
            )).declare_as_argument()

        self.declare(
            "compute_market_settlements",
            ConfigValue(
                domain=bool,
                default=False,
                description=
                "Solves a day-ahead as well as real-time market and reports "
                "the daily profit for each generator based on the computed prices.",
            )).declare_as_argument()

        self.declare(
            "price_threshold",
            ConfigValue(
                domain=PositiveFloat,
                default=10000.,
                description="Maximum possible value the price can take "
                "If the price exceeds this value due to Load Mismatch, then "
                "it is set to this value.",
            )).declare_as_argument()

        self.declare(
            "reserve_price_threshold",
            ConfigValue(
                domain=PositiveFloat,
                default=1000.,
                description="Maximum possible value the reserve price can take "
                "If the reserve price exceeds this value, then "
                "it is set to this value.",
            )).declare_as_argument()

        # # PRESCIENT_SOLVER_OPTIONS

        self.declare(
            "sced_solver",
            ConfigValue(
                domain=In(prescient_solvers),
                default="cbc",
                description="The name of the Pyomo solver for SCEDs",
            )).declare_as_argument()

        self.declare(
            "deterministic_ruc_solver",
            ConfigValue(
                domain=In(prescient_solvers),
                default="cbc",
                description="The name of the Pyomo solver for RUCs",
            )).declare_as_argument()

        self.declare(
            "sced_solver_options",
            ConfigValue(
                domain=_SolverOptions,
                default=None,
                description="Solver options applied to all SCED solves",
            )).declare_as_argument()

        self.declare(
            "deterministic_ruc_solver_options",
            ConfigValue(
                domain=_SolverOptions,
                default=None,
                description=
                "Solver options applied to all deterministic RUC solves",
            )).declare_as_argument()

        self.declare(
            "write_deterministic_ruc_instances",
            ConfigValue(
                domain=bool,
                default=False,
                description="Write all individual RUC instances.",
            )).declare_as_argument()

        self.declare(
            "write_sced_instances",
            ConfigValue(
                domain=bool,
                default=False,
                description="Write all individual SCED instances.",
            )).declare_as_argument()

        self.declare(
            "print_sced",
            ConfigValue(
                domain=bool,
                default=False,
                description="Print results from SCED solves.",
            )).declare_as_argument()

        self.declare(
            "ruc_mipgap",
            ConfigValue(
                domain=NonNegativeFloat,
                default=0.01,
                description=
                "Specifies the mipgap for all deterministic RUC solves.",
            )).declare_as_argument()

        self.declare(
            "symbolic_solver_labels",
            ConfigValue(
                domain=bool,
                default=False,
                description="When interfacing with the solver, "
                "use symbol names derived from the model.",
            )).declare_as_argument()

        self.declare(
            "enable_quick_start_generator_commitment",
            ConfigValue(
                domain=bool,
                default=False,
                description=
                "Allows quick start generators to be committed if load shedding occurs",
            )).declare_as_argument()

        self.declare(
            "day_ahead_pricing",
            ConfigValue(
                domain=_InEnumStr(PricingType),
                default="aCHP",
                description=
                "Choose the pricing mechanism for the day-ahead market. Choices are "
                "LMP -- locational marginal price, "
                "ELMP -- enhanced locational marginal price, and "
                "aCHP -- approximated convex hull price. "
                "Default is aCHP.",
            )).declare_as_argument()

        # # PRESCIENT_OUTPUT_OPTIONS

        self.declare(
            "output_ruc_initial_conditions",
            ConfigValue(
                domain=bool,
                default=False,
                description=
                "Output ruc (deterministic or stochastic) initial conditions prior "
                "to each solve. Default is False.",
            )).declare_as_argument()

        self.declare(
            "output_ruc_solutions",
            ConfigValue(
                domain=bool,
                default=False,
                description="Output ruc solutions following each solve."
                " Default is False.",
            )).declare_as_argument()

        self.declare(
            "output_sced_initial_conditions",
            ConfigValue(
                domain=bool,
                default=False,
                description=
                "Output sced initial conditions prior to each solve. Default is False.",
            )).declare_as_argument()

        self.declare(
            "output_sced_loads",
            ConfigValue(
                domain=bool,
                default=False,
                description=
                "Output sced loads prior to each solve. Default is False.",
            )).declare_as_argument()

        self.declare(
            "output_solver_logs",
            ConfigValue(
                domain=bool,
                default=False,
                description="Output solver logs during execution.",
            )).declare_as_argument()

        self.declare(
            "output_max_decimal_places",
            ConfigValue(
                domain=PositiveInt,
                default=6,
                description=
                "When writing summary files, this rounds the output to the "
                "specified accuracy. Default is 6.",
            )).declare_as_argument()

        self.declare(
            "disable_stackgraphs",
            ConfigValue(
                domain=bool,
                default=False,
                description="Disable stackgraph generation",
            )).declare_as_argument()