Skip to content

Commit edb2dae

Browse files
committed
Refactor ModelBuilder
This is a major code change.
1 parent 7f65ebb commit edb2dae

29 files changed

+4106
-3023
lines changed

examples/custom_workflow.ipynb

Lines changed: 73 additions & 63 deletions
Large diffs are not rendered by default.

examples/quickstart.ipynb

Lines changed: 12 additions & 11 deletions
Large diffs are not rendered by default.

src/pownet/__init__.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,11 @@
1-
""" This is the pownet module.
2-
"""
1+
from .core import (
2+
Simulator,
3+
OutputProcessor,
4+
SystemRecord,
5+
DataProcessor,
6+
ModelBuilder,
7+
Visualizer,
8+
UserConstraint,
9+
)
10+
11+
from .input import SystemInput

src/pownet/builder/basebuilder.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""basebuilder.py: This module defines the abstract base class for component builders in the Pownet framework."""
2+
3+
from abc import ABC, abstractmethod
4+
5+
import gurobipy as gp
6+
7+
from ..input import SystemInput
8+
9+
10+
class ComponentBuilder(ABC):
11+
"""
12+
Abstract base class for component builders in the Pownet framework.
13+
14+
This class defines the interface for building components, which includes methods for
15+
creating a model, adding variables, constraints, and objectives to the model.
16+
"""
17+
18+
def __init__(self, model: gp.Model, inputs: SystemInput):
19+
self.model = model
20+
self.inputs = inputs
21+
self.sim_horizon = inputs.sim_horizon
22+
self.timesteps = range(1, self.inputs.sim_horizon + 1)
23+
24+
@abstractmethod
25+
def add_variables(self, step_k: int) -> None:
26+
pass
27+
28+
@abstractmethod
29+
def get_fixed_objective_terms(self) -> gp.LinExpr:
30+
pass
31+
32+
@abstractmethod
33+
def get_variable_objective_terms(self, step_k: int, **kwargs) -> gp.LinExpr:
34+
pass
35+
36+
@abstractmethod
37+
def add_constraints(self, step_k: int, init_conds: dict, **kwargs) -> None:
38+
pass
39+
40+
@abstractmethod
41+
def update_variables(self, step_k: int) -> None:
42+
pass
43+
44+
@abstractmethod
45+
def update_constraints(self, step_k: int, init_conds: dict, **kwargs) -> None:
46+
pass
47+
48+
@abstractmethod
49+
def get_variables(self) -> dict[str, gp.tupledict]:
50+
pass
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
"""energy_storage.py: Energy storage unit builder."""
2+
3+
from .basebuilder import ComponentBuilder
4+
5+
import gurobipy as gp
6+
7+
from ..input import SystemInput
8+
from ..optim_model.variable import add_var_with_variable_ub, update_var_with_variable_ub
9+
from ..optim_model.objfunc import get_marginal_cost_coeff
10+
from ..optim_model.constraints import energy_storage_constr
11+
12+
13+
class EnergyStorageUnitBuilder(ComponentBuilder):
14+
"""Builder class for energy storage units.
15+
16+
Variables
17+
===========================
18+
- `pcharge`: Power charging an energy storage system. Unit: MW.
19+
- `pdischarge`: Power discharging an energy storage system. Unit: MW.
20+
- `charge_state`: State of charge of an energy storage system. Unit: MWh.
21+
- `ucharge`: Indicator that an ESS is charging. Unitless.
22+
- `udischarge`: Indicator that an ESS is discharging. Unitless.
23+
24+
Fixed objective terms
25+
===========================
26+
None
27+
28+
Variable objective terms
29+
===========================
30+
- Energy cost
31+
32+
Constraints
33+
===========================
34+
- Linking upper bounds of dispatch variables to unit status variables
35+
36+
"""
37+
38+
def __init__(self, model: gp.Model, inputs: SystemInput):
39+
super().__init__(model, inputs)
40+
41+
# Variables
42+
self.pcharge = gp.tupledict()
43+
self.pdischarge = gp.tupledict()
44+
45+
self.charge_state = gp.tupledict()
46+
self.ucharge = gp.tupledict() # Charging indicator
47+
self.udischarge = gp.tupledict() # Discharging indicator
48+
49+
# Fixed objective terms
50+
self.total_fixed_objective_expr = gp.LinExpr()
51+
52+
# Variable objective terms
53+
self.total_energy_cost_expr = gp.LinExpr()
54+
55+
# Constraints
56+
# Constraints
57+
self.c_link_ess_charge = gp.tupledict()
58+
self.c_link_ess_dischage = gp.tupledict()
59+
self.c_link_ess_state = gp.tupledict()
60+
self.c_unit_ess_balance_init = gp.tupledict()
61+
self.c_unit_ess_balance = gp.tupledict()
62+
63+
def add_variables(self, step_k: int) -> None:
64+
"""
65+
Add energy storage variables to the model.
66+
67+
Args:
68+
step_k (int): The current simulation step.
69+
70+
Returns:
71+
None
72+
"""
73+
74+
# Variables with fixed upper bounds
75+
var_with_fixed_ub = [
76+
("pcharge", self.inputs.ess_max_charge),
77+
("pdischarge", self.inputs.ess_max_discharge),
78+
]
79+
80+
for varname, capacity_dict in var_with_fixed_ub:
81+
setattr(
82+
self,
83+
varname,
84+
self.model.addVars(
85+
self.inputs.storage_units,
86+
self.timesteps,
87+
lb=0,
88+
ub={
89+
(unit, t): capacity_dict[unit]
90+
for t in self.timesteps
91+
for unit in self.inputs.storage_units
92+
},
93+
name=varname,
94+
),
95+
)
96+
97+
# Variables with time-dependent upper bounds
98+
self.charge_state = add_var_with_variable_ub(
99+
model=self.model,
100+
varname="charge_state",
101+
timesteps=self.timesteps,
102+
step_k=step_k,
103+
units=self.inputs.storage_units,
104+
capacity_df=self.inputs.ess_derated_capacity,
105+
)
106+
107+
# Binary variables
108+
binary_variables = ["ucharge", "udischarge"]
109+
for varname in binary_variables:
110+
setattr(
111+
self,
112+
varname,
113+
self.model.addVars(
114+
self.inputs.storage_units,
115+
self.timesteps,
116+
vtype=gp.GRB.BINARY,
117+
name=varname,
118+
),
119+
)
120+
121+
def get_fixed_objective_terms(self) -> gp.LinExpr:
122+
"""Energy storage units have no fixed objective terms."""
123+
return self.total_fixed_objective_expr
124+
125+
def get_variable_objective_terms(self, step_k: int) -> gp.LinExpr:
126+
"""Calculate the variable objective terms for energy storage units.
127+
128+
Args:
129+
step_k (int): The current simulation step.
130+
131+
Returns:
132+
gp.LinExpr: The variable objective terms.
133+
"""
134+
# Energy cost is calculated based on the marginal cost of the units.
135+
self.total_energy_cost_expr = gp.LinExpr()
136+
energy_cost_coeffs = get_marginal_cost_coeff(
137+
step_k=step_k,
138+
timesteps=self.timesteps,
139+
units=self.inputs.storage_units,
140+
nondispatch_contracts=self.inputs.ess_contracts,
141+
contract_costs=self.inputs.contract_costs,
142+
)
143+
self.total_energy_cost_expr.add(self.pdischarge.prod(energy_cost_coeffs))
144+
return self.total_energy_cost_expr
145+
146+
def add_constraints(self, step_k: int, init_conds: dict, **kwargs) -> None:
147+
"""Add constraints to the model.
148+
149+
Args:
150+
step_k (int): The current simulation step.
151+
init_conds (dict): Initial conditions for the model.
152+
153+
Returns:
154+
None
155+
"""
156+
self.c_link_ess_charge = energy_storage_constr.add_c_link_ess_charge(
157+
model=self.model,
158+
pcharge=self.pcharge,
159+
ucharge=self.ucharge,
160+
timesteps=self.timesteps,
161+
units=self.inputs.storage_units,
162+
max_charge=self.inputs.ess_max_charge,
163+
)
164+
165+
self.c_link_ess_dischage = energy_storage_constr.add_c_link_ess_discharge(
166+
model=self.model,
167+
pdischarge=self.pdischarge,
168+
udischarge=self.udischarge,
169+
timesteps=self.timesteps,
170+
units=self.inputs.storage_units,
171+
max_discharge=self.inputs.ess_max_discharge,
172+
)
173+
174+
self.c_link_ess_state = energy_storage_constr.add_c_link_ess_state(
175+
model=self.model,
176+
ucharge=self.ucharge,
177+
udischarge=self.udischarge,
178+
timesteps=self.timesteps,
179+
units=self.inputs.storage_units,
180+
)
181+
182+
self.c_unit_ess_balance_init = (
183+
energy_storage_constr.add_c_unit_ess_balance_init(
184+
model=self.model,
185+
pcharge=self.pcharge,
186+
pdischarge=self.pdischarge,
187+
charge_state=self.charge_state,
188+
units=self.inputs.storage_units,
189+
charge_state_init=init_conds["initial_charge_state"],
190+
charge_efficiency=self.inputs.ess_charge_efficiency,
191+
discharge_efficiency=self.inputs.ess_discharge_efficiency,
192+
self_discharge_rate=self.inputs.ess_self_discharge_rate,
193+
)
194+
)
195+
196+
self.c_unit_ess_balance = energy_storage_constr.add_c_unit_ess_balance(
197+
model=self.model,
198+
pcharge=self.pcharge,
199+
pdischarge=self.pdischarge,
200+
charge_state=self.charge_state,
201+
units=self.inputs.storage_units,
202+
sim_horizon=self.inputs.sim_horizon,
203+
charge_efficiency=self.inputs.ess_charge_efficiency,
204+
discharge_efficiency=self.inputs.ess_discharge_efficiency,
205+
self_discharge_rate=self.inputs.ess_self_discharge_rate,
206+
)
207+
208+
def update_variables(self, step_k: int) -> None:
209+
"""Update the variables for energy storage units.
210+
211+
Args:
212+
step_k (int): The current simulation step.
213+
214+
Returns:
215+
None
216+
"""
217+
update_var_with_variable_ub(
218+
variables=self.charge_state,
219+
step_k=step_k,
220+
capacity_df=self.inputs.ess_derated_capacity,
221+
)
222+
223+
def update_constraints(self, step_k: int, init_conds: dict, **kwargs) -> None:
224+
"""Update the constraints for energy storage units.
225+
226+
Args:
227+
step_k (int): The current simulation step.
228+
init_conds (dict): Initial conditions for the model.
229+
230+
Returns:
231+
None
232+
"""
233+
self.model.remove(self.c_unit_ess_balance_init)
234+
self.c_unit_ess_balance_init = (
235+
energy_storage_constr.add_c_unit_ess_balance_init(
236+
model=self.model,
237+
pcharge=self.pcharge,
238+
pdischarge=self.pdischarge,
239+
charge_state=self.charge_state,
240+
units=self.inputs.storage_units,
241+
charge_state_init=init_conds["initial_charge_state"],
242+
charge_efficiency=self.inputs.ess_charge_efficiency,
243+
discharge_efficiency=self.inputs.ess_discharge_efficiency,
244+
self_discharge_rate=self.inputs.ess_self_discharge_rate,
245+
)
246+
)
247+
248+
def get_variables(self) -> dict[str, gp.tupledict]:
249+
"""Return all variables in the energy storage unit builder.
250+
251+
Returns:
252+
dict[str, gp.tupledict]: A dictionary containing all variables in the builder.
253+
"""
254+
return {
255+
"pcharge": self.pcharge,
256+
"pdischarge": self.pdischarge,
257+
"charge_state": self.charge_state,
258+
"ucharge": self.ucharge,
259+
"udischarge": self.udischarge,
260+
}

0 commit comments

Comments
 (0)