Multiscale MILP (using Attributes)#
This is a continuation of ‘The Scheduling Example’, refer to learn the basics on how Energia models processes.
In this example, we add another Process [Solar PV] and Storage [Li-ion Battery]. Technology choice is modeled using binaries. Moreover, the model is multiscale as the operational capacities are decision variables.
# !pip install energiapy # uncomment and run to install Energia, if not in environment
from energia import *
m = Model('design_scheduling_w_attrs')
m.q = Periods()
m.y = 4 * m.q
m.usd = Currency()
Resources#
Set bounds on Resource flows#
Unlike wind which has bound on the total consumption, we set a daily limit on solar energy. The same bound is repeated in each quarter. The following constraints are written.
\(\mathbf{cons}_{solar, network, quarter_0} \leq 100\)
\(\mathbf{cons}_{solar, network, quarter_1} \leq 100\)
\(\mathbf{cons}_{solar, network, quarter_2} \leq 100\)
\(\mathbf{cons}_{solar, network, quarter_3} \leq 100\)
m.solar = Resource(consume_max=[100] * 4)
m.wind = Resource(consume_max=100 * 4)
m.power = Resource(demand_nominal=180, demand_min=[0.6, 0.7, 0.8, 0.3])
⚖ Initiated solar balance in (l0, q) ⏱ 0.0001 s
🔗 Bound [≤] solar consume in (l0, q) ⏱ 0.0009 s
⚖ Initiated wind balance in (l0, y) ⏱ 0.0001 s
🔗 Bound [≤] wind consume in (l0, y) ⏱ 0.0010 s
⚖ Initiated power balance in (l0, q) ⏱ 0.0001 s
🔗 Bound [≥] power release in (l0, q) ⏱ 0.0008 s
Operations#
Capacity as a variable#
Here we want the optimization problem to determine the optimal capacity. Moreover, we set binaries to avoid the lower bound being adhered to if the process is not set up.
If the bounds are meant to be compulsory limits, skip the .x
m.wf = Process(
m.power == -1.0 * m.wind,
capacity_max=100,
capacity_min=10,
capacity_optional=True,
operate_normalize=True,
operate_max=[0.9, 0.8, 0.5, 0.7],
capex=990637 + 3354,
opex=49,
)
🔗 Bound [≤] wf capacity in (l0, y) ⏱ 0.0002 s
🔗 Bound [≥] wf capacity in (l0, y) ⏱ 0.0003 s
🔗 Bound [≤] wf operate in (l0, q) ⏱ 0.0005 s
🔗 Bound [=] usd spend in (l0, y) ⏱ 0.0003 s
🧭 Mapped time for operate (wf, l0, q) ⟺ (wf, l0, y) ⏱ 0.0002 s
🔗 Bound [=] usd spend in (l0, y) ⏱ 0.0002 s
Unlike in Example 1, where the capacity was know, capacity is a variable here.
Moreover, the expenditure associated with operating and capacitating are different
m.pv = Process(
m.power == -1 * m.solar,
capacity_max=100,
capacity_min=10,
capacity_optional=True,
operate_normalize=True,
operate_max=[0.6, 0.8, 0.9, 0.7],
capex=567000 + 872046,
opex=90000,
)
🔗 Bound [≤] pv capacity in (l0, y) ⏱ 0.0003 s
🔗 Bound [≥] pv capacity in (l0, y) ⏱ 0.0002 s
🔗 Bound [≤] pv operate in (l0, q) ⏱ 0.0003 s
🔗 Bound [=] usd spend in (l0, y) ⏱ 0.0002 s
🧭 Mapped time for operate (pv, l0, q) ⟺ (pv, l0, y) ⏱ 0.0002 s
🔗 Bound [=] usd spend in (l0, y) ⏱ 0.0003 s
Storage Operation#
energia now allows storing to require the use of other resources, example power for hydrogen cryogenic storage.
Provide an equation similar to Process, in this case the basis is the stored resource If no other resource is provided, it is assumed to be the charging/discharging efficiency
Note that the following are created internally:
auxilary resource with name resource.stored
charging and discharging processes as storage.charge and storage.discharge
The parameters for each of these can be set individually, thus allowing for a wide range of modeling approaches
m.lii = Storage(
m.power == 0.9,
capacity_max=100,
capacity_min=10,
capacity_optional=True,
charge_capacity_max=100,
discharge_capacity_max=100,
capex=1302182 + 41432,
inventory_cost=2000,
)
🔗 Bound [≤] lii.charge capacity in (l0, y) ⏱ 0.0003 s
🔗 Bound [≤] lii.discharge capacity in (l0, y) ⏱ 0.0003 s
🔗 Bound [≤] lii.stored invcapacity in (l0, y) ⏱ 0.0003 s
🔗 Bound [≥] lii.stored invcapacity in (l0, y) ⏱ 0.0002 s
🔗 Bound [=] usd spend in (l0, y) ⏱ 0.0003 s
🔗 Bound [=] usd spend in (l0, y) ⏱ 0.0003 s
Locating Operations#
Operations can be located as
operation.locate(<list of locations>)
or
m.location.operations(<list of operations>)
They both do the same thing
m.locate(m.wf, m.pv, m.lii)
💡 Assumed wf capacity unbounded in (l0, y) ⏱ 0.0001 s
💡 Assumed wf operate bounded by capacity in (l0, q) ⏱ 0.0001 s
⚖ Updated power balance with produce(power, l0, q, operate, wf) ⏱ 0.0001 s
🔗 Bound [=] power produce in (l0, q) ⏱ 0.0009 s
⚖ Updated wind balance with expend(wind, l0, y, operate, wf) ⏱ 0.0001 s
🔗 Bound [=] wind expend in (l0, y) ⏱ 0.0007 s
🏭 Operating streams introduced for wf in l0 ⏱ 0.0026 s
🏗 Construction streams introduced for wf in l0 ⏱ 0.0000 s
🌍 Located wf in l0 ⏱ 0.0042 s
💡 Assumed pv capacity unbounded in (l0, y) ⏱ 0.0001 s
💡 Assumed pv operate bounded by capacity in (l0, q) ⏱ 0.0001 s
⚖ Updated power balance with produce(power, l0, q, operate, pv) ⏱ 0.0001 s
🔗 Bound [=] power produce in (l0, q) ⏱ 0.0010 s
⚖ Updated solar balance with expend(solar, l0, q, operate, pv) ⏱ 0.0001 s
🔗 Bound [=] solar expend in (l0, q) ⏱ 0.0010 s
🏭 Operating streams introduced for pv in l0 ⏱ 0.0031 s
🏗 Construction streams introduced for pv in l0 ⏱ 0.0000 s
🌍 Located pv in l0 ⏱ 0.0060 s
💡 Assumed lii capacity unbounded in (l0, y) ⏱ 0.0001 s
🧭 Mapped time for inventory (lii.stored, l0, q) ⟺ (lii.stored, l0, y) ⏱ 0.0002 s
⚖ Initiated lii.stored balance in (l0, q) ⏱ 0.0005 s
🔗 Bound [≤] lii.stored inventory in (l0, q) ⏱ 0.0020 s
💡 Assumed lii inventory bounded by capacity in (l0, q) ⏱ 0.0025 s
💡 Assumed lii.charge capacity unbounded in (l0, y) ⏱ 0.0001 s
🔗 Bound [≤] lii.charge operate in (l0, y) ⏱ 0.0005 s
💡 Assumed lii.charge operate bounded by capacity in (l0, y) ⏱ 0.0010 s
🧭 Mapped time for operate (lii.charge, l0, q) ⟺ (lii.charge, l0, y) ⏱ 0.0003 s
⚖ Updated lii.stored balance with produce(lii.stored, l0, q, operate, lii.charge) ⏱ 0.0001 s
🔗 Bound [=] lii.stored produce in (l0, q) ⏱ 0.0009 s
⚖ Updated power balance with expend(power, l0, q, operate, lii.charge) ⏱ 0.0001 s
🔗 Bound [=] power expend in (l0, q) ⏱ 0.0010 s
🏭 Operating streams introduced for lii.charge in l0 ⏱ 0.0039 s
🏗 Construction streams introduced for lii.charge in l0 ⏱ 0.0000 s
🌍 Located lii.charge in l0 ⏱ 0.0065 s
💡 Assumed lii.discharge capacity unbounded in (l0, y) ⏱ 0.0001 s
🔗 Bound [≤] lii.discharge operate in (l0, y) ⏱ 0.0003 s
💡 Assumed lii.discharge operate bounded by capacity in (l0, y) ⏱ 0.0008 s
🧭 Mapped time for operate (lii.discharge, l0, q) ⟺ (lii.discharge, l0, y) ⏱ 0.0002 s
⚖ Updated power balance with produce(power, l0, q, operate, lii.discharge) ⏱ 0.0002 s
🔗 Bound [=] power produce in (l0, q) ⏱ 0.0013 s
⚖ Updated lii.stored balance with expend(lii.stored, l0, q, operate, lii.discharge) ⏱ 0.0001 s
🔗 Bound [=] lii.stored expend in (l0, q) ⏱ 0.0009 s
🏭 Operating streams introduced for lii.discharge in l0 ⏱ 0.0044 s
🏗 Construction streams introduced for lii.discharge in l0 ⏱ 0.0000 s
🌍 Located lii.discharge in l0 ⏱ 0.0067 s
🏗 Construction streams introduced for lii in l0 ⏱ 0.0000 s
🌍 Located lii in l0 ⏱ 0.0178 s
Inventory Balance#
Inventory is passed on from one time period (t - 1) to the next (t) and hence features in the general resource balance for resource.stored
Optimize!#
m.usd.spend.opt()
🧭 Mapped samples for spend (usd, l0, y, capacity, wf) ⟺ (usd, l0, y) ⏱ 0.0002 s
🧭 Mapped samples for spend (usd, l0, y, operate, wf) ⟺ (usd, l0, y) ⏱ 0.0002 s
🧭 Mapped samples for spend (usd, l0, y, capacity, pv) ⟺ (usd, l0, y) ⏱ 0.0003 s
🧭 Mapped samples for spend (usd, l0, y, operate, pv) ⟺ (usd, l0, y) ⏱ 0.0002 s
🧭 Mapped samples for spend (usd, l0, y, invcapacity, lii.stored) ⟺ (usd, l0, y) ⏱ 0.0002 s
🧭 Mapped samples for spend (usd, l0, y, inventory, lii.stored) ⟺ (usd, l0, y) ⏱ 0.0002 s
📝 Generated Program(design_scheduling_w_attrs).mps ⏱ 0.0051 s
Read MPS format model from file Program(design_scheduling_w_attrs).mps
Reading time = 0.01 seconds
PROGRAM(DESIGN_SCHEDULING_W_ATTRS): 85 rows, 78 columns, 198 nonzeros
📝 Generated gurobipy model. See .formulation ⏱ 0.0165 s
Gurobi Optimizer version 12.0.3 build v12.0.3rc0 (win64 - Windows 11.0 (26100.2))
CPU model: 13th Gen Intel(R) Core(TM) i7-13700, instruction set [SSE2|AVX|AVX2]
Thread count: 16 physical cores, 24 logical processors, using up to 24 threads
Optimize a model with 85 rows, 78 columns and 198 nonzeros
Model fingerprint: 0x0c4004ab
Variable types: 75 continuous, 3 integer (3 binary)
Coefficient statistics:
Matrix range [6e-01, 1e+06]
Objective range [1e+00, 1e+00]
Bounds range [1e+00, 1e+00]
RHS range [7e+01, 4e+02]
Presolve removed 70 rows and 63 columns
Presolve time: 0.00s
Presolved: 15 rows, 15 columns, 44 nonzeros
Variable types: 15 continuous, 0 integer (0 binary)
Root relaxation: objective 3.006497e+08, 4 iterations, 0.00 seconds (0.00 work units)
Nodes | Current Node | Objective Bounds | Work
Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time
* 0 0 0 3.006497e+08 3.0065e+08 0.00% - 0s
Explored 1 nodes (4 simplex iterations) in 0.01 seconds (0.00 work units)
Thread count was 24 (of 24 available processors)
Solution count 1: 3.0065e+08
Optimal solution found (tolerance 1.00e-04)
Best objective 3.006497358951e+08, best bound 3.006497358951e+08, gap 0.0000%
📝 Generated Solution object for Program(design_scheduling_w_attrs). See .solution ⏱ 0.0005 s
✅ Program(design_scheduling_w_attrs) optimized using gurobi. Display using .output() ⏱ 0.0285 s
m.show(True)
Mathematical Program for Program(design_scheduling_w_attrs)
Index Sets
Objective
s.t.
Balance Constraints
Binds Constraints
Calculations Constraints
Mapping Constraints
Solution#
Inventory Profiles#
The inventory maintained in each time period is:
m.inventory.output()
The amount charged into inventory is:
m.produce(m.lii.stored, m.lii.charge.operate, m.q).output()
The amount discharged from inventory is:
m.produce(m.power, m.lii.discharge.operate, m.q).output()
Integer Decisions#
All the operations are setup in this case
m.capacity.reporting.output()
m.capacity.output()
m.capacity.output(aslist=True)
[100.0, 100.0, 100.0, 100.0]