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
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)
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)
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
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
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, )
def __setattr__(self, name, value): if name in PrescientConfig.__slots__: super(ConfigDict, self).__setattr__(name, value) else: ConfigDict.__setattr__(self, name, value)
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()