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

Build function to update constraints #79

Open
alexlusak opened this issue Jun 25, 2024 · 8 comments
Open

Build function to update constraints #79

alexlusak opened this issue Jun 25, 2024 · 8 comments
Labels
enhancement New feature or request

Comments

@alexlusak
Copy link

It would be very useful to add in functionality to have similar syntax to
.gppd.add_constrs(model, 'lhs <= rhs', name='c1')
but instead for re-assignment of a lhs or rhs of an expression.

For example, if models are being built across each day of a year, but the model size is too complex to construct the entire year in one go, constraints can be built on the first day of the year. They would need to be updated as values change across days. Something like the following would be very useful:
.gppd.update_constrs_rhs(model, [CONSTRAINT NAME], [NEW RHS COLUMN NAME])

As it stands right now I believe there would need to be some for loop construction across itertuples and manually update each constraint entry.

@simonbowly
Copy link
Member

Hi @alexlusak, thanks for the feedback!

Just to confirm, is the requirement here to modify only the right hand side of existing constraints? This can be done using the attribute accessors. Say you have built a constraint:

df_constr = df.gppd.add_constrs(model, "lhs <= rhs", name="c1")

you can modify the right hand side of the set of constraints by assigning to the 'RHS' attribute of the constraints as follows:

df_constr["c1"].gppd.RHS = new_rhs

where new_rhs is a series on the same index as the original dataframe.

@alexlusak
Copy link
Author

@simonbowly that was exactly what I was looking for, thanks!

I couldn't seem to find anything in the docs on this - if there isn't, perhaps it's something worth adding a small section on?

@simonbowly
Copy link
Member

Agreed, thanks for pointing that out, there are no concrete example of this in the docs. I'll add some in #80.

@alexlusak
Copy link
Author

That would be great. In a similar vein, is there similar syntax for updating a constraint coefficient?

@simonbowly
Copy link
Member

This is a bit tricker. Currently there is no syntax in gurobipy-pandas for this, it has to be done directly in gurobipy using chgCoeff with a handle to the variable and constraint you want to change the coefficient for. So you would need to loop manually over the dataframe to make the changes.

I'm happy to consider adding some functionality to do this, but I'm having trouble thinking of how it might look as an API in gurobipy-pandas. The attribute accessor updates just one value per constraint, and the attribute name (in the above case, 'RHS') specifies what data is being updated. For coefficients, you could have an arbitrary number of them in each constraint, so I'm not sure the same API style make sense. Do you have a concrete example?

@alexlusak
Copy link
Author

alexlusak commented Jun 26, 2024

Sure, I can provide one example. I have a dataframe with a float, price; variable, v1; and constraint, c1. price is a coefficient on v1 in constraint c1, so each day I would like to update this price as it changes. Therefore, something like the following would be useful to have:

df['c1'].gppd.update_coef('v1', 'price')

whereas right now it needs to be something like:

for row in df.itertuples():
    # access constraint location and update coefficient with price and v1 in the same row

so getting some vectorized version of this could be super useful on top of being more legible.

To your point though I suppose this only works if there's one coefficient? Though I could imagine a function as such just takes the variable name as its first argument and any successive arguments are all considered to be possible coefficients? And if another variable in that constraint needs updating, can be done as a separate call?

@simonbowly simonbowly added enhancement New feature or request labels Jun 26, 2024
@simonbowly
Copy link
Member

Got it, so say we have a simple dataframe built like this:

df = (
    pd.DataFrame({
        "price1": [11, 12, 13, 14, 15],
        "price2": [21, 22, 23, 24, 25],
    })
    .gppd.add_vars(model, name="x")
    .gppd.add_vars(model, name="y")
    .gppd.add_constrs(model, "y == price1 * x", name="c1")
)

result:

|    |   price1 |   price2 | x                 | y                 | c1                    |
|---:|---------:|---------:|:------------------|:------------------|:----------------------|
|  0 |       11 |       21 | <gurobi.Var x[0]> | <gurobi.Var y[0]> | <gurobi.Constr c1[0]> |
|  1 |       12 |       22 | <gurobi.Var x[1]> | <gurobi.Var y[1]> | <gurobi.Constr c1[1]> |
|  2 |       13 |       23 | <gurobi.Var x[2]> | <gurobi.Var y[2]> | <gurobi.Constr c1[2]> |
|  3 |       14 |       24 | <gurobi.Var x[3]> | <gurobi.Var y[3]> | <gurobi.Constr c1[3]> |
|  4 |       15 |       25 | <gurobi.Var x[4]> | <gurobi.Var y[4]> | <gurobi.Constr c1[4]> |

and looking up c1 in the model we'll see the current constraint coefficients:

>>> df["c1"].apply(model.getRow)
0    11.0 x[0] + -1.0 y[0]
1    12.0 x[1] + -1.0 y[1]
2    13.0 x[2] + -1.0 y[2]
3    14.0 x[3] + -1.0 y[3]
4    15.0 x[4] + -1.0 y[4]

To change the coefficients on each 'x' to the corresponding value in 'price2' would just require an itertuples loop as you said:

>>> for tup in df.itertuples():
>>>    model.chgCoeff(tup.c1, tup.x, tup.price2)
>>> model.update()
>>> df["c1"].apply(model.getRow)
0    21.0 x[0] + -1.0 y[0]
1    22.0 x[1] + -1.0 y[1]
2    23.0 x[2] + -1.0 y[2]
3    24.0 x[3] + -1.0 y[3]
4    25.0 x[4] + -1.0 y[4]

We could add a function like df.gppd.update_coeff(model, "c1", "x", "price2") that does just that: for each row in the dataframe, update the coefficient of variable x in constraint c1 to the value in price2. The implementation would just use an itertuples loop as above though, there's no vectorized way to make these changes in the underlying model through gurobipy.

Extending the idea a little to multiple coefficients per constraint, I think the possibilities would be:

  1. Use a separate call (e.g. to also update all the y coefficients based on another column)
  2. Somehow munge the dataframe so you can use the same syntax

For (2), you could use the same syntax (one update per row) if you construct the right dataframe, e.g.

>>> df2 = pd.concat([df.loc[:2, "x"], df.loc[3:, "y"]]).to_frame(name="v").assign(coeff=0.1, c1=df['c1'])
>>> df2
                   v  coeff                     c1
0  <gurobi.Var x[0]>    0.1  <gurobi.Constr c1[0]>
1  <gurobi.Var x[1]>    0.1  <gurobi.Constr c1[1]>
2  <gurobi.Var x[2]>    0.1  <gurobi.Constr c1[2]>
3  <gurobi.Var y[3]>    0.1  <gurobi.Constr c1[3]>
4  <gurobi.Var y[4]>    0.1  <gurobi.Constr c1[4]>
>>> for tup in df2.itertuples():
>>>    model.chgCoeff(tup.c1, tup.v, tup.coeff)
>>> model.update()
>>> df["c1"].apply(model.getRow)
0    0.1 x[0] + -1.0 y[0]
1    0.1 x[1] + -1.0 y[1]
2    0.1 x[2] + -1.0 y[2]
3    24.0 x[3] + 0.1 y[3]
4    25.0 x[4] + 0.1 y[4]
Name: c1, dtype: object

(A separate call might be cleaner if the code to build the update dataframe looks anything like the one above!)

@alexlusak
Copy link
Author

Got it, that all makes sense. Probably not the biggest priority given the simplicity of writing an itertuples call (and especially given there's no way to have it vectorized), and I know this is documented in the Advanced Patterns section.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants