|
| 1 | +############################################################################### |
| 2 | +# mpi-sppy: MPI-based Stochastic Programming in PYthon |
| 3 | +# |
| 4 | +# Copyright (c) 2024, Lawrence Livermore National Security, LLC, Alliance for |
| 5 | +# Sustainable Energy, LLC, The Regents of the University of California, et al. |
| 6 | +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md for |
| 7 | +# full copyright and license information. |
| 8 | +############################################################################### |
| 9 | + |
| 10 | +import numpy as np |
| 11 | + |
| 12 | +import pyomo.environ as pyo |
| 13 | +from pyomo.contrib.pynumero.linalg.scipy_interface import ScipyLU |
| 14 | + |
| 15 | +import mpisppy.extensions.extension |
| 16 | +import mpisppy.MPI as MPI |
| 17 | +from mpisppy.utils.kkt.interface import InteriorPointInterface |
| 18 | + |
| 19 | + |
| 20 | +class SensiRho(mpisppy.extensions.extension.Extension): |
| 21 | + """ |
| 22 | + Rho determination algorithm using nonant sensitivities |
| 23 | + """ |
| 24 | + |
| 25 | + def __init__(self, ph): |
| 26 | + self.ph = ph |
| 27 | + |
| 28 | + self.multiplier = 1.0 |
| 29 | + |
| 30 | + if ( |
| 31 | + "sensi_rho_options" in ph.options |
| 32 | + and "multiplier" in ph.options["sensi_rho_options"] |
| 33 | + ): |
| 34 | + self.multiplier = ph.options["sensi_rho_options"]["multiplier"] |
| 35 | + |
| 36 | + @staticmethod |
| 37 | + def _compute_rho_min_max(ph, npop, mpiop, start): |
| 38 | + local_nodenames = [] |
| 39 | + local_xmaxmin = {} |
| 40 | + global_xmaxmin = {} |
| 41 | + |
| 42 | + for k, s in ph.local_scenarios.items(): |
| 43 | + nlens = s._mpisppy_data.nlens |
| 44 | + for node in s._mpisppy_node_list: |
| 45 | + if node.name not in local_nodenames: |
| 46 | + ndn = node.name |
| 47 | + local_nodenames.append(ndn) |
| 48 | + nlen = nlens[ndn] |
| 49 | + |
| 50 | + local_xmaxmin[ndn] = start * np.ones(nlen, dtype="d") |
| 51 | + global_xmaxmin[ndn] = np.zeros(nlen, dtype="d") |
| 52 | + |
| 53 | + for k, s in ph.local_scenarios.items(): |
| 54 | + nlens = s._mpisppy_data.nlens |
| 55 | + rho = s._mpisppy_model.rho |
| 56 | + for node in s._mpisppy_node_list: |
| 57 | + ndn = node.name |
| 58 | + xmaxmin = local_xmaxmin[ndn] |
| 59 | + |
| 60 | + xmaxmin_partial = np.fromiter( |
| 61 | + (rho[ndn,i]._value for i, _ in enumerate(node.nonant_vardata_list)), |
| 62 | + dtype="d", |
| 63 | + count=nlens[ndn], |
| 64 | + ) |
| 65 | + xmaxmin = npop(xmaxmin, xmaxmin_partial) |
| 66 | + local_xmaxmin[ndn] = xmaxmin |
| 67 | + |
| 68 | + for nodename in local_nodenames: |
| 69 | + ph.comms[nodename].Allreduce( |
| 70 | + [local_xmaxmin[nodename], MPI.DOUBLE], |
| 71 | + [global_xmaxmin[nodename], MPI.DOUBLE], |
| 72 | + op=mpiop, |
| 73 | + ) |
| 74 | + |
| 75 | + xmaxmin_dict = {} |
| 76 | + for ndn, global_xmaxmin_dict in global_xmaxmin.items(): |
| 77 | + for i, v in enumerate(global_xmaxmin_dict): |
| 78 | + xmaxmin_dict[ndn, i] = v |
| 79 | + |
| 80 | + return xmaxmin_dict |
| 81 | + |
| 82 | + @staticmethod |
| 83 | + def _compute_rho_avg(ph): |
| 84 | + local_nodenames = [] |
| 85 | + local_avg = {} |
| 86 | + global_avg = {} |
| 87 | + |
| 88 | + for k, s in ph.local_scenarios.items(): |
| 89 | + nlens = s._mpisppy_data.nlens |
| 90 | + rho = s._mpisppy_model.rho |
| 91 | + for node in s._mpisppy_node_list: |
| 92 | + if node.name not in local_nodenames: |
| 93 | + ndn = node.name |
| 94 | + local_nodenames.append(ndn) |
| 95 | + nlen = nlens[ndn] |
| 96 | + |
| 97 | + local_avg[ndn] = np.zeros(nlen, dtype="d") |
| 98 | + global_avg[ndn] = np.zeros(nlen, dtype="d") |
| 99 | + |
| 100 | + for k, s in ph.local_scenarios.items(): |
| 101 | + nlens = s._mpisppy_data.nlens |
| 102 | + rho = s._mpisppy_model.rho |
| 103 | + for node in s._mpisppy_node_list: |
| 104 | + ndn = node.name |
| 105 | + |
| 106 | + local_rhos = np.fromiter( |
| 107 | + (rho[ndn,i]._value for i, _ in enumerate(node.nonant_vardata_list)), |
| 108 | + dtype="d", |
| 109 | + count=nlens[ndn], |
| 110 | + ) |
| 111 | + # print(f"{k=}, {local_rhos=}, {s._mpisppy_probability=}, {s._mpisppy_data.prob_coeff[ndn]=}") |
| 112 | + # TODO: is this the right thing, or should it be s._mpisppy_probability? |
| 113 | + local_rhos *= s._mpisppy_data.prob_coeff[ndn] |
| 114 | + |
| 115 | + local_avg[ndn] += local_rhos |
| 116 | + |
| 117 | + for nodename in local_nodenames: |
| 118 | + ph.comms[nodename].Allreduce( |
| 119 | + [local_avg[nodename], MPI.DOUBLE], |
| 120 | + [global_avg[nodename], MPI.DOUBLE], |
| 121 | + op=MPI.SUM, |
| 122 | + ) |
| 123 | + |
| 124 | + rhoavg_dict = {} |
| 125 | + for ndn, global_rhoavg_dict in global_avg.items(): |
| 126 | + for i, v in enumerate(global_rhoavg_dict): |
| 127 | + rhoavg_dict[ndn, i] = v |
| 128 | + |
| 129 | + return rhoavg_dict |
| 130 | + |
| 131 | + @staticmethod |
| 132 | + def _compute_rho_max(ph): |
| 133 | + return SensiRho._compute_rho_min_max(ph, np.maximum, MPI.MAX, -np.inf) |
| 134 | + |
| 135 | + @staticmethod |
| 136 | + def _compute_rho_min(ph): |
| 137 | + return SensiRho._compute_rho_min_max(ph, np.minimum, MPI.MIN, np.inf) |
| 138 | + |
| 139 | + def pre_iter0(self): |
| 140 | + pass |
| 141 | + |
| 142 | + def post_iter0(self): |
| 143 | + ph = self.ph |
| 144 | + |
| 145 | + # first, solve the subproblems with Ipopt, |
| 146 | + # and gather sensitivity information |
| 147 | + ipopt = pyo.SolverFactory("ipopt") |
| 148 | + nonant_sensis = {} |
| 149 | + for k, s in ph.local_subproblems.items(): |
| 150 | + solution_cache = pyo.ComponentMap() |
| 151 | + for var in s.component_data_objects(pyo.Var): |
| 152 | + solution_cache[var] = var._value |
| 153 | + relax_int = pyo.TransformationFactory('core.relax_integer_vars') |
| 154 | + relax_int.apply_to(s) |
| 155 | + |
| 156 | + assert hasattr(s, "_relaxed_integer_vars") |
| 157 | + |
| 158 | + # add the needed suffixes / remove later |
| 159 | + s.ipopt_zL_out = pyo.Suffix(direction=pyo.Suffix.IMPORT) |
| 160 | + s.ipopt_zU_out = pyo.Suffix(direction=pyo.Suffix.IMPORT) |
| 161 | + s.dual = pyo.Suffix(direction=pyo.Suffix.IMPORT_EXPORT) |
| 162 | + |
| 163 | + results = ipopt.solve(s) |
| 164 | + pyo.assert_optimal_termination(results) |
| 165 | + |
| 166 | + kkt_builder = InteriorPointInterface(s) |
| 167 | + kkt_builder.set_barrier_parameter(1e-9) |
| 168 | + kkt_builder.set_bounds_relaxation_factor(1e-8) |
| 169 | + #rhs = kkt_builder.evaluate_primal_dual_kkt_rhs() |
| 170 | + #print(f"{rhs}") |
| 171 | + #print(f"{rhs.flatten()}") |
| 172 | + kkt = kkt_builder.evaluate_primal_dual_kkt_matrix() |
| 173 | + |
| 174 | + # print(f"{kkt=}") |
| 175 | + # could do better than SuperLU |
| 176 | + kkt_lu = ScipyLU() |
| 177 | + # always regularize equality constraints |
| 178 | + kkt_builder.regularize_equality_gradient(kkt=kkt, coef=-1e-8, copy_kkt=False) |
| 179 | + kkt_lu.do_numeric_factorization(kkt, raise_on_error=True) |
| 180 | + |
| 181 | + grad_vec = np.zeros(kkt.shape[1]) |
| 182 | + grad_vec[0:kkt_builder._nlp.n_primals()] = kkt_builder._nlp.evaluate_grad_objective() |
| 183 | + |
| 184 | + grad_vec_kkt_inv = kkt_lu._lu.solve(grad_vec, "T") |
| 185 | + |
| 186 | + for scenario_name in s.scen_list: |
| 187 | + nonant_sensis[scenario_name] = {} |
| 188 | + rho = ph.local_scenarios[scenario_name]._mpisppy_model.rho |
| 189 | + for ndn_i, v in ph.local_scenarios[scenario_name]._mpisppy_data.nonant_indices.items(): |
| 190 | + var_idx = kkt_builder._nlp._vardata_to_idx[v] |
| 191 | + |
| 192 | + y_vec = np.zeros(kkt.shape[0]) |
| 193 | + y_vec[var_idx] = 1.0 |
| 194 | + |
| 195 | + x_denom = y_vec.T @ kkt_lu._lu.solve(y_vec) |
| 196 | + x = (-1 / x_denom) |
| 197 | + e_x = x * y_vec |
| 198 | + |
| 199 | + sensitivity = grad_vec_kkt_inv @ -e_x |
| 200 | + # print(f"df/d{v.name}: {sensitivity:.2e}, ∂f/∂{v.name}: {grad_vec[var_idx]:.2e}, " |
| 201 | + # f"rho {v.name}: {ph.local_scenarios[scenario_name]._mpisppy_model.rho[ndn_i]._value:.2e}, ", |
| 202 | + # f"value: {v._value:.2e}" |
| 203 | + # ) |
| 204 | + |
| 205 | + rho[ndn_i]._value = abs(sensitivity) |
| 206 | + |
| 207 | + relax_int.apply_to(s, options={"undo":True}) |
| 208 | + assert not hasattr(s, "_relaxed_integer_vars") |
| 209 | + del s.ipopt_zL_out |
| 210 | + del s.ipopt_zU_out |
| 211 | + del s.dual |
| 212 | + for var, val in solution_cache.items(): |
| 213 | + var._value = val |
| 214 | + |
| 215 | + for s in ph.local_scenarios.values(): |
| 216 | + xbars = s._mpisppy_model.xbars |
| 217 | + for ndn_i, rho in s._mpisppy_model.rho.items(): |
| 218 | + nv = s._mpisppy_data.nonant_indices[ndn_i] # var_data object |
| 219 | + rho._value = rho._value / max(1, abs(nv._value - xbars[ndn_i]._value)) |
| 220 | + rho._value *= self.multiplier |
| 221 | + # if ph.cylinder_rank == 0: |
| 222 | + # print(f"{s.name=}, {nv.name=}, {rho.value=}") |
| 223 | + |
| 224 | + rhoavg = self._compute_rho_avg(ph) |
| 225 | + for s in ph.local_scenarios.values(): |
| 226 | + xbars = s._mpisppy_model.xbars |
| 227 | + for ndn_i, rho in s._mpisppy_model.rho.items(): |
| 228 | + rho._value = rhoavg[ndn_i] |
| 229 | + # if ph.cylinder_rank == 0: |
| 230 | + # nv = s._mpisppy_data.nonant_indices[ndn_i] # var_data object |
| 231 | + # print(f"{s.name=}, {nv.name=}, {rho.value=}") |
| 232 | + |
| 233 | + if ph.cylinder_rank == 0: |
| 234 | + print("Rho values updated by SensiRho Extension") |
| 235 | + |
| 236 | + def miditer(self): |
| 237 | + pass |
| 238 | + |
| 239 | + def enditer(self): |
| 240 | + pass |
| 241 | + |
| 242 | + def post_everything(self): |
| 243 | + pass |
0 commit comments