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