1# ___________________________________________________________________________ 2# 3# Pyomo: Python Optimization Modeling Objects 4# Copyright 2017 National Technology and Engineering Solutions of Sandia, LLC 5# Under the terms of Contract DE-NA0003525 with National Technology and 6# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain 7# rights in this software. 8# This software is distributed under the 3-clause BSD License. 9# ___________________________________________________________________________ 10 11import abc 12import logging 13import numpy as np 14from scipy.sparse import coo_matrix 15 16from pyomo.common.deprecation import RenamedClass 17from pyomo.common.log import is_debug_set 18from pyomo.common.timing import ConstructionTimer 19from pyomo.core.base import Var, Set, Constraint, value 20from pyomo.core.base.block import _BlockData, Block, declare_custom_block 21from pyomo.core.base.initializer import Initializer 22from pyomo.core.base.set import UnindexedComponent_set 23from pyomo.core.base.reference import Reference 24 25from ..sparse.block_matrix import BlockMatrix 26 27 28logger = logging.getLogger('pyomo.contrib.pynumero') 29 30""" 31This module is used for interfacing an external model as 32a block in a Pyomo model. 33 34An ExternalGreyBoxModel is model is a model that does not 35provide constraints explicitly as algebraic expressions, but 36instead provides a set of methods that can compute the residuals 37of the constraints (or outputs) and their derivatives. 38 39This allows one to interface external codes (e.g., compiled 40external models) with a Pyomo model. 41 42Note: To solve a Pyomo model that contains these external models 43 we have a specialized interface built on PyNumero that provides 44 an interface to the CyIpopt solver. 45 46To use this interface: 47 * Create a class that is derived from ExternalGreyBoxModel and 48 implement the necessary methods. This derived class must provide 49 a list of names for: the inputs to your model, the equality constraints 50 (or residuals) that need to be converged, and any outputs that 51 are computed from your model. It will also need to provide methods to 52 compute the residuals, outputs, and the jacobian of these with respect to 53 the inputs. Implement the methods to evaluate hessians if applicable. 54 See the documentation on ExternalGreyBoxModel for more details. 55 56 * Create a Pyomo model and make use of the ExternalGreyBoxBlock 57 to produce a Pyomo modeling component that represents your 58 external model. This block is a Pyomo component, and when you 59 call set_external_model() and provide an instance of your derived 60 ExternalGreyBoxModel, it will automatically create pyomo variables to 61 represent the inputs and the outputs from the external model. You 62 can implement a callback to modify the Pyomo block after it is 63 constructed. This also provides a mechanism to initalize variables, 64 etc. 65 66 * Create a PyomoGreyBoxNLP and provide it with the Pyomo model 67 that contains the ExternalGreyBoxBlocks. This class presents 68 an NLP interface (i.e., the PyNumero NLP abstract class), and 69 can be used with any solver that makes use of this interface 70 (e.g., the CyIpopt solver interface provided in PyNumero) 71 72See pyomo/contrib/pynumero/examples/external_grey_box for examples 73of the use of this interface. 74 75Note: 76 77 * Currently, you cannot "fix" a pyomo variable that corresponds to an 78 input or output and you must use a constraint instead (this is 79 because Pyomo removes fixed variables before sending them to the 80 solver) 81 82""" 83 84class ExternalGreyBoxModel(object): 85 """ 86 This is the base class for building external input output models 87 for use with Pyomo and CyIpopt. See the module documentation above, 88 and documentation of individual methods. 89 90 There are examples in: 91 pyomo/contrib/pynumero/examples/external_grey_box/react-example/ 92 93 Most methods are documented in the class itself. However, there are 94 methods that are not implemented in the base class that may need 95 to be implemented to provide support for certain features. 96 97 Hessian support: 98 99 If you would like to support Hessian computations for your 100 external model, you will need to implement the following methods to 101 support setting the multipliers that are used when computing the 102 Hessian of the Lagrangian. 103 - set_equality_constraint_multipliers: see documentation in method 104 - set_output_constraint_multipliers: see documentation in method 105 You will also need to implement the following methods to evaluate 106 the required Hessian information: 107 108 def evaluate_hessian_equality_constraints(self): 109 Compute the product of the equality constraint multipliers 110 with the hessian of the equality constraints. 111 E.g., y_eq^k is the vector of equality constraint multipliers 112 from set_equality_constraint_multipliers, w_eq(u)=0 are the 113 equality constraints, and u^k are the vector of inputs from 114 set_inputs. This method must return 115 H_eq^k = sum_i (y_eq^k)_i * grad^2_{uu} w_eq(u^k) 116 117 def evaluate_hessian_outputs(self): 118 Compute the product of the output constraint multipliers with the 119 hessian of the outputs. E.g., y_o^k is the vector of output 120 constraint multipliers from set_output_constraint_multipliers, 121 u^k are the vector of inputs from set_inputs, and w_o(u) is the 122 function that computes the vector of outputs at the values for 123 the input variables. This method must return 124 H_o^k = sum_i (y_o^k)_i * grad^2_{uu} w_o(u^k) 125 126 Examples that show Hessian support are also found in: 127 pyomo/contrib/pynumero/examples/external_grey_box/react-example/ 128 129 """ 130 131 def n_inputs(self): 132 """ This method returns the number of inputs. You do not 133 need to overload this method in derived classes. 134 """ 135 return len(self.input_names()) 136 137 def n_equality_constraints(self): 138 """ This method returns the number of equality constraints. 139 You do not need to overload this method in derived classes. 140 """ 141 return len(self.equality_constraint_names()) 142 143 def n_outputs(self): 144 """ This method returns the number of outputs. You do not 145 need to overload this method in derived classes. 146 """ 147 return len(self.output_names()) 148 149 def input_names(self): 150 """ 151 Provide the list of string names to corresponding to the inputs 152 of this external model. These should be returned in the same order 153 that they are to be used in set_input_values. 154 """ 155 raise NotImplementedError('Derived ExternalGreyBoxModel classes need to implement the method: input_names') 156 157 def equality_constraint_names(self): 158 """ 159 Provide the list of string names corresponding to any residuals 160 for this external model. These should be in the order corresponding 161 to values returned from evaluate_residuals. Return an empty list 162 if there are no equality constraints. 163 """ 164 return [] 165 166 def output_names(self): 167 """ 168 Provide the list of string names corresponding to the outputs 169 of this external model. These should be in the order corresponding 170 to values returned from evaluate_outputs. Return an empty list if there 171 are no computed outputs. 172 """ 173 return [] 174 175 def finalize_block_construction(self, pyomo_block): 176 """ 177 Implement this callback to provide any additional 178 specifications to the Pyomo block that is created 179 to represent this external grey box model. 180 181 Note that pyomo_block.inputs and pyomo_block.outputs 182 have been created, and this callback provides an 183 opportunity to set initial values, bounds, etc. 184 """ 185 pass 186 187 def set_input_values(self, input_values): 188 """ 189 This method is called by the solver to set the current values 190 for the input variables. The derived class must cache these if 191 necessary for any subsequent calls to evalute_outputs or 192 evaluate_derivatives. 193 """ 194 raise NotImplementedError('Derived ExternalGreyBoxModel classes need' 195 ' to implement the method: set_input_values') 196 197 def set_equality_constraint_multipliers(self, eq_con_multiplier_values): 198 """ 199 This method is called by the solver to set the current values 200 for the multipliers of the equality constraints. The derived 201 class must cache these if necessary for any subsequent calls 202 to evaluate_hessian_equality_constraints 203 """ 204 # we should check these for efficiency 205 assert self.n_equality_constraints() == len(eq_con_multiplier_values) 206 if not hasattr(self, 'evaluate_hessian_equality_constraints') \ 207 or self.n_equality_constraints() == 0: 208 return 209 210 raise NotImplementedError('Derived ExternalGreyBoxModel classes need to implement' 211 ' set_equality_constraint_multlipliers when they' 212 ' support Hessian computations.') 213 214 def set_output_constraint_multipliers(self, output_con_multiplier_values): 215 """ 216 This method is called by the solver to set the current values 217 for the multipliers of the output constraints. The derived 218 class must cache these if necessary for any subsequent calls 219 to evaluate_hessian_outputs 220 """ 221 # we should check these for efficiency 222 assert self.n_outputs() == len(output_con_multiplier_values) 223 if not hasattr(self, 'evaluate_hessian_output_constraints') \ 224 or self.n_outputs() == 0: 225 return 226 227 raise NotImplementedError('Derived ExternalGreyBoxModel classes need to implement' 228 ' set_output_constraint_multlipliers when they' 229 ' support Hessian computations.') 230 231 def get_equality_constraint_scaling_factors(self): 232 """ 233 This method is called by the solver interface to get desired 234 values for scaling the equality constraints. None means no 235 scaling is desired. Note that, depending on the solver, 236 one may need to set solver options so these factors are used 237 """ 238 return None 239 240 def get_output_constraint_scaling_factors(self): 241 """ 242 This method is called by the solver interface to get desired 243 values for scaling the constraints with output variables. Returning 244 None means that no scaling of the output constraints is desired. 245 Note that, depending on the solver, one may need to set solver options 246 so these factors are used 247 """ 248 return None 249 250 def evaluate_equality_constraints(self): 251 """ 252 Compute the residuals from the model (using the values 253 set in input_values) and return as a numpy array 254 """ 255 raise NotImplementedError('evaluate_equality_constraints called ' 256 'but not implemented in the derived class.') 257 258 def evaluate_outputs(self): 259 """ 260 Compute the outputs from the model (using the values 261 set in input_values) and return as a numpy array 262 """ 263 raise NotImplementedError('evaluate_outputs called ' 264 'but not implemented in the derived class.') 265 266 def evaluate_jacobian_equality_constraints(self): 267 """ 268 Compute the derivatives of the residuals with respect 269 to the inputs (using the values set in input_values). 270 This should be a scipy matrix with the rows in 271 the order of the residual names and the cols in 272 the order of the input variables. 273 """ 274 raise NotImplementedError('evaluate_jacobian_equality_constraints called ' 275 'but not implemented in the derived class.') 276 277 def evaluate_jacobian_outputs(self): 278 """ 279 Compute the derivatives of the outputs with respect 280 to the inputs (using the values set in input_values). 281 This should be a scipy matrix with the rows in 282 the order of the output variables and the cols in 283 the order of the input variables. 284 """ 285 raise NotImplementedError('evaluate_equality_outputs called ' 286 'but not implemented in the derived class.') 287 288 # 289 # Implement the following methods to provide support for 290 # Hessian computations: see documentation in class docstring 291 # 292 # def evaluate_hessian_equality_constraints(self): 293 # def evaluate_hessian_outputs(self): 294 # 295 296 297class ExternalGreyBoxBlockData(_BlockData): 298 299 def set_external_model(self, 300 external_grey_box_model, 301 inputs=None, 302 outputs=None, 303 ): 304 """ 305 Parameters 306 ---------- 307 external_grey_box_model: ExternalGreyBoxModel 308 The external model that will be interfaced to in this block 309 inputs: List of VarData objects 310 If provided, these VarData will be used as inputs into the 311 external model. 312 outputs: List of VarData objects 313 If provided, these VarData will be used as outputs from the 314 external model. 315 316 """ 317 self._ex_model = ex_model = external_grey_box_model 318 if ex_model is None: 319 self._input_names = self._output_names = None 320 self.inputs = self.outputs = None 321 return 322 323 # Shouldn't need input names if we provide input vars, but 324 # no reason to remove this. 325 # _could_ make inputs a reference-to-mapping 326 self._input_names = ex_model.input_names() 327 if self._input_names is None or len(self._input_names) == 0: 328 raise ValueError( 329 'No input_names specified for external_grey_box_model.' 330 ' Must specify at least one input.') 331 332 self._input_names_set = Set(initialize=self._input_names, ordered=True) 333 334 if inputs is None: 335 self.inputs = Var(self._input_names_set) 336 else: 337 if ex_model.n_inputs() != len(inputs): 338 raise ValueError( 339 "Dimension mismatch in provided input vars for external " 340 "model.\nExpected %s input vars, got %s." 341 % (ex_model.n_inputs(), len(inputs)) 342 ) 343 self.inputs = Reference(inputs) 344 345 self._equality_constraint_names = ex_model.equality_constraint_names() 346 self._output_names = ex_model.output_names() 347 348 self._output_names_set = Set(initialize=self._output_names, ordered=True) 349 350 if outputs is None: 351 self.outputs = Var(self._output_names_set) 352 else: 353 if ex_model.n_outputs() != len(outputs): 354 raise ValueError( 355 "Dimension mismatch in provided output vars for external " 356 "model.\nExpected %s output vars, got %s." 357 % (ex_model.n_outputs(), len(outputs)) 358 ) 359 self.outputs = Reference(outputs) 360 361 # call the callback so the model can set initialization, bounds, etc. 362 external_grey_box_model.finalize_block_construction(self) 363 364 def get_external_model(self): 365 return self._ex_model 366 367 368class ExternalGreyBoxBlock(Block): 369 370 _ComponentDataClass = ExternalGreyBoxBlockData 371 372 def __new__(cls, *args, **kwds): 373 if cls != ExternalGreyBoxBlock: 374 target_cls = cls 375 elif not args or (args[0] is UnindexedComponent_set and len(args) == 1): 376 target_cls = ScalarExternalGreyBoxBlock 377 else: 378 target_cls = IndexedExternalGreyBoxBlock 379 return super(ExternalGreyBoxBlock, cls).__new__(target_cls) 380 381 def __init__(self, *args, **kwds): 382 kwds.setdefault('ctype', ExternalGreyBoxBlock) 383 self._init_model = Initializer(kwds.pop('external_model', None)) 384 Block.__init__(self, *args, **kwds) 385 386 def construct(self, data=None): 387 """ 388 Construct the ExternalGreyBoxBlockDatas 389 """ 390 if self._constructed: 391 return 392 # Do not set the constructed flag - Block.construct() will do that 393 394 timer = ConstructionTimer(self) 395 if is_debug_set(logger): 396 logger.debug("Constructing external grey box model %s" 397 % (self.name)) 398 399 super(ExternalGreyBoxBlock, self).construct(data) 400 401 if self._init_model is not None: 402 block = self.parent_block() 403 for index, data in self.items(): 404 data.set_external_model(self._init_model(block, index)) 405 406 407class ScalarExternalGreyBoxBlock(ExternalGreyBoxBlockData, ExternalGreyBoxBlock): 408 def __init__(self, *args, **kwds): 409 ExternalGreyBoxBlockData.__init__(self, component=self) 410 ExternalGreyBoxBlock.__init__(self, *args, **kwds) 411 412 # Pick up the display() from Block and not BlockData 413 display = ExternalGreyBoxBlock.display 414 415 416class SimpleExternalGreyBoxBlock(metaclass=RenamedClass): 417 __renamed__new_class__ = ScalarExternalGreyBoxBlock 418 __renamed__version__ = '6.0' 419 420 421class IndexedExternalGreyBoxBlock(ExternalGreyBoxBlock): 422 pass 423