Skip to content

Commit 17d1b76

Browse files
committed
Fix merging HydroCHIPPs
1 parent a74a2a9 commit 17d1b76

File tree

6 files changed

+146
-10
lines changed

6 files changed

+146
-10
lines changed

src/pownet/builder/hydro.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class HydroUnitBuilder(ComponentBuilder):
1515
Variables
1616
===========================
1717
- `phydro`: Hydropower output. Unit: MW.
18+
- `uhydro`: Hydropower unit status. Unit: binary (0 or 1).
1819
1920
Fixed objective terms
2021
===========================
@@ -46,6 +47,10 @@ def __init__(self, model: gp.Model, inputs: SystemInput):
4647

4748
# Constraints
4849
self.c_link_hydro_pu = gp.tupledict()
50+
self.c_link_weekly_hydro_pu = gp.tupledict()
51+
52+
self.c_hydro_limit_daily = gp.tupledict()
53+
self.c_hydro_limit_weekly = gp.tupledict()
4954

5055
def add_variables(self, step_k: int) -> None:
5156
"""Add variables to the model for hydro units.
@@ -112,6 +117,17 @@ def add_constraints(self, step_k: int, init_conds: dict, **kwargs) -> None:
112117
units=self.inputs.hydro_unit_node.keys(),
113118
capacity_df=self.inputs.hydro_capacity,
114119
)
120+
121+
# Weekly upper bound
122+
self.c_link_weekly_hydro_pu = nondispatch_constr.add_c_link_unit_pu(
123+
model=self.model,
124+
pdispatch=self.phydro,
125+
u=self.uhydro,
126+
timesteps=self.timesteps,
127+
units=self.inputs.weekly_hydro_unit_node.keys(),
128+
contracted_capacity=self.inputs.hydro_contracted_capacity,
129+
)
130+
115131
# Daily upper bound
116132
self.c_hydro_limit_daily = nondispatch_constr.add_c_hydro_limit_daily(
117133
model=self.model,
@@ -122,6 +138,17 @@ def add_constraints(self, step_k: int, init_conds: dict, **kwargs) -> None:
122138
hydro_capacity=self.inputs.daily_hydro_capacity,
123139
)
124140

141+
# Weekly lower and upper bounds
142+
self.c_hydro_limit_weekly = nondispatch_constr.add_c_hydro_limit_weekly(
143+
model=self.model,
144+
phydro=self.phydro,
145+
step_k=step_k,
146+
sim_horizon=self.inputs.sim_horizon,
147+
hydro_units=self.inputs.weekly_hydro_unit_node.keys(),
148+
hydro_capacity=self.inputs.weekly_hydro_capacity,
149+
hydro_capacity_min=self.inputs.hydro_min_capacity,
150+
)
151+
125152
def update_variables(self, step_k: int) -> None:
126153
"Hydropower variables do not have time-dependent lower/upper bounds."
127154
return
@@ -157,6 +184,17 @@ def update_constraints(self, step_k: int, init_conds: dict, **kwargs) -> None:
157184
hydro_capacity=self.inputs.daily_hydro_capacity,
158185
)
159186

187+
self.model.remove(self.c_hydro_limit_weekly)
188+
self.c_hydro_limit_weekly = nondispatch_constr.add_c_hydro_limit_weekly(
189+
model=self.model,
190+
phydro=self.phydro,
191+
step_k=step_k,
192+
sim_horizon=self.inputs.sim_horizon,
193+
hydro_units=self.inputs.weekly_hydro_unit_node.keys(),
194+
hydro_capacity=self.inputs.weekly_hydro_capacity,
195+
hydro_capacity_min=self.inputs.hydro_min_capacity,
196+
)
197+
160198
def update_daily_hydropower_capacity(
161199
self, step_k: int, new_capacity: dict[(str, int), float]
162200
) -> None:

src/pownet/builder/system.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ def __init__(self, model: gp.Model, inputs: SystemInput):
108108
self.c_import_curtail_ess = gp.tupledict()
109109

110110
self.c_daily_hydro_curtail_ess = gp.tupledict()
111+
self.c_weekly_hydro_curtail_ess = gp.tupledict()
111112

112113
def add_variables(self, step_k: int) -> None:
113114

@@ -349,6 +350,19 @@ def get_variable_objective_terms(
349350
* self.c_daily_hydro_curtail_ess.prod(daily_hydro_coeffs)
350351
)
351352

353+
# Weekly hydro curtailment penalty
354+
weekly_hydro_coeffs = get_marginal_cost_coeff(
355+
step_k=step_k,
356+
timesteps=self.timesteps,
357+
units=self.inputs.weekly_hydro_must_take_units,
358+
nondispatch_contracts=self.inputs.nondispatch_contracts,
359+
contract_costs=self.inputs.contract_costs,
360+
)
361+
self.must_take_curtail_penalty_expr += (
362+
curtail_cost_factor
363+
* self.c_weekly_hydro_curtail_ess.prod(weekly_hydro_coeffs)
364+
)
365+
352366
# Solar, wind, and import curtailment penalties
353367
nondispatch_tuples = [
354368
(

src/pownet/core/output.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,21 @@ def get_hourly_curtailment(
7171
node_variables["vartype"] == unit_type_map[unit_type]
7272
].pivot(columns="node", index="hour", values="value")
7373

74+
def get_unit_hourly_generation(self, node_variables: pd.DataFrame) -> pd.DataFrame:
75+
power_variables = self._get_power_variables(node_variables)
76+
hourly_generation = (
77+
power_variables[["unit", "value", "hour"]].groupby(["node", "hour"]).sum()
78+
)
79+
hourly_generation = hourly_generation.reset_index()
80+
hourly_generation = hourly_generation.pivot(
81+
columns=["hour"], index=["node"]
82+
).T.reset_index(drop=True)
83+
# PowNet indexing starts at 1
84+
hourly_generation.index += 1
85+
hourly_generation.index.name = "Hour"
86+
87+
return hourly_generation
88+
7489
def get_hourly_generation(self, node_variables: pd.DataFrame) -> pd.DataFrame:
7590
power_variables = self._get_power_variables(node_variables)
7691
hourly_generation = (

src/pownet/core/visualizer.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def plot_fuelmix_bar(
4242
fig, ax = plt.subplots(figsize=(8, 5))
4343

4444
dispatch.plot.bar(
45-
stacked=True, ax=ax, linewidth=0, color=self.fuel_color_map, legend=True
45+
stacked=True, ax=ax, linewidth=0, color=self.fuel_color_map, legend=False
4646
)
4747
ax.plot(
4848
range(0, total_timesteps),
@@ -72,7 +72,6 @@ def plot_fuelmix_bar(
7272
bbox_inches="tight",
7373
dpi=350,
7474
)
75-
plt.tight_layout()
7675
plt.show()
7776

7877
def plot_fuelmix_area(

src/pownet/input.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -499,8 +499,10 @@ def _load_hydropower(self) -> None:
499499
)
500500
)
501501

502-
if os.path.exists(os.path.join(self.model_dir, "hydro_capacity_min.csv")):
503-
self.hydro_capacity_min = pd.read_csv(os.path.join(self.model_dir, "hydro_capacity_min.csv"))
502+
if os.path.exists(os.path.join(self.model_dir, "hydro_min_capacity.csv")):
503+
self.hydro_min_capacity = pd.read_csv(
504+
os.path.join(self.model_dir, "hydro_min_capacity.csv")
505+
)
504506

505507
# Check that the names do not repeat across different types
506508
repeated_units = set(self.hydro_unit_node.keys()).intersection(
@@ -521,9 +523,9 @@ def _load_hydropower(self) -> None:
521523
)
522524

523525
# Check that the names do not repeat across different types
524-
repeated_units_daily_weekly = set(self.daily_hydro_unit_node.keys()).intersection(
525-
self.weekly_hydro_unit_node.keys()
526-
)
526+
repeated_units_daily_weekly = set(
527+
self.daily_hydro_unit_node.keys()
528+
).intersection(self.weekly_hydro_unit_node.keys())
527529
if repeated_units_daily_weekly:
528530
raise ValueError(
529531
f"PowNet: Found hydropower units to formulate with both daily and weekly formulations: {repeated_units_daily_weekly}"
@@ -783,9 +785,11 @@ def load_data(self):
783785
# List of units
784786
#################
785787
self.thermal_units = list(self.thermal_unit_node.keys())
786-
self.hydro_units = (list(self.hydro_unit_node.keys())
787-
+ list(self.daily_hydro_unit_node.keys())
788-
+ list(self.weekly_hydro_unit_node.keys()))
788+
self.hydro_units = (
789+
list(self.hydro_unit_node.keys())
790+
+ list(self.daily_hydro_unit_node.keys())
791+
+ list(self.weekly_hydro_unit_node.keys())
792+
)
789793
self.solar_units = list(self.solar_unit_node.keys())
790794
self.wind_units = list(self.wind_unit_node.keys())
791795
self.import_units = list(self.import_unit_node.keys())

src/pownet/optim_model/constraints/nondispatch_constr.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,69 @@ def add_c_hydro_limit_daily_dict(
142142
name=cname,
143143
)
144144
return constraints
145+
146+
147+
def add_c_hydro_limit_weekly(
148+
model: gp.Model,
149+
phydro: gp.tupledict,
150+
step_k: int,
151+
sim_horizon: int,
152+
hydro_units: list,
153+
hydro_capacity: pd.DataFrame,
154+
hydro_capacity_min: pd.DataFrame,
155+
) -> gp.tupledict:
156+
"""
157+
Defines the weekly limit of hydro generation. Assumes that a certain amount of water is
158+
available for hydropower generation each day. In this case, the dataframe hydro_capacity has a length of 365 days
159+
instead of 8760 hours.
160+
161+
Args:
162+
model (gp.Model): The optimization model
163+
phydro (gp.tupledict): The power output of hydro units
164+
step_k (int): The current iteration
165+
sim_horizon (int): The simulation horizon
166+
hydro_units (list): The list of hydro units
167+
hydro_capacity (pd.DataFrame): The capacity of the hydro unit
168+
169+
Returns:
170+
gp.tupledict: The constraints for the weekly hydro limit
171+
172+
Raises:
173+
ValueError: If the simulation horizon is not divisible by 24
174+
175+
"""
176+
# When formulating with weekly hydropower, sim_horizon must be divisible
177+
# by 168 because the hydro_capacity is weekly.
178+
179+
if sim_horizon % 168 != 0:
180+
raise ValueError(
181+
"The simulation horizon must be divisible by 168 when using weekly hydropower capacity."
182+
)
183+
constraints = gp.tupledict()
184+
max_week = sim_horizon // 168
185+
for week in range(step_k, step_k + max_week):
186+
for hydro_unit in hydro_units:
187+
cname = f"hydro_limit_weekly_ub[{hydro_unit},{week}]"
188+
cname_min = f"hydro_limit_weekly_lb[{hydro_unit},{week}]"
189+
current_week = week - step_k + 1
190+
191+
# Upper bound constraint
192+
constraints[cname] = model.addConstr(
193+
gp.quicksum(
194+
phydro[hydro_unit, t]
195+
for t in range(1 + (current_week - 1) * 168, current_week * 168 + 1)
196+
)
197+
<= hydro_capacity.loc[week, hydro_unit],
198+
name=cname,
199+
)
200+
201+
# Lower bound constraint
202+
constraints[cname_min] = model.addConstr(
203+
gp.quicksum(
204+
phydro[hydro_unit, t]
205+
for t in range(1 + (current_week - 1) * 168, current_week * 168 + 1)
206+
)
207+
>= hydro_capacity_min.loc[week, hydro_unit],
208+
name=cname_min,
209+
)
210+
return constraints

0 commit comments

Comments
 (0)