Scheduling#

There is a single process to be optimized for cost [USD] over 4 quarters of a year. There is variability in terms of how much of the (known) process [Wind Farm] capacity can be accessed, and resource (Power) demand.

Initialize#

# !pip install energiapy # uncomment and run to install Energia, if not in environment
from energia import *

m = Model('scheduling')

Time#

We have 4 quarter which form a year

m.q = Periods()
m.y = 4 * m.q

The horizon (\(\overset{\ast}{t} = argmin_{n \in N} |\mathcal{T}_{n}|\)) is determined by Energia implicitly. Check it by printing Model.Horizon

m.horizon
y

Space#

If nothing is provided, a default location is created. The created single location will serve as the de facto network

In single location case studies, it may be easier to skip providing a location all together

m.network
l0

Resources#

In the Resource Task Network (RTN) methodology all commodities are resources. In this case, we have a few general resources (wind, power) and a monetary resource (USD).

m.usd = Currency()

Setting Bounds#

The first bound is set for over the network and year, given that no spatiotemporal disposition is provided:

\(\mathbf{cons}_{wind, network, year_0} \leq 400\)

For the second bound, given that a nominal is given, the list is treated as multiplicative factor. The length matches with the quarterly scale. Thus:

\(\mathbf{rlse}_{power, network, quarter_0} \geq 60\)

\(\mathbf{rlse}_{power, network, quarter_1} \geq 70\)

\(\mathbf{rlse}_{power, network, quarter_2} \geq 100\)

\(\mathbf{rlse}_{power, network, quarter_3} \geq 30\)

m.power = Resource(demand_nominal=100, demand_min=[0.6, 0.7, 1, 0.3])
m.wind = Resource(consume_max=400)
⚖  Initiated power balance in (l0, q)                                       ⏱ 0.0002 s
🔗  Bound [≥] power release in (l0, q)                                       ⏱ 0.0011 s
⚖  Initiated wind balance in (l0, y)                                        ⏱ 0.0001 s
🔗  Bound [≤] wind consume in (l0, y)                                        ⏱ 0.0006 s

This is the same as:

# m.wind.consume <= 400
# m.power.release.prep(100) >= [0.6, 0.7, 1, 0.3]
m.y.show(True)
\[\displaystyle [8]\text{ }{\mathbf{{cons}}}_{{wind},{l0},{{{y}_{0}}}} = 0\]
\[\displaystyle [9]\text{ }{\mathbf{{cons}}}_{{wind},{l0},{{{y}_{0}}}} - 400.0 \leq 0\]

Check the model anytime, using m.show(), or object.show()

The first constraint is a general resource balance, generated for every resource at every spatiotemporal disposition at which an aspect regarding it is defined.

Skip the True in .show() for a more concise set notation based print

m.power.show(True)
\[\displaystyle [4]\text{ }-{\mathbf{{rlse}}}_{{power},{l0},{{{y}_{0}}},{{{q}_{0}}}} + 60.0 \leq 0\]
\[\displaystyle [5]\text{ }-{\mathbf{{rlse}}}_{{power},{l0},{{{y}_{0}}},{{{q}_{1}}}} + 70.0 \leq 0\]
\[\displaystyle [6]\text{ }-{\mathbf{{rlse}}}_{{power},{l0},{{{y}_{0}}},{{{q}_{2}}}} + 100.0 \leq 0\]
\[\displaystyle [7]\text{ }-{\mathbf{{rlse}}}_{{power},{l0},{{{y}_{0}}},{{{q}_{3}}}} + 30.0 \leq 0\]
\[\displaystyle [0]\text{ }-{\mathbf{{rlse}}}_{{power},{l0},{{{y}_{0}}},{{{q}_{0}}}} = 0\]
\[\displaystyle [1]\text{ }-{\mathbf{{rlse}}}_{{power},{l0},{{{y}_{0}}},{{{q}_{1}}}} = 0\]
\[\displaystyle [2]\text{ }-{\mathbf{{rlse}}}_{{power},{l0},{{{y}_{0}}},{{{q}_{2}}}} = 0\]
\[\displaystyle [3]\text{ }-{\mathbf{{rlse}}}_{{power},{l0},{{{y}_{0}}},{{{q}_{3}}}} = 0\]

Process#

There are multiple ways to model this.

Whats most important, however, is the resource balance. The resource in the brackets is the basis resource. For a negative basis, multiply the resource by the negative factor or just use negation if that applies.

You can set bounds on the extent to which the wind farm can operate. We are actually writing the following constraint:

\(\mathbf{opr} <= \phi \cdot \mathbf{cap}\)

However, we know the capacity, so it is treated as a parameter.

Note that the incoming values are normalized by default. Use norm = False to avoid that

Operating Level as Decision#

m.wf = Process(
    m.power == -1 * m.wind,
    operate_nominal=200,
    operate_normalize=False,
    operate_max=[0.9, 0.8, 0.5, 0.7],
    opex=[4000, 4200, 4300, 3900],
)
🔗  Bound [≤] wf operate in (l0, q)                                          ⏱ 0.0003 s
🧭  Mapped time for operate (wf, l0, q) ⟺ (wf, l0, y)                        ⏱ 0.0002 s
🔗  Bound [=] usd spend in (l0, q)                                           ⏱ 0.0006 s

Is the same as:

m.wf = Process() m.wf(m.power) == -1 * m.wind m.wf.operate.prep(200, norm=False) <= [0.9, 0.8, 0.5, 0.7] m.wf.show(True)

# m.wf = Process()
# m.wf(m.power) == -1 * m.wind
# m.wf.operate.prep(200, norm=False) <= [0.9, 0.8, 0.5, 0.7]
# m.wf.show(True)

Capacity and Operating Level as Decisions#

# m.wf.capacity == True
# m.wf.operate <= [0.9, 0.8, 0.5, 0.7]
# m.wf.show(True)

Fixed Capacity and Operating Level as Decision#

# m.wf.capacity == 200
# m.wf.operate <= [0.9, 0.8, 0.5, 0.7]
# m.wf.show(True)

General Calculations#

Any general calculation can be computed as

Add a cost to the process operation. This implies that the cost of operating is variable in every quarter. In general:

\(\dot{\mathbf{v}} == \theta \cdot \mathbf{v}\)

# m.usd.spend(m.wf.operate) == [4000, 4200, 4300, 3900]
m.operate.show()
\[\displaystyle [6]\text{ }{\mathbf{{\mathbf{spend}}}}_{{usd},{l0},{y},{q},{\mathbf{{opr}}},{wf}} - {\mathrm{φ}}_{{}} \cdot {\mathbf{{\mathbf{{\mathbf{{opr}}}}}}}_{{wf},{l0},{y},{q}} = 0\]
\[\displaystyle [5]\text{ }{\mathbf{{\mathbf{{\mathbf{{opr}}}}}}}_{{wf},{l0},{y}} - \sum_{i \in {q}} {\mathbf{{\mathbf{{\mathbf{{opr}}}}}}}_{{wf}, {l0}, {y}, i} = 0\]
\[\displaystyle [4]\text{ }{\mathbf{{\mathbf{{opr}}}}}_{{wf},{l0},{y},{q}} - {\mathrm{φ}}_{{}} \leq 0\]

Note that printing can be achieved via the aspect [operate, spend, etc.] or the object

Locating the process#

The production streams are only generated once the process is places in some location. In this study, the only location is available is the default network.

m.network.locate(m.wf)
💡  Assumed wf capacity unbounded in (l0, y)                                 ⏱ 0.0002 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.0043 s

Alternatively,

# m.wf.locate(m.network)

The Model#

The model consists of the following:

  1. A general resource balances for wind in (network, year) and power in (network, quarter)

  2. Bounds on wind consumption [upper] and power release [lower], and wf operation [upper]

  3. Conversion constraints, giving produced and expended resources based on operation

  4. Calculation of spending USD in every quarter

  5. Mapping constraints for operate: q -> y and spend: q -> y (generated after objective is set)

m.show(True)

Mathematical Program for Program(scheduling)



Index Sets

\[\displaystyle {locations} = \{ {{l0}} \}\]
\[\displaystyle {currencies} = \{ {{usd}} \}\]
\[\displaystyle {y} = \{ {{{{y}_{0}}}} \}\]
\[\displaystyle {q} = \{ {{{{q}_{0}}}, {{{q}_{1}}}, {{{q}_{2}}}, {{{q}_{3}}}} \}\]
\[\displaystyle {resources} = \{ {{power}, {wind}} \}\]
\[\displaystyle {processes} = \{ {{wf}} \}\]



s.t.

Balance Constraints

\[\displaystyle [0]\text{ }-{\mathbf{{rlse}}}_{{power},{l0},{{{y}_{0}}},{{{q}_{0}}}} + {\mathbf{prod}}_{{power},{l0},{{{y}_{0}}},{{{q}_{0}}},{\mathbf{{opr}}},{wf}} = 0\]
\[\displaystyle [1]\text{ }-{\mathbf{{rlse}}}_{{power},{l0},{{{y}_{0}}},{{{q}_{1}}}} + {\mathbf{prod}}_{{power},{l0},{{{y}_{0}}},{{{q}_{1}}},{\mathbf{{opr}}},{wf}} = 0\]
\[\displaystyle [2]\text{ }-{\mathbf{{rlse}}}_{{power},{l0},{{{y}_{0}}},{{{q}_{2}}}} + {\mathbf{prod}}_{{power},{l0},{{{y}_{0}}},{{{q}_{2}}},{\mathbf{{opr}}},{wf}} = 0\]
\[\displaystyle [3]\text{ }-{\mathbf{{rlse}}}_{{power},{l0},{{{y}_{0}}},{{{q}_{3}}}} + {\mathbf{prod}}_{{power},{l0},{{{y}_{0}}},{{{q}_{3}}},{\mathbf{{opr}}},{wf}} = 0\]
\[\displaystyle [8]\text{ }{\mathbf{{cons}}}_{{wind},{l0},{{{y}_{0}}}} - {\mathbf{{expd}}}_{{wind},{l0},{{{y}_{0}}},{\mathbf{{opr}}},{wf}} = 0\]

Binds Constraints

\[\displaystyle [4]\text{ }-{\mathbf{{rlse}}}_{{power},{l0},{{{y}_{0}}},{{{q}_{0}}}} + 60.0 \leq 0\]
\[\displaystyle [5]\text{ }-{\mathbf{{rlse}}}_{{power},{l0},{{{y}_{0}}},{{{q}_{1}}}} + 70.0 \leq 0\]
\[\displaystyle [6]\text{ }-{\mathbf{{rlse}}}_{{power},{l0},{{{y}_{0}}},{{{q}_{2}}}} + 100.0 \leq 0\]
\[\displaystyle [7]\text{ }-{\mathbf{{rlse}}}_{{power},{l0},{{{y}_{0}}},{{{q}_{3}}}} + 30.0 \leq 0\]
\[\displaystyle [9]\text{ }{\mathbf{{cons}}}_{{wind},{l0},{{{y}_{0}}}} - 400.0 \leq 0\]
\[\displaystyle [10]\text{ }{\mathbf{{opr}}}_{{wf},{l0},{{{y}_{0}}},{{{q}_{0}}}} - 200.0 \leq 0\]
\[\displaystyle [11]\text{ }{\mathbf{{opr}}}_{{wf},{l0},{{{y}_{0}}},{{{q}_{1}}}} - 177.7777777777778 \leq 0\]
\[\displaystyle [12]\text{ }{\mathbf{{opr}}}_{{wf},{l0},{{{y}_{0}}},{{{q}_{2}}}} - 111.11111111111111 \leq 0\]
\[\displaystyle [13]\text{ }{\mathbf{{opr}}}_{{wf},{l0},{{{y}_{0}}},{{{q}_{3}}}} - 155.55555555555554 \leq 0\]

Calculations Constraints

\[\displaystyle [15]\text{ }{\mathbf{spend}}_{{usd},{l0},{{{y}_{0}}},{{{q}_{0}}},{\mathbf{{opr}}},{wf}} - 4000.0 \cdot {\mathbf{{opr}}}_{{wf},{l0},{{{y}_{0}}},{{{q}_{0}}}} = 0\]
\[\displaystyle [16]\text{ }{\mathbf{spend}}_{{usd},{l0},{{{y}_{0}}},{{{q}_{1}}},{\mathbf{{opr}}},{wf}} - 4200.0 \cdot {\mathbf{{opr}}}_{{wf},{l0},{{{y}_{0}}},{{{q}_{1}}}} = 0\]
\[\displaystyle [17]\text{ }{\mathbf{spend}}_{{usd},{l0},{{{y}_{0}}},{{{q}_{2}}},{\mathbf{{opr}}},{wf}} - 4300.0 \cdot {\mathbf{{opr}}}_{{wf},{l0},{{{y}_{0}}},{{{q}_{2}}}} = 0\]
\[\displaystyle [18]\text{ }{\mathbf{spend}}_{{usd},{l0},{{{y}_{0}}},{{{q}_{3}}},{\mathbf{{opr}}},{wf}} - 3900.0 \cdot {\mathbf{{opr}}}_{{wf},{l0},{{{y}_{0}}},{{{q}_{3}}}} = 0\]
\[\displaystyle [19]\text{ }{\mathbf{prod}}_{{power},{l0},{{{y}_{0}}},{{{q}_{0}}},{\mathbf{{opr}}},{wf}} - 1.0 \cdot {\mathbf{{opr}}}_{{wf},{l0},{{{y}_{0}}},{{{q}_{0}}}} = 0\]
\[\displaystyle [20]\text{ }{\mathbf{prod}}_{{power},{l0},{{{y}_{0}}},{{{q}_{1}}},{\mathbf{{opr}}},{wf}} - 1.0 \cdot {\mathbf{{opr}}}_{{wf},{l0},{{{y}_{0}}},{{{q}_{1}}}} = 0\]
\[\displaystyle [21]\text{ }{\mathbf{prod}}_{{power},{l0},{{{y}_{0}}},{{{q}_{2}}},{\mathbf{{opr}}},{wf}} - 1.0 \cdot {\mathbf{{opr}}}_{{wf},{l0},{{{y}_{0}}},{{{q}_{2}}}} = 0\]
\[\displaystyle [22]\text{ }{\mathbf{prod}}_{{power},{l0},{{{y}_{0}}},{{{q}_{3}}},{\mathbf{{opr}}},{wf}} - 1.0 \cdot {\mathbf{{opr}}}_{{wf},{l0},{{{y}_{0}}},{{{q}_{3}}}} = 0\]
\[\displaystyle [23]\text{ }{\mathbf{{expd}}}_{{wind},{l0},{{{y}_{0}}},{\mathbf{{opr}}},{wf}} - 1.0 \cdot {\mathbf{{opr}}}_{{wf},{l0},{{{y}_{0}}}} = 0\]

Mapping Constraints

\[\displaystyle [14]\text{ }{\mathbf{{opr}}}_{{wf},{l0},{{{y}_{0}}}} - \sum_{i \in {q}} {\mathbf{{opr}}}_{{wf}, {l0}, {{{y}_{0}}}, i} = 0\]

Plotting#

The scenario can be plotted as shown

m.scenario[m.operate][m.wf][m.l0][m.q]["ub"].line()
../_images/41afa436898ba541cfbe1aba2385612dcc737be94ff8a40aca84d8aadf36a40e.png

Optimize#

For maximization use opt(False)

m.usd.spend.opt()
🧭  Mapped time for spend (usd, l0, q, operate, wf) ⟺ (usd, l0, y)           ⏱ 0.0002 s
📝  Generated Program(scheduling).mps                                        ⏱ 0.0017 s
Warning: column name V14 in bound section not defined
Read MPS format model from file Program(scheduling).mps
Reading time = 0.00 seconds
PROGRAM(SCHEDULING): 25 rows, 20 columns, 47 nonzeros
📝  Generated gurobipy model. See .formulation                               ⏱ 0.0049 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 25 rows, 20 columns and 47 nonzeros
Model fingerprint: 0xea244414
Coefficient statistics:
  Matrix range     [1e+00, 4e+03]
  Objective range  [1e+00, 1e+00]
  Bounds range     [0e+00, 0e+00]
  RHS range        [3e+01, 4e+02]
Presolve removed 25 rows and 20 columns
Presolve time: 0.00s
Presolve: All rows and columns removed
Iteration    Objective       Primal Inf.    Dual Inf.      Time
       0    1.0810000e+06   0.000000e+00   0.000000e+00      0s

Solved in 0 iterations and 0.00 seconds (0.00 work units)
Optimal objective  1.081000000e+06
📝  Generated Solution object for Program(scheduling). See .solution         ⏱ 0.0002 s
✅  Program(scheduling) optimized using gurobi. Display using .output()      ⏱ 0.0126 s

The Solution#

The overall#

m.output()

Solution for Program(scheduling)



Objective

\[\displaystyle min \hspace{0.2cm} {\mathbf{spend}}_{{usd},{l0},{{{y}_{0}}}}=1081000.0\]



Variables

\[\displaystyle {\mathbf{{rlse}}}_{{power},{l0},{{{y}_{0}}},{{{q}_{0}}}}=60.0\]
\[\displaystyle {\mathbf{{rlse}}}_{{power},{l0},{{{y}_{0}}},{{{q}_{1}}}}=70.0\]
\[\displaystyle {\mathbf{{rlse}}}_{{power},{l0},{{{y}_{0}}},{{{q}_{2}}}}=100.0\]
\[\displaystyle {\mathbf{{rlse}}}_{{power},{l0},{{{y}_{0}}},{{{q}_{3}}}}=30.0\]
\[\displaystyle {\mathbf{{cons}}}_{{wind},{l0},{{{y}_{0}}}}=260.0\]
\[\displaystyle {\mathbf{{opr}}}_{{wf},{l0},{{{y}_{0}}},{{{q}_{0}}}}=60.0\]
\[\displaystyle {\mathbf{{opr}}}_{{wf},{l0},{{{y}_{0}}},{{{q}_{1}}}}=70.0\]
\[\displaystyle {\mathbf{{opr}}}_{{wf},{l0},{{{y}_{0}}},{{{q}_{2}}}}=100.0\]
\[\displaystyle {\mathbf{{opr}}}_{{wf},{l0},{{{y}_{0}}},{{{q}_{3}}}}=30.0\]
\[\displaystyle {\mathbf{{opr}}}_{{wf},{l0},{{{y}_{0}}}}=260.0\]
\[\displaystyle {\mathbf{spend}}_{{usd},{l0},{{{y}_{0}}},{{{q}_{0}}},{\mathbf{{opr}}},{wf}}=240000.0\]
\[\displaystyle {\mathbf{spend}}_{{usd},{l0},{{{y}_{0}}},{{{q}_{1}}},{\mathbf{{opr}}},{wf}}=294000.0\]
\[\displaystyle {\mathbf{spend}}_{{usd},{l0},{{{y}_{0}}},{{{q}_{2}}},{\mathbf{{opr}}},{wf}}=430000.0\]
\[\displaystyle {\mathbf{spend}}_{{usd},{l0},{{{y}_{0}}},{{{q}_{3}}},{\mathbf{{opr}}},{wf}}=117000.0\]
\[\displaystyle {\mathbf{spend}}_{{usd},{l0},{{{y}_{0}}}}=1081000.0\]
\[\displaystyle {\mathbf{prod}}_{{power},{l0},{{{y}_{0}}},{{{q}_{0}}},{\mathbf{{opr}}},{wf}}=60.0\]
\[\displaystyle {\mathbf{prod}}_{{power},{l0},{{{y}_{0}}},{{{q}_{1}}},{\mathbf{{opr}}},{wf}}=70.0\]
\[\displaystyle {\mathbf{prod}}_{{power},{l0},{{{y}_{0}}},{{{q}_{2}}},{\mathbf{{opr}}},{wf}}=100.0\]
\[\displaystyle {\mathbf{prod}}_{{power},{l0},{{{y}_{0}}},{{{q}_{3}}},{\mathbf{{opr}}},{wf}}=30.0\]
\[\displaystyle {\mathbf{{expd}}}_{{wind},{l0},{{{y}_{0}}},{\mathbf{{opr}}},{wf}}=260.0\]

Individual#

The individual solutions can be obtained as list as well (using aslist = True)

m.operate.output()
\[\displaystyle {\mathbf{{opr}}}_{{wf},{l0},{{{y}_{0}}},{{{q}_{0}}}}=60.0\]
\[\displaystyle {\mathbf{{opr}}}_{{wf},{l0},{{{y}_{0}}},{{{q}_{1}}}}=70.0\]
\[\displaystyle {\mathbf{{opr}}}_{{wf},{l0},{{{y}_{0}}},{{{q}_{2}}}}=100.0\]
\[\displaystyle {\mathbf{{opr}}}_{{wf},{l0},{{{y}_{0}}},{{{q}_{3}}}}=30.0\]
\[\displaystyle {\mathbf{{opr}}}_{{wf},{l0},{{{y}_{0}}}}=260.0\]

Plotting#

There are two plots natively available: line and bar

m.operate(m.wf, m.l0, m.q).bar()
../_images/83a92be41810ef46c5cbb1b92d7119551b985020f5afbe3528ee64ec5e0f6ee9.png
m.power.release(m.l0, m.q).line()
# same as m.release(m.power, m.l0, m.q).line()
../_images/865885a06cd6cdcc7cf10fff580a605e58baaf2c70028df52e232e596d73b170.png