Source code for ams.routines.grbopt

"""
Routines using gurobi-optimods.
"""
import logging

import io

import scipy.io

import numpy as np
import pandas as pd

from ams.core import Config

from ams.io.pypower import system2ppc

from ams.opt import Var, Objective

from ams.routines.pypower import DCPF1
from ams.shared import opf

logger = logging.getLogger(__name__)


[docs] class OPF(DCPF1): """ Optimal Power Flow (OPF) routine using gurobi-optimods. This class provides an interface for performing optimal power flow analysis with gurobi-optimods, supporting both AC and DC OPF formulations. In addition to optimizing generator dispatch, this routine can also optimize transmission line statuses (branch switching), enabling topology optimization. Refer to the gurobi-optimods documentation for further details: https://gurobi-optimods.readthedocs.io/en/stable/mods/opf/opf.html .. versionadded:: 1.0.10 """
[docs] def __init__(self, system, config, **kwargs): super().__init__(system, config, **kwargs) self.type = 'ACED' # Overwrite the config to be empty, as it is not used in this routine self.config = Config(self.class_name) self.obj = Objective(name='obj', info='total cost, placeholder', unit='$', sense='min',) self.obj.e_str = ('cp.sum(cp.multiply(c2, pg**2)) + cp.sum(cp.multiply(c1, pg))' ' + cp.sum(cp.multiply(ug, c0))') self.pi = Var(info='Lagrange multiplier on real power mismatch', name='pi', unit='$/p.u.', model='Bus', src=None,) self.uld = Var(info='Line commitment decision', name='uld', tex_name=r'u_{l,d}', model='Line', src='u',)
[docs] def solve(self, **kwargs): ppc = system2ppc(self.system) mat = io.BytesIO() scipy.io.savemat(mat, {'mpc': ppc}) mat.seek(0) res = opf.solve_opf(opf.read_case_matpower(mat), **kwargs) return res
[docs] def unpack(self, res, **kwargs): """ Unpack the results from the gurobi-optimods. """ # NOTE: Map gurobi-optimods results to PPC-compatible format. # Only relevant columns are populated, as required by `DCOPF.unpack()`. # If future versions of gurobi-optimods provide additional outputs, # this mapping may need to be updated to extract and assign new fields. res_new = dict() res_new['success'] = res['success'] res_new['et'] = res['et'] res_new['f'] = res['f'] res_new['baseMVA'] = res['baseMVA'] bus = pd.DataFrame(res['bus']) res_new['bus'] = np.zeros((self.system.Bus.n, 17)) res_new['bus'][:, 7] = bus['Vm'].values res_new['bus'][:, 8] = bus['Va'].values # NOTE: As of v2.3.2, gurobi-optimods does not return LMP gen = pd.DataFrame(res['gen']) res_new['gen'] = np.zeros((self.system.StaticGen.n, 14)) res_new['gen'][:, 1] = gen['Pg'].values res_new['gen'][:, 2] = gen['Qg'].values branch = pd.DataFrame(res['branch']) res_new['branch'] = np.zeros((self.system.Line.n, 14)) res_new['branch'][:, 13] = branch['Pf'].values # NOTE: unpack branch_switching decision res_new['branch'][:, 10] = branch['switching'].values return super().unpack(res_new)
[docs] def run(self, **kwargs): """ Run the OPF routine using gurobi-optimods. This method invokes `gurobi-optimods.opf.solve_opf` to solve the OPF problem. Parameters ---------- - opftype : str Type of OPF to solve (default: ACPlocal). - branch_switching : bool Enable branch switching (default: False). - min_active_branches : float Defines the minimum number of branches that must be turned on when branch switching is active, i.e. the minimum number of turned on branches is equal to ``numbranches * min_active_branches``. Has no effect if ``branch_switching`` is set to False. - use_mip_start : bool Use MIP start (default: False). - time_limit : float Time limit for the solver (default: 0.0, no limit). """ # NOTE: gurobi-optimods iteration count is stored differently from PYPOWER, # so we do not attempt to extract it here and rely on the default. return self._run_with_external_solver( solver_name="gurobi-optimods", iter_key_path=['raw', 'output', 'iterations'], **kwargs )