A simple model
Here's a simple model to show you Pyoframe's syntax. Click on the buttons to discover what's happening.
import pyoframe as pf
m = pf.Model()
# You can buy tofu or chickpeas
m.tofu = pf.Variable(lb=0) # (1)!
m.chickpeas = pf.Variable(lb=0)
# You want to maximize your protein intake (10g per tofu, 8g per chickpeas)
m.maximize = 10 * m.tofu + 8 * m.chickpeas # (2)!
# You must stay with your $10 budget (4$ per tofu, $2 per chickpeas)
m.budget_constraint = 4 * m.tofu + 2 * m.chickpeas <= 10 # (3)!
m.optimize() # (4)!
print("You should buy:")
print(f"\t{m.tofu.solution} blocks of tofu")
print(f"\t{m.chickpeas.solution} cans of chickpeas")
- Create a variable with a lower bound of zero (
lb=0
) so that you can't buy a negative quantity of tofu! - Define your objective by setting the reserved variables
.maximize
or.minimize
. - Creates constraints by using
<=
,>=
, or==
. - Pyoframe automatically detects your installed solver and optimizes your model!
Use dimensions
The above model would quickly become unworkable if we had more than just tofu and chickpeas. I'll walk you through how we can make a food
dimension to make this scalable. You can also skip to the end to see the example in full!
Note that instead of hardcoding our values, we'll be reading them from the following csv file.
food_data.csv
food protein cost tofu 10 4 chickpeas 8 2
Load your data
Nothing special here. Load your data using your favourite dataframe library. We like Polars because it's fast but Pandas works too.
Create the model
Create an dimensioned variable
Previously, we created two variables: m.tofu
and m.chickpeas
. Instead, we now create a single variable dimensioned over food
.
If you print the variable, you'll see it actually contains a tofu
and chickpeas
variable!
>>> m.Buy
<Variable name=Buy lb=0 size=2 dimensions={'food': 2}>
[tofu]: Buy[tofu]
[chickpeas]: Buy[chickpeas]
Tip
Naming your model's decision variables with an uppercase first letter (e.g. m.Buy
) makes it easier to remember what's a variable and what isn't.
Create the objective
Previously we had:
How do we make use of our dimensioned variable m.Buy
instead?
First, we multiply the variable by the protein amount.
>>> data[["food", "protein"]] * m.Buy
<Expression size=2 dimensions={'food': 2} terms=2>
[tofu]: 10 Buy[tofu]
[chickpeas]: 8 Buy[chickpeas]
Variable
into an Expression
where the coefficients are the protein amounts!
Second, notice that our Expression
still has a food
dimension—it really contains two seperate expressions, one for tofu and one for chickpeas. Our model's objective must be a single expression (without dimensions) so let's sum over the food
dimensions using pf.sum()
.
>>> pf.sum("food", data[["food", "protein"]] * m.Buy)
<Expression size=1 dimensions={} terms=2>
10 Buy[tofu] +8 Buy[chickpeas]
This works and since food
is the only dimensions we don't even need to specify it. Putting it all together:
Adding the constraint
This is similar to how we created the objective, except now we're using cost
and we turn our Expression
into a Constraint
by with the <=
operation.
Putting it all together
import pandas as pd
import pyoframe as pf
data = pd.read_csv("food_data.csv")
m = pf.Model()
m.Buy = pf.Variable(data[["food"]], lb=0)
m.maximize = pf.sum(data[["food", "protein"]] * m.Buy)
m.budget_constraint = pf.sum(data[["food", "cost"]] * m.Buy) <= 10
m.optimize()
So you should buy:
>>> m.Buy.solution
┌───────────┬──────────┐
│ food ┆ solution │
│ --- ┆ --- │
│ str ┆ f64 │
╞═══════════╪══════════╡
│ tofu ┆ 0.0 │
│ chickpeas ┆ 5.0 │
└───────────┴──────────┘
m.Buy
is dimensioned, m.Buy.solution
returned a dataframe with the solution for each of indices!
info
Pyoframe currently always returns Polars dataframes although we plan to add support for returning Pandas dataframes in the future. Upvote the issue if you'd like this feature and in the meantime use .to_pandas()
to convert from a Polars dataframe.