Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How does PyEPO (SPO Loss) package handle 2D decision & constraint variables? #51

Open
HishamSalem opened this issue Jan 18, 2025 · 2 comments
Assignees
Labels
question Further information is requested

Comments

@HishamSalem
Copy link

HishamSalem commented Jan 18, 2025

I am trying to build a SPO model where the prediction components are supply & Demand.

In our specific case our cost factor is distance traveled for the nodes. However for us to minimize the distance we need to allocate supply to meet the expected demand.

How can we make it so that the loss function can handle the demand which is a constraint in the optimization model as we try to allocate supply?

the shape of inputs of the setobj is (Node,[supply,demand]).

@HishamSalem HishamSalem changed the title How does PyEPO (SPO Loss) package handle 2D decision variables? How does PyEPO (SPO Loss) package handle 2D decision & constraint variables? Jan 20, 2025
@LucasBoTang LucasBoTang self-assigned this Jan 22, 2025
@LucasBoTang LucasBoTang added the question Further information is requested label Jan 22, 2025
@LucasBoTang
Copy link
Collaborator

Hi @HishamSalem,

When creating your own optModel, you can override the setObj and solve methods for optModel. Here is an example of Warcraft images.

You need to do as follows:

import gurobipy as gp
from gurobipy import GRB

from pyepo.model.grb.grbmodel import optGrbModel

class yourModel(optGrbModel):

    def _getModel(self):
        """
        A method to build Gurobi model

        Returns:
            tuple: optimization model and variables
        """
        # ceate a model
        m = gp.Model("your model")
        # varibles
        x = m.addVars(need, supply, name="x")
        # sense
        m.modelSense = GRB.MINIMIZE
        # some constraints
        pass
        return m, x

    def setObj(self, c):
        """
        A method to set objective function

        Args:
            c (np.ndarray): cost of objective function
        """
        # sum up vector cost
        obj = gp.quicksum(c[i,j] * self.x[i,j] for i, j in self.x)
        self._model.setObjective(obj)

    def solve(self):
        """
        A method to solve model

        Returns:
            tuple: optimal solution (list) and objective value (float)
        """
        # update gurobi model
        self._model.update()
        # solve
        self._model.optimize()
        # kxk solution map
        sol = np.zeros_like(self.x)
        # get value
        for i,j in self.x:
            sol[i,j] = self.x[i,j].x
        return sol, self._model.objVal

I hope it will be helpful.

@HishamSalem
Copy link
Author

Thank you @LucasBoTang, I get the idea behind this approach, but say if we have 2 targets of demand and supply. If a model does poor demand, the supply allocation would fail as it is a constraint into the optimization. For example we build a pytorch model that predicts both, how would we get the spo class work?

What I tried to do is "flatten the outputs from the gurobi model" but I am not sure if this is accurate. Does your suggestion state that we should only represent the supply even if the predictive model would produce other outputs that are represented in the downstream optimization?

here is a snipit of the solve part of the model.

    def solve(self):
        self._model.optimize()
        if self._model.status != GRB.OPTIMAL:
            # Return an all-zero array so that the shape is consistent
            sol_vec = np.zeros(2 * self.num_nodes)
            return sol_vec, 1e12  # large “bad” objective

        # read supply
        supply_solution = np.zeros(self.num_nodes)
        for (h3_i), var in self.s.items():
            i = self._h3_ind_map[h3_i]
            supply_solution[i] = var.X

        flow_matrix = np.zeros((self.num_nodes, self.num_nodes))
        for (h3_i, h3_j), var in self.x.items():
            i = self._h3_ind_map[h3_i]
            j = self._h3_ind_map[h3_j]
            flow_matrix[i, j] = var.X

        orders_served = flow_matrix.sum(axis=1)
        sol_n2 = np.column_stack((supply_solution, orders_served))
        
        # 1) Clip negatives to zero
        np.clip(sol_n2, 0, None, out=sol_n2)

        # 2) Remove -0.0 artifacts by forcing near-zero values to 0
        sol_n2[np.isclose(sol_n2, 0, atol=1e-12)] = 0.0

        sol_vec = sol_n2.flatten()  # shape (2N,)
        return sol_vec, self._model.objVal

how would this work with
loss = spo(cp, c, w, z)

If you have a multi output then the loss function wouldn't work given the assumption that the inputs in the spo would be flattened. Advice will be very helpful as it will be awesome for me to understand how we can use multiple outputs together for your amazing package.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants