Source code for apem.order_book_based_model.euphemia.cutting_strategies.price_based

from typing import TYPE_CHECKING

import pandas as pd
import gurobipy as gp
from gurobipy import GRB

from apem.order_book_based_model.euphemia.pricing.price_determination_subproblem import PriceSubproblem
import apem.order_book_based_model.euphemia.cutting_strategies.no_good as no_good_cutting

if TYPE_CHECKING:
    from apem.order_book_based_model.euphemia.master_problem.master_problem import MasterProblem


def _log(self: "MasterProblem", message: str) -> None:
    if hasattr(self, "run_logger"):
        self.run_logger.info(message)
    elif hasattr(self, "_emit"):
        self._emit(message)


[docs] def handle_price_based_cutting(self: "MasterProblem", callback_model: gp.Model) -> None: """Build and post a price-based cut from the current incumbent solution. The routine solves an unconstrained pricing subproblem that only includes step-order and piecewise-linear-order constraints. If prices are found, it updates provisional prices, identifies paradoxically accepted or rejected orders, and posts lazy cuts that deactivate the current infeasible economic pattern. If no informative price-based cut can be generated, it falls back to a no-good cut. :param self: Active Euphemia master-problem instance. :param callback_model: Gurobi callback model used to post lazy cuts. :return: ``None``. """ _log(self, "Creating unconstrained subproblem") price_subproblem = PriceSubproblem(master_problem=self) price_subproblem.isConstrained = False price_subproblem.solve_price_determination_subproblem() if price_subproblem.pricing_model.Status == GRB.OPTIMAL: # Update prices (No final prices!) self.set_prices(price_subproblem.extract_prices(), reinsertion=False) # Add price-based cut to block orders pab_blocks = self.get_block_bids(threshold=False) _log(self, f"PABs: {pab_blocks}") for b in pab_blocks: block_order = self.block_orders[self.block_orders['id'] == b].iloc[0] add_price_based_cut_to_block(self=self, callback_model=callback_model, block_order=block_order) # Deactivate PAMICs/PAMPs terms = [] violated_complex_mic = self.get_MIC_complex_orders(threshold=True) _log(self, f"PAMIC complex: {violated_complex_mic}") if violated_complex_mic: terms.extend(self.accept_complex[i] for i in violated_complex_mic) violated_scalable_mic = self.get_MIC_scalable_orders(threshold=True) _log(self, f"PAMIC scalable complex: {violated_scalable_mic}") if violated_scalable_mic: terms.extend(self.accept_scalable[i] for i in violated_scalable_mic) violated_complex_load_gradient = self.get_load_gradient_orders(threshold=True, complex=True) _log(self, f"PA complex load gradient: {violated_complex_load_gradient}") if violated_complex_load_gradient: terms.extend(self.accept_complex[i] for i in violated_complex_load_gradient) violated_scalable_load_gradient = self.get_load_gradient_orders(threshold=True, complex=False) _log(self, f"PA complex load gradient: {violated_scalable_load_gradient}") if violated_scalable_load_gradient: terms.extend(self.accept_scalable[i] for i in violated_scalable_load_gradient) if terms: _log(self, f"Deactivate PA (scalable) complex orders: {gp.quicksum(terms)} == 0") callback_model.cbLazy(gp.quicksum(terms) == 0) # If no paradoxically accepted orders but subproblem infeasible add simple no good cut elif not pab_blocks: no_good_cutting.add_no_good_cut(self=self, callback_model=callback_model) else: _log(self, "Something went wrong and in the unconstrained problem no prices could be found") no_good_cutting.add_no_good_cut(self=self, callback_model=callback_model)
[docs] def add_price_based_cut_to_block( self: "MasterProblem", callback_model: gp.Model, block_order: pd.Series, ) -> None: """Post a lazy price-based cut for one paradoxically accepted block order. The cut combines the target block with accepted or rejected overlapping block orders, depending on whether the target block is a buy or sell order, and forces at least one of those binary acceptance decisions to change. :param self: Active Euphemia master-problem instance. :param callback_model: Gurobi callback model used to post the lazy cut. :param block_order: Row describing the paradoxically accepted block order. :return: ``None``. """ terms = [1 - self.MAR_aux[block_order['id']]] # (1 - ACCEPT_hat) def is_sale(bo_id: int) -> bool: return self.block_orders.loc[self.block_orders['id'] == bo_id, [f"q{t}" for t in self.periods]].values.sum() > 0 for overlapping_order_id in block_order['overlap_set']: accepted = self.current_alloc_solution[f"y[{overlapping_order_id}]"][0] > self.epsilon sale = is_sale(overlapping_order_id) if is_sale(block_order['id']): if sale and accepted: terms.append(1 - self.MAR_aux[overlapping_order_id]) if (not sale) and (not accepted): terms.append(self.MAR_aux[overlapping_order_id]) else: if (not sale) and accepted: terms.append(1 - self.MAR_aux[overlapping_order_id]) if sale and (not accepted): terms.append(self.MAR_aux[overlapping_order_id]) callback_model.cbLazy(gp.quicksum(terms) >= 1) _log(self, f"Added {gp.quicksum(terms)} >= 1")