PyPSA / linopy

Linear optimization with N-D labeled arrays in Python
https://linopy.readthedocs.io
MIT License
154 stars 42 forks source link

Indicate applied masking in string representation of constraints #237

Closed apfelix closed 4 months ago

apfelix commented 4 months ago

Hi,

I just started my first modelling experiment with linopy and wanted to use masking for constraints. As the model I'm creating is rather large, I printed the constraints I was adding and wanted to check if my masking has worked (as I already did with masks for creating variables).

However, in the prints, there was no indication that I had masked some of the constraints. Also the mask attribute was returning the value None. When testing this on a small example and printing the model to an .lp file, I realised that the masking actually works, but there is just no indication of it in the string representation (or anywhere else I could find it).

Some example code to reproduce the issue based on https://linopy.readthedocs.io/en/latest/transport-tutorial.html

import linopy
import xarray as xr

# Creation of a Model
m = linopy.Model()

i = {"Canning Plants": ["seattle", "san-diego"]}
j = {"Markets": ["new-york", "chicago", "topeka"]}

a = xr.DataArray([350, 600], coords=i, name="capacity of plant i in cases")
b = xr.DataArray([325, 300, 275], coords=j, name="demand at market j in cases")

# change seattle -> new-york distance from 2.5 in the example to 3.5
# to see a different result when masking the plant capacity constraint
d = xr.DataArray(
    [[2.5, 1.7, 1.8], [3.5, 1.8, 1.4]], coords=i | j, name="distance in thousands of miles"
)

f = 90  # Freight in dollars per case per thousand miles

c = d * f / 1000
c.name = "transport cost in thousands of dollars per case"

x = m.add_variables(lower=0.0, coords=c.coords, name="Shipment quantities in cases")

con = x.sum(dims="Markets") <= a

# only apply plant limit for plants over 400 capacity
mask_for_1 = a > 400
con1 = m.add_constraints(con, name="Observe supply limit at plant i", mask=mask_for_1)

print(f"{con1=}")
print(f"{con1.mask=}")

con = x.sum(dims="Canning Plants") >= b
con2 = m.add_constraints(con, name="Satisfy demand at market j")

obj = c * x
m.add_objective(obj)

# print model to lp
m.to_file("linopy_transport_example_test.lp")

Printing lists two constraints (one for each element in i):

con1=Constraint `Observe supply limit at plant i` (Canning Plants: 2):
-----------------------------------------------------------------
[seattle]: +1 Shipment quantities in cases[seattle, new-york] + 1 Shipment quantities in cases[seattle, chicago] + 1 Shipment quantities in cases[seattle, topeka]         ≤ 350.0
[san-diego]: +1 Shipment quantities in cases[san-diego, new-york] + 1 Shipment quantities in cases[san-diego, chicago] + 1 Shipment quantities in cases[san-diego, topeka] ≤ 600.0
con1.mask=None

However, the .lp file shows only one of them:

[...]
s.t.

c1:
+1 x3
+1 x4
+1 x5
<= +600
[...]

Also solving the model gives different results based on whether masking is applied or not.

What I would have expected would be prints as for masked variables. Changing the variable creation above to

mask_x = c > 0.2
x = m.add_variables(
    lower=0.0,
    coords=c.coords,
    mask=mask_x,
    name="Shipment quantities in cases",
)
print(x)

gives the following output

Variable (Canning Plants: 2, Markets: 3) - 4 masked entries
-----------------------------------------------------------
[seattle, new-york]: Shipment quantities in cases[seattle, new-york] ∈ [0, inf]
[seattle, chicago]: None
[seattle, topeka]: None
[san-diego, new-york]: Shipment quantities in cases[san-diego, new-york] ∈ [0, inf]
[san-diego, chicago]: None
[san-diego, topeka]: None
FabianHofmann commented 4 months ago

@apfelix, thanks for the nice reproducible example! There was a problem with the mask attribute getter in the constraint class. Could you try with the current master and give feedback if everything works as expected?

apfelix commented 4 months ago

Thanks for the quick fix, looks good in general! However, I think the reported number of masked constraints is wrong.

When testing with

mask_for_2 = b == 300
con = x.sum(dims="Canning Plants") >= b
con2 = m.add_constraints(con, name="Satisfy demand at market j", mask=mask_for_2)

print(f"{con2=}")

gives

con2=Constraint `Satisfy demand at market j` (Markets: 3) - 1 masked entries:
------------------------------------------------------------------------
[new-york]: None
[chicago]: +1 Shipment quantities in cases[seattle, chicago] + 1 Shipment quantities in cases[san-diego, chicago] ≥ 300.0
[topeka]: None

but it should be 2 masked entries at the end of the first row. I think the number of not-masked constraints is shown :)

FabianHofmann commented 4 months ago

argh, thanks, did not see that. It will make it in the next release