core
Constraint(lhs, sense)
Bases: ModelElementWithId
A linear programming constraint.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
lhs
|
Expression
|
Expression The left hand side of the constraint. |
required |
sense
|
ConstraintSense
|
Sense The sense of the constraint. |
required |
Source code in pyoframe/core.py
slack
property
writable
The slack of the constraint. Will raise an error if the model has not already been solved. The first call to this property will load the slack values from the solver (lazy loading).
relax(cost, max=None)
Relaxes the constraint by adding a variable to the constraint that can be non-zero at a cost.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
cost
|
SupportsToExpr
|
SupportsToExpr The cost of relaxing the constraint. Costs should be positives as they will automatically become negative for maximization problems. |
required |
max
|
Optional[SupportsToExpr]
|
SupportsToExpr, default None The maximum value of the relaxation variable. |
None
|
Returns:
Type | Description |
---|---|
Constraint
|
The same constraint |
Examples:
>>> import pyoframe as pf
>>> m = pf.Model("max")
>>> homework_due_tomorrow = pl.DataFrame({"project": ["A", "B", "C"], "cost_per_hour_underdelivered": [10, 20, 30], "hours_to_finish": [9, 9, 9], "max_underdelivered": [1, 9, 9]})
>>> m.hours_spent = pf.Variable(homework_due_tomorrow[["project"]], lb=0)
>>> m.must_finish_project = m.hours_spent >= homework_due_tomorrow[["project", "hours_to_finish"]]
>>> m.only_one_day = sum("project", m.hours_spent) <= 24
>>> _ = m.must_finish_project.relax(homework_due_tomorrow[["project", "cost_per_hour_underdelivered"]], max=homework_due_tomorrow[["project", "max_underdelivered"]])
>>> _ = m.solve(log_to_console=False)
Writing ...
>>> m.hours_spent.solution
shape: (3, 2)
┌─────────┬──────────┐
│ project ┆ solution │
│ --- ┆ --- │
│ str ┆ f64 │
╞═════════╪══════════╡
│ A ┆ 8.0 │
│ B ┆ 7.0 │
│ C ┆ 9.0 │
└─────────┴──────────┘
>>> # It can also be done all in one go!
>>> m = pf.Model("max")
>>> homework_due_tomorrow = pl.DataFrame({"project": ["A", "B", "C"], "cost_per_hour_underdelivered": [10, 20, 30], "hours_to_finish": [9, 9, 9], "max_underdelivered": [1, 9, 9]})
>>> m.hours_spent = pf.Variable(homework_due_tomorrow[["project"]], lb=0)
>>> m.must_finish_project = (m.hours_spent >= homework_due_tomorrow[["project", "hours_to_finish"]]).relax(5)
>>> m.only_one_day = (sum("project", m.hours_spent) <= 24).relax(1)
>>> _ = m.solve(log_to_console=False)
Writing ...
>>> m.objective.value
-3.0
>>> m.hours_spent.solution
shape: (3, 2)
┌─────────┬──────────┐
│ project ┆ solution │
│ --- ┆ --- │
│ str ┆ f64 │
╞═════════╪══════════╡
│ A ┆ 9.0 │
│ B ┆ 9.0 │
│ C ┆ 9.0 │
└─────────┴──────────┘
Source code in pyoframe/core.py
1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 |
|
Expression(data)
Bases: ModelElement
, SupportsMath
, SupportPolarsMethodMixin
A linear expression.
df = pd.DataFrame({"item" : [1, 1, 1, 2, 2], "time": ["mon", "tue", "wed", "mon", "tue"], "cost": [1, 2, 3, 4, 5]}).set_index(["item", "time"]) m = Model("min") m.Time = Variable(df.index) m.Size = Variable(df.index) expr = df["cost"] * m.Time + df["cost"] * m.Size expr
[1,mon]: Time[1,mon] + Size[1,mon][1,tue]: 2 Time[1,tue] +2 Size[1,tue][1,wed]: 3 Time[1,wed] +3 Size[1,wed][2,mon]: 4 Time[2,mon] +4 Size[2,mon][2,tue]: 5 Time[2,tue] +5 Size[2,tue]
Source code in pyoframe/core.py
value: pl.DataFrame
property
The value of the expression. Only available after the model has been solved.
Examples:
>>> import pyoframe as pf
>>> m = pf.Model("max")
>>> m.X = pf.Variable({"dim1": [1, 2, 3]}, ub=10)
>>> m.expr_1 = 2 * m.X + 1
>>> m.expr_2 = pf.sum(m.expr_1)
>>> m.objective = m.expr_2 - 3
>>> result = m.solve(log_to_console=False)
...
>>> m.expr_1.value
shape: (3, 2)
┌──────┬──────────┐
│ dim1 ┆ solution │
│ --- ┆ --- │
│ i64 ┆ f64 │
╞══════╪══════════╡
│ 1 ┆ 21.0 │
│ 2 ┆ 21.0 │
│ 3 ┆ 21.0 │
└──────┴──────────┘
>>> m.expr_2.value
63.0
__add__(other)
Examples:
>>> import pandas as pd
>>> from pyoframe import Variable
>>> add = pd.DataFrame({"dim1": [1,2,3], "add": [10, 20, 30]}).to_expr()
>>> var = Variable(add)
>>> var + add
<Expression size=3 dimensions={'dim1': 3} terms=6>
[1]: x1 +10
[2]: x2 +20
[3]: x3 +30
>>> var + add + 2
<Expression size=3 dimensions={'dim1': 3} terms=6>
[1]: x1 +12
[2]: x2 +22
[3]: x3 +32
>>> var + pd.DataFrame({"dim1": [1,2], "add": [10, 20]})
Traceback (most recent call last):
...
pyoframe.constants.PyoframeError: Failed to add expressions:
<Expression size=3 dimensions={'dim1': 3} terms=3> + <Expression size=2 dimensions={'dim1': 2} terms=2>
Due to error:
Dataframe has unmatched values. If this is intentional, use .drop_unmatched() or .keep_unmatched()
shape: (1, 2)
┌──────┬────────────┐
│ dim1 ┆ dim1_right │
│ --- ┆ --- │
│ i64 ┆ i64 │
╞══════╪════════════╡
│ 3 ┆ null │
└──────┴────────────┘
>>> 5 + 2 * Variable()
<Expression size=1 dimensions={} terms=2>
2 x4 +5
Source code in pyoframe/core.py
map(mapping_set, drop_shared_dims=True)
Replaces the dimensions that are shared with mapping_set with the other dimensions found in mapping_set.
This is particularly useful to go from one type of dimensions to another. For example, to convert data that is indexed by city to data indexed by country (see example).
Parameters:
Name | Type | Description | Default |
---|---|---|---|
mapping_set
|
SetTypes The set to map the expression to. This can be a DataFrame, Index, or another Set. |
required | |
drop_shared_dims
|
bool, default True If True, the dimensions shared between the expression and the mapping set are dropped from the resulting expression and repeated rows are summed. If False, the shared dimensions are kept in the resulting expression. |
True
|
Returns:
Type | Description |
---|---|
Expression A new Expression containing the result of the mapping operation. |
Examples:
import polars as pl from pyoframe import Variable, Model pop_data = pl.DataFrame({"city": ["Toronto", "Vancouver", "Boston"], "year": [2024, 2024, 2024], "population": [10, 2, 8]}).to_expr() cities_and_countries = pl.DataFrame({"city": ["Toronto", "Vancouver", "Boston"], "country": ["Canada", "Canada", "USA"]}) pop_data.map(cities_and_countries)
pop_data.map(cities_and_countries, drop_shared_dims=False)
Source code in pyoframe/core.py
rolling_sum(over, window_size)
Calculates the rolling sum of the Expression over a specified window size for a given dimension.
This method applies a rolling sum operation over the dimension specified by over
,
using a window defined by window_size
.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
over
|
str The name of the dimension (column) over which the rolling sum is calculated. This dimension must exist within the Expression's dimensions. |
required | |
window_size
|
int The size of the moving window in terms of number of records. The rolling sum is calculated over this many consecutive elements. |
required |
Returns:
Type | Description |
---|---|
Expression A new Expression instance containing the result of the rolling sum operation. This new Expression retains all dimensions (columns) of the original data, with the rolling sum applied over the specified dimension. |
Examples:
>>> import polars as pl
>>> from pyoframe import Variable, Model
>>> cost = pl.DataFrame({"item" : [1, 1, 1, 2, 2], "time": [1, 2, 3, 1, 2], "cost": [1, 2, 3, 4, 5]})
>>> m = Model("min")
>>> m.quantity = Variable(cost[["item", "time"]])
>>> (m.quantity * cost).rolling_sum(over="time", window_size=2)
<Expression size=5 dimensions={'item': 2, 'time': 3} terms=8>
[1,1]: quantity[1,1]
[1,2]: quantity[1,1] +2 quantity[1,2]
[1,3]: 2 quantity[1,2] +3 quantity[1,3]
[2,1]: 4 quantity[2,1]
[2,2]: 4 quantity[2,1] +5 quantity[2,2]
Source code in pyoframe/core.py
sum(over)
Examples:
>>> import pandas as pd
>>> from pyoframe import Variable
>>> df = pd.DataFrame({"item" : [1, 1, 1, 2, 2], "time": ["mon", "tue", "wed", "mon", "tue"], "cost": [1, 2, 3, 4, 5]}).set_index(["item", "time"])
>>> quantity = Variable(df.reset_index()[["item"]].drop_duplicates())
>>> expr = (quantity * df["cost"]).sum("time")
>>> expr.data
shape: (2, 3)
┌──────┬─────────┬───────────────┐
│ item ┆ __coeff ┆ __variable_id │
│ --- ┆ --- ┆ --- │
│ i64 ┆ f64 ┆ u32 │
╞══════╪═════════╪═══════════════╡
│ 1 ┆ 6.0 ┆ 1 │
│ 2 ┆ 9.0 ┆ 2 │
└──────┴─────────┴───────────────┘
Source code in pyoframe/core.py
within(set)
Examples
import pandas as pd general_expr = pd.DataFrame({"dim1": [1, 2, 3], "value": [1, 2, 3]}).to_expr() filter_expr = pd.DataFrame({"dim1": [1, 3], "value": [5, 6]}).to_expr() general_expr.within(filter_expr).data shape: (2, 3) ┌──────┬─────────┬───────────────┐ │ dim1 ┆ __coeff ┆ __variable_id │ │ --- ┆ --- ┆ --- │ │ i64 ┆ f64 ┆ u32 │ ╞══════╪═════════╪═══════════════╡ │ 1 ┆ 1.0 ┆ 0 │ │ 3 ┆ 3.0 ┆ 0 │ └──────┴─────────┴───────────────┘
Source code in pyoframe/core.py
Set(*data, **named_data)
Bases: ModelElement
, SupportsMath
, SupportPolarsMethodMixin
A set which can then be used to index variables.
Examples:
>>> import pyoframe as pf
>>> pf.Set(x=range(2), y=range(3))
<Set size=6 dimensions={'x': 2, 'y': 3}>
[(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2)]
Source code in pyoframe/core.py
SupportsMath(**kwargs)
Bases: ABC
, SupportsToExpr
Any object that can be converted into an expression.
Source code in pyoframe/core.py
__eq__(value)
Equality constraint. Examples
from pyoframe import Variable Variable() == 1
x1 = 1
Source code in pyoframe/core.py
__ge__(other)
Equality constraint. Examples
from pyoframe import Variable Variable() >= 1
__le__(other)
Equality constraint. Examples
from pyoframe import Variable Variable() <= 1 <Constraint sense='<=' size=1 dimensions={} terms=2> x1 <= 1
__rsub__(other)
Support right subtraction.
from pyoframe import Variable var = Variable({"dim1": [1,2,3]}) 1 - var
[1]: 1 - x1 [2]: 1 - x2 [3]: 1 - x3
Source code in pyoframe/core.py
__sub__(other)
import polars as pl from pyoframe import Variable df = pl.DataFrame({"dim1": [1,2,3], "value": [1,2,3]}) var = Variable(df["dim1"]) var - df
[1]: x1 -1 [2]: x2 -2 [3]: x3 -3
Source code in pyoframe/core.py
__truediv__(other)
Support division.
from pyoframe import Variable var = Variable({"dim1": [1,2,3]}) var / 2
[1]: 0.5 x1 [2]: 0.5 x2 [3]: 0.5 x3
Source code in pyoframe/core.py
Variable(*indexing_sets, lb=None, ub=None, vtype=VType.CONTINUOUS, equals=None)
Bases: ModelElementWithId
, SupportsMath
, SupportPolarsMethodMixin
Represents one or many decision variable in an optimization model.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
*indexing_sets
|
SetTypes | Iterable[SetTypes]
|
SetTypes (typically a DataFrame or Set) If no indexing_sets are provided, a single variable with no dimensions is created. Otherwise, a variable is created for each element in the Cartesian product of the indexing_sets (see Set for details on behaviour). |
()
|
lb
|
float | int | SupportsToExpr | None
|
float The lower bound for all variables. |
None
|
ub
|
float | int | SupportsToExpr | None
|
float The upper bound for all variables. |
None
|
vtype
|
VType | VTypeValue
|
VType | VTypeValue The type of the variable. Can be either a VType enum or a string. Default is VType.CONTINUOUS. |
CONTINUOUS
|
equals
|
Optional[SupportsMath]
|
SupportsToExpr When specified, a variable is created and a constraint is added to make the variable equal to the provided expression. |
None
|
Examples:
>>> import pandas as pd
>>> from pyoframe import Variable
>>> df = pd.DataFrame({"dim1": [1, 1, 2, 2, 3, 3], "dim2": ["a", "b", "a", "b", "a", "b"]})
>>> Variable(df)
<Variable lb=-inf ub=inf size=6 dimensions={'dim1': 3, 'dim2': 2}>
[1,a]: x1
[1,b]: x2
[2,a]: x3
[2,b]: x4
[3,a]: x5
[3,b]: x6
>>> Variable(df[["dim1"]])
Traceback (most recent call last):
...
ValueError: Duplicate rows found in input data.
>>> Variable(df[["dim1"]].drop_duplicates())
<Variable lb=-inf ub=inf size=3 dimensions={'dim1': 3}>
[1]: x7
[2]: x8
[3]: x9
Source code in pyoframe/core.py
RC
property
writable
The reduced cost of the variable. Will raise an error if the model has not already been solved. The first call to this property will load the reduced costs from the solver (lazy loading).
next(dim, wrap_around=False)
Creates an expression where the variable at each index is the next variable in the specified dimension.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
dim
|
str
|
The dimension over which to shift the variable. |
required |
wrap_around
|
bool
|
If True, the last index in the dimension is connected to the first index. |
False
|
Examples:
>>> import pandas as pd
>>> from pyoframe import Variable, Model
>>> time_dim = pd.DataFrame({"time": ["00:00", "06:00", "12:00", "18:00"]})
>>> space_dim = pd.DataFrame({"city": ["Toronto", "Berlin"]})
>>> m = Model("min")
>>> m.bat_charge = Variable(time_dim, space_dim)
>>> m.bat_flow = Variable(time_dim, space_dim)
>>> # Fails because the dimensions are not the same
>>> m.bat_charge + m.bat_flow == m.bat_charge.next("time")
Traceback (most recent call last):
...
pyoframe.constants.PyoframeError: Failed to add expressions:
<Expression size=8 dimensions={'time': 4, 'city': 2} terms=16> + <Expression size=6 dimensions={'city': 2, 'time': 3} terms=6>
Due to error:
Dataframe has unmatched values. If this is intentional, use .drop_unmatched() or .keep_unmatched()
shape: (2, 4)
┌───────┬─────────┬────────────┬────────────┐
│ time ┆ city ┆ time_right ┆ city_right │
│ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ str ┆ str ┆ str │
╞═══════╪═════════╪════════════╪════════════╡
│ 18:00 ┆ Toronto ┆ null ┆ null │
│ 18:00 ┆ Berlin ┆ null ┆ null │
└───────┴─────────┴────────────┴────────────┘
>>> (m.bat_charge + m.bat_flow).drop_unmatched() == m.bat_charge.next("time")
<Constraint sense='=' size=6 dimensions={'time': 3, 'city': 2} terms=18>
[00:00,Berlin]: bat_charge[00:00,Berlin] + bat_flow[00:00,Berlin] - bat_charge[06:00,Berlin] = 0
[00:00,Toronto]: bat_charge[00:00,Toronto] + bat_flow[00:00,Toronto] - bat_charge[06:00,Toronto] = 0
[06:00,Berlin]: bat_charge[06:00,Berlin] + bat_flow[06:00,Berlin] - bat_charge[12:00,Berlin] = 0
[06:00,Toronto]: bat_charge[06:00,Toronto] + bat_flow[06:00,Toronto] - bat_charge[12:00,Toronto] = 0
[12:00,Berlin]: bat_charge[12:00,Berlin] + bat_flow[12:00,Berlin] - bat_charge[18:00,Berlin] = 0
[12:00,Toronto]: bat_charge[12:00,Toronto] + bat_flow[12:00,Toronto] - bat_charge[18:00,Toronto] = 0
>>> (m.bat_charge + m.bat_flow) == m.bat_charge.next("time", wrap_around=True)
<Constraint sense='=' size=8 dimensions={'time': 4, 'city': 2} terms=24>
[00:00,Berlin]: bat_charge[00:00,Berlin] + bat_flow[00:00,Berlin] - bat_charge[06:00,Berlin] = 0
[00:00,Toronto]: bat_charge[00:00,Toronto] + bat_flow[00:00,Toronto] - bat_charge[06:00,Toronto] = 0
[06:00,Berlin]: bat_charge[06:00,Berlin] + bat_flow[06:00,Berlin] - bat_charge[12:00,Berlin] = 0
[06:00,Toronto]: bat_charge[06:00,Toronto] + bat_flow[06:00,Toronto] - bat_charge[12:00,Toronto] = 0
[12:00,Berlin]: bat_charge[12:00,Berlin] + bat_flow[12:00,Berlin] - bat_charge[18:00,Berlin] = 0
[12:00,Toronto]: bat_charge[12:00,Toronto] + bat_flow[12:00,Toronto] - bat_charge[18:00,Toronto] = 0
[18:00,Berlin]: bat_charge[18:00,Berlin] + bat_flow[18:00,Berlin] - bat_charge[00:00,Berlin] = 0
[18:00,Toronto]: bat_charge[18:00,Toronto] + bat_flow[18:00,Toronto] - bat_charge[00:00,Toronto] = 0
Source code in pyoframe/core.py
1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 |
|