Source code for pyomo.contrib.sensitivity_toolbox.pynumero

# ____________________________________________________________________________________
#
# Pyomo: Python Optimization Modeling Objects
# Copyright (c) 2008-2026 National Technology and Engineering Solutions of Sandia, LLC
# Under the terms of Contract DE-NA0003525 with National Technology and Engineering
# Solutions of Sandia, LLC, the U.S. Government retains certain rights in this
# software.  This software is distributed under the 3-clause BSD License.
# ____________________________________________________________________________________

from pyomo.common.dependencies import numpy as np
from pyomo.common.dependencies import scipy, attempt_import

import pyomo.environ as pyo

# Use attempt_import here due to unguarded NumPy import in this file
nlp = attempt_import('pyomo.contrib.pynumero.interfaces.pyomo_nlp')[0]
from pyomo.common.collections import ComponentSet, ComponentMap
import pyomo.core.expr.calculus.derivatives as derivatives


def _coo_reorder_cols(mat, remap):
    """Change the order of columns in a COO matrix. The main use of this is
    to reorder variables in the Jacobian matrix. This changes the matrix in
    place.

    Parameters
    ----------
    mat: scipy.sparse.coo_matrix
        Reorder the columns of this matrix
    remap: dict
        dictionary where keys are old column and value is new column, if a column
        doesn't move, it doesn't need to be included.

    Returns
    -------
    NoneType
        None
    """
    for i in range(len(mat.data)):
        try:
            mat.col[i] = remap[mat.col[i]]
        except KeyError:
            pass  # it's fine if we don't move a col in remap


[docs] def get_dsdp_dfdp(model, theta): """Calculate the derivatives of the state variables (s) with respect to parameters (p) (ds/dp), and the derivative of the objective function (f) with respect to p (df/dp). The number of parameters in theta should be the same as the number of degrees of freedom. Parameters ---------- model: pyomo.environ.Block | pyomo.contrib.pynumero.interfaces.PyomoNLP Model to calculate sensitivity on. To retain the cached objects in the pynumero interface, create a PyomoNLP first and pass it to this function. theta: list A list of parameters as pyomo.environ.VarData, the number of parameters should be equal to the degrees of freedom. Returns ------- scipy.sparse.csc_matrix, csc_matrix, ComponentMap, ComponentMap ds/dp (ns + np by np), df/dp (1 by np), row map, column map. The column map maps Pyomo variables p to columns and the row map maps Pyomo variables s to rows. """ # Create a Pynumero NLP and get Jacobian if isinstance(model, nlp.PyomoNLP): m2 = model else: m2 = nlp.PyomoNLP(model) J = m2.evaluate_jacobian_eq() v_list = m2.get_pyomo_variables() # Map variables to columns in J mv_map = {id(v): i for i, v in enumerate(v_list)} s_list = list(ComponentSet(v_list) - ComponentSet(theta)) ns = len(s_list) np = len(theta) col_remap = {mv_map[id(v)]: i for i, v in enumerate(s_list + theta)} _coo_reorder_cols(J, remap=col_remap) J = J.tocsc() dB = -( J
[docs] @ scipy.sparse.vstack( (scipy.sparse.coo_matrix((ns, np)), scipy.sparse.identity(np)) ).tocsc() ) # Calculate sensitivity matrix dsdp = scipy.sparse.linalg.spsolve(J[:, range(ns)], dB) # Get a map of state vars to columns s_map = {id(v): i for i, v in enumerate(s_list)} # Get the outputs we are interested in from the list of output vars column_map = ComponentMap([(v, i) for i, v in enumerate(theta)]) row_map = ComponentMap([(v, i) for i, v in enumerate(s_list)]) dfdx = scipy.sparse.coo_matrix(m2.evaluate_grad_objective()) _coo_reorder_cols(dfdx, remap=col_remap) dfdx = dfdx.tocsc() dfdp = dfdx[0, :ns] @ dsdp + dfdx[0, ns:] # Add independent variables to the rows as the end dsdp = scipy.sparse.vstack( [dsdp, scipy.sparse.identity(len(column_map), format="csc")] ) n = len(row_map) for p, i in column_map.items(): row_map[p] = i + n # return sensitivity of the outputs to p and component maps return dsdp, dfdp, row_map, column_map
def get_dydp(y_list, dsdp, row_map, column_map=None): """Reduce the sensitivity matrix from get_dsdp_dfdp to only a specified set of state variables of interest. Parameters ---------- y_list: list A list of state variables or named expressions dsdp: csc_matrix A sensitivity matrix calculated by get_dsdp_dfdp row_map: ComponentMap A row map from get_dsdp_dfdp column_map: ComponentMap A column map from get_dsdp_dfdp, only needed if y_list contains expressions Returns ------- csc_matrix, ComponentMap dy/dp and a new row map with only y variables """ j = 0 new_row_map = ComponentMap() expr_row_map = ComponentMap() rows = [None] * len(y_list) for i, v in enumerate(y_list): new_row_map[v] = i if v not in row_map: expr_row_map[v] = j j += 1 else: rows[i] = dsdp[row_map[v], :] if j > 0: if column_map is None: raise ValueError( "A column_map must be provided if y_list contains named expressions." ) wrt_list = [s for s in row_map] dedx = [None] * j for v, i in expr_row_map.items(): dedx[i] = scipy.sparse.csc_matrix( derivatives.differentiate(v, wrt_list=wrt_list) ) dedx = scipy.sparse.vstack(dedx) ns = len(row_map) - len(column_map) dedp = dedx[:, :ns] @ dsdp[:ns, :] + dedx[:, ns:] for v, i in expr_row_map.items(): rows[new_row_map[v]] = dedp[i, :] dydp = scipy.sparse.vstack(rows) return dydp, new_row_map