Repository: Pyomo/pyomo

Issue:
bug in pynumero example: sensitivity.py
It seems like the [sensitivity.py](https://github.com/Pyomo/pyomo/blob/master/pyomo/contrib/pynumero/examples/sensitivity.py) code is not calculating the sensitivity correctly. The sensitivity based x_new is a bit different from the result obtained by solving the perturbed NLP in the code. The cause of this seems to be the creation of `Np`

```
Np = BlockMatrix(2, 1)
Np[0, 0] = nlp.extract_submatrix_hessian_lag(pyomo_variables_rows=nlp.get_pyomo_variables(), pyomo_variables_cols=[m.eta1, m.eta2])
Np[1, 0] = nlp.extract_submatrix_jacobian(pyomo_variables=[m.eta1, m.eta2], pyomo_constraints=nlp.get_pyomo_constraints())
```
It seems that in this example Np should be 9x2 matrix with all zeros except elements (8, 1) and (9, 2) which should be one (based on eq 13 in this [reference paper](http://www.optimization-online.org/DB_FILE/2011/04/3008.pdf)). 

I have a [jupyter notebook](https://github.com/notesofdabbler/learn_pyomo/blob/master/wk_pynumero/wk_sensitivity.ipynb) that recreates this example and the proposed modification which seems to fix the issue.


Discussion:
@notesofdabbler Thanks. If you already have a fix, can you create a pull request?
@santiagoropb Can you look at this quickly?
Sorry, I noticed a mistake in the modification I did. I will recheck my modified code later today and see if the modification I did was the problem and whether the issue I raised is still relevant or not.
I have now fixed my code in this [jupyter notebook](https://github.com/notesofdabbler/learn_pyomo/blob/master/wk_pynumero/wk_sensitivity.ipynb). I would appreciate a review of this proposed modification.
@notesofdabbler the matrix Np is calculated according to equation 24. In that context I don't think it should be a matrix of only zeros and ones. 
@santiagoropb Thanks for your quick review and response. You are correct that `Np` is not a matrix of zeros and ones in the context of eq 24. However, the code is not calculating `new_x` correctly since the columns of Np are also in M and therefore `ds = [dx1: 0, dx2: 0, dx3:0, deta1: 1, deta2: 1]`. 

It looks like using eq 24 with the following modifications will fix the sensitivity calculation:
1. M[0, 0] matrix is a submatrix with just the variables `m.x1, m.x2, m.x3` and M[1, 0] has only  constraints `m.const1, m.const2`.
2. Np would also need to just have the variables `m.x1, m.x2, m.x3` for Np[0, 0] and constraints `m.const1, m.const2` for Np[1, 0]
3. Changing `Np.tocsc() to -Np.tocsc()` in `ds = spsolve(M.tocsc(), Np.tocsc())`

When I tried this modification in the [jupyter notebook](https://github.com/notesofdabbler/learn_pyomo/blob/master/wk_pynumero/wk_sensitivity.ipynb), it seems to fix the issue.

The alternative way that fixes the issue is using eq 13 based Np (matrix of zeros and ones) since 
eq 13 is a variant of eq 24 when there are explicity equations with a set of variables `w` set equal
to the parameters `p`. In this case matrix `M` as calculated in sensitivity.py doesn't have to be changed.
@notesofdabbler you are certainly right! thanks for catching this. I will make a PR tonight to correct the example.

Resolving patch:
diff --git a/pyomo/contrib/pynumero/examples/sensitivity.py b/pyomo/contrib/pynumero/examples/sensitivity.py
--- a/pyomo/contrib/pynumero/examples/sensitivity.py
+++ b/pyomo/contrib/pynumero/examples/sensitivity.py
@@ -81,34 +81,39 @@ def compute_init_lam(nlp, x=None, lam_max=1e3):
 nlp.set_primals(x)
 nlp.set_duals(y)
 
-J = nlp.evaluate_jacobian()
-H = nlp.evaluate_hessian_lag()
+J = nlp.extract_submatrix_jacobian(pyomo_variables=[m.x1, m.x2, m.x3], pyomo_constraints=[m.const1, m.const2])
+H = nlp.extract_submatrix_hessian_lag(pyomo_variables_rows=[m.x1, m.x2, m.x3], pyomo_variables_cols=[m.x1, m.x2, m.x3])
 
 M = BlockSymMatrix(2)
 M[0, 0] = H
 M[1, 0] = J
 
 Np = BlockMatrix(2, 1)
-Np[0, 0] = nlp.extract_submatrix_hessian_lag(pyomo_variables_rows=nlp.get_pyomo_variables(), pyomo_variables_cols=[m.eta1, m.eta2])
-Np[1, 0] = nlp.extract_submatrix_jacobian(pyomo_variables=[m.eta1, m.eta2], pyomo_constraints=nlp.get_pyomo_constraints())
+Np[0, 0] = nlp.extract_submatrix_hessian_lag(pyomo_variables_rows=[m.x1, m.x2, m.x3], pyomo_variables_cols=[m.eta1, m.eta2])
+Np[1, 0] = nlp.extract_submatrix_jacobian(pyomo_variables=[m.eta1, m.eta2], pyomo_constraints=[m.const1, m.const2])
 
-ds = spsolve(M.tocsc(), Np.tocsc())
-print(nlp.variable_names())
+ds = spsolve(M.tocsc(), -Np.tocsc())
 
+print("ds:\n", ds.todense())
 #################################################################
 
 p0 = np.array([pyo.value(m.nominal_eta1), pyo.value(m.nominal_eta2)])
 p = np.array([4.45, 1.05])
 dp = p - p0
-dx = ds.dot(dp)[0:nlp.n_primals()]
-new_x = x + dx
-print(new_x)
+dx = ds.dot(dp)[0:3]
+x_indices = nlp.get_primal_indices([m.x1, m.x2, m.x3])
+x_names = np.array(nlp.variable_names())
+new_x = x[x_indices] + dx
+print("dp:", dp)
+print("dx:", dx)
+print("Variable names: \n",x_names[x_indices])
+print("Sensitivity based x:\n", new_x)
 
 #################################################################
 m = create_model(4.45, 1.05)
 opt = pyo.SolverFactory('ipopt')
-results = opt.solve(m, tee=True)
+results = opt.solve(m, tee=False)
 nlp = PyomoNLP(m)
-print(nlp.init_primals())
-
+new_x = nlp.init_primals()
+print("NLP based x:\n", new_x[nlp.get_primal_indices([m.x1, m.x2, m.x3])])
 


Test patch: