---
jupytext:
text_representation:
extension: .md
format_name: myst
format_version: 0.13
jupytext_version: 1.16.2
kernelspec:
display_name: Python 3 (ipykernel)
language: python
name: python3
---
(pq-example)=
# PQ curve limits
## Introduction
This example demonstrates how PQ curves are built in SHOP, and how the PQ curves are affected by different limits on production and discharge.
An introduction to PQ curves can be found [here](pq_curves).
The model setup for this example is available in the following format:
- pyshop
- [](pq_example_case.py)
For saving PQ curves, use the command [save pq_curves /on](save_pq_curves). Note that only the PQ curves for the latest iteration is saved.
```{code-cell} ipython3
#Necessary imports
import pandas as pd
import plotly.graph_objs as go
import plotly.express as px
from pyshop import ShopSession
#Functions used in this example for building a basic SHOP model and running it
from pq_example_case import build_model, run_model
```
## Create a SHOP session and import model
We will use a simple example model with one reservoir and one plant.
```{code-cell} ipython3
#Create a standard ShopSession
shop=ShopSession()
#Build a basic SHOP model
build_model(shop)
#Display topology to the screen
display(shop.model.build_connection_tree())
```
## Turbine efficiency curves
In addition to the net generator head, SHOP uses the generator attributes [gen_eff_curve](generator:gen_eff_curve) and [turb_eff_curves](generator:turb_eff_curves) to build the PQ curves. The [turb_eff_curves](generator:turb_eff_curves) is the most important of these attributes, and describe the turbine efficiency as a function of discharge for different net head values. The plot below shows the [turb_eff_curves](generator:turb_eff_curves) for our generator.
```{code-cell} ipython3
for gen in shop.model.generator:
#Get generator turbine efficiency curves
turb_eff_curves=gen.turb_eff_curves.get()
#Create figure
fig = go.Figure()
x_discharge = pd.DataFrame([], dtype="float64")
y_net_head = pd.Series([], dtype="float64")
z_efficiency = pd.DataFrame([], dtype="float64")
for curve in turb_eff_curves:
if (len(x_discharge) > 0):
x_discharge = pd.concat([x_discharge, pd.Series(curve.index)], axis=1)
else:
x_discharge = pd.concat([pd.Series(curve.index)], axis=1)
if (len(y_net_head) > 0):
y_net_head = pd.concat([y_net_head, pd.Series(curve.name)])
else:
y_net_head = pd.concat([pd.Series(curve.name)])
if (len(z_efficiency) > 0):
z_efficiency = pd.concat([z_efficiency, pd.Series(curve.values)], axis=1)
else:
z_efficiency = pd.concat([pd.Series(curve.values)], axis=1)
title_name=" Turbine efficiency curves of "+gen.get_name()+""
fig = go.Figure(data=[go.Surface(x=x_discharge.values.T, y=y_net_head.values, z=z_efficiency.values.T)])
fig.update_layout(scene = dict(
xaxis_title="Discharge (m3/s)",
yaxis_title="Net head (meter)",
zaxis_title="Efficiency (%)"),
title=title_name,
scene_camera_eye=dict(x=1.5, y=-1.8, z=1.24),
width=700, height=500,
margin=dict(r=10, b=10, l=10, t=50))
fig.show()
```
## Run model and plot results
We start by running the example model.
```{code-cell} ipython3
run_model(shop)
```
In our example case, the reservoir goes from being full to being empty, so that the net head varies from its maximum to its minimum.
```{code-cell} ipython3
for rsv in shop.model.reservoir:
# Reservoir water level trajectory
fig = go.Figure()
water_level=rsv.head.get()
fig.add_trace(go.Scatter(x=water_level.index, y=water_level.values, name="Reservoir water level", marker_color="red"))
# Regulation max water level
max_level=rsv.hrl.get()
txy_max_level=pd.Series(index=water_level.index, dtype="float64")
txy_max_level=txy_max_level.fillna(max_level)
fig.add_trace(go.Scatter(x=txy_max_level.index, y=txy_max_level.values, name="Reservoir regulation max water level", marker_color="rgb(0, 0, 255)", line=dict(width=3, dash="dot")))
# Regulation min water level .
min_level=rsv.lrl.get()
txy_min_level=pd.Series(index=water_level.index, dtype="float64")
txy_min_level=txy_min_level.fillna(min_level)
fig.add_trace(go.Scatter(x=txy_min_level.index, y=txy_min_level.values, name="Reservoir regulation min water level", marker_color="rgb(0, 200, 0)", line=dict(width=3, dash="dot")))
change = water_level.values[-1] - water_level.values[0]
change = "{:,.4f}".format(change)
change_pencentage = (water_level.values[-1] - water_level.values[0])/(rsv.hrl.get() - rsv.lrl.get()) * 100
change_pencentage = "{:,.2f}".format(change_pencentage)
fig.update_layout(title="Reservoir water level trajectory of "+rsv.get_name()+"
(Change "+str(change)+" meter, "+str(change_pencentage)+"%)
", xaxis_title="Time (Hour)", yaxis_title="Water level (meter above sea level)")
fig.update_layout(legend=dict(orientation="h", yanchor="bottom", y=-0.5, xanchor="center", x=0.5))
fig.show()
for plant in shop.model.plant:
eff_head=plant.eff_head.get()
fig = go.Figure()
fig.add_trace(go.Scatter(x=eff_head.index, y=eff_head.values, name=str(eff_head.name), mode="lines+markers", line=dict(width=2)))
fig.update_layout(title="Effective head of "+gen.get_name(), xaxis_title="Time", yaxis_title="Eff head (m)", colorway=px.colors.qualitative.Light24)
fig.show()
```
The plots below show the generator's [original_pq_curves](generator:original_pq_curves), [convex_pq_curves](generator:convex_pq_curves) and [final_pq_curves](generator:final_pq_curves). Since no other limits have been set, the PQ curves are limited by the minimum and maximum discharge points from the [turb_eff_curves](generator:turb_eff_curves).
```{code-cell} ipython3
for gen in shop.model.generator:
#Get generator discharge and production
discharge = gen.discharge.get()
production = gen.production.get()
#Get PQ curves
original_pq_curve=gen.original_pq_curves.get()
convex_pq_curve=gen.convex_pq_curves.get()
final_pq_curve=gen.final_pq_curves.get()
#Plot original PQ curves
fig = go.Figure()
for i, curve in enumerate(original_pq_curve):
if i%6==0: #plot every 6th hour only
fig.add_trace(go.Scatter(x=curve.index, y=curve.values, name=str(curve.name), mode="lines+markers", line=dict(width=2)))
fig.update_layout(title="Original PQ curves of "+gen.get_name(), xaxis_title="Discharge (m3/s)", yaxis_title="Production (MW)", colorway=px.colors.sequential.Plasma)
fig.show()
#Plot convex PQ curves
fig = go.Figure()
for i, curve in enumerate(convex_pq_curve):
if i%6==0: #plot every 6th hour only
fig.add_trace(go.Scatter(x=curve.index, y=curve.values, name=str(curve.name), mode="lines+markers", line=dict(width=2)))
fig.update_layout(title="Convex PQ curves of "+gen.get_name(), xaxis_title="Discharge (m3/s)", yaxis_title="Production (MW)", colorway=px.colors.sequential.Plasma)
fig.show()
#Plot final PQ curves
fig = go.Figure()
for i, curve in enumerate(final_pq_curve):
if i%6==0: #plot every 6th hour only
fig.add_trace(go.Scatter(x=curve.index, y=curve.values, name=str(curve.name), mode="lines+markers", line=dict(width=2)))
# Add working point for each time
current_discharge = discharge.loc[curve.name]
current_production = production.loc[curve.name]
if i%6==0: #plot every 6th hour only
fig.add_trace(go.Scatter(x=[current_discharge], y=[current_production], mode="markers", marker_symbol="x", marker=dict(color="Black", size=5), name="working point", showlegend=False))
fig.add_trace(go.Scatter(x=[current_discharge], y=[current_production], mode="markers", marker_symbol="x", marker=dict(color="Black", size=5), name="working point"))
fig.update_layout(title="Final PQ curves of "+gen.get_name(), xaxis_title="Discharge (m3/s)", yaxis_title="Production (MW)", colorway=px.colors.sequential.Plasma)
fig.show()
```
## Production limits
+++
We will now run the example case again, but this time wih stricter limits on production; [p_min](generator:p_min) and [p_max](generator:p_max).
```{code-cell} ipython3
#Create a standard ShopSession
shop2=ShopSession()
#Build a basic SHOP model
build_model(shop2)
#Set the p_min and p_max attributes
for gen in shop2.model.generator:
gen.p_min.set(20)
gen.p_max.set(82)
#Run model
run_model(shop2)
```
Now the PQ curves are limited from below by the minimum production [p_min](generator:p_min). From above the curves are limited by the maximum production [p_max](generator:p_max) while the net head is high, while for lower net heads the PQ curve is still limited by the maximum discharge from the [turb_eff_curves](generator:turb_eff_curves).
```{code-cell} ipython3
for gen in shop2.model.generator:
original_pq_curve=gen.original_pq_curves.get()
fig = go.Figure()
for i, curve in enumerate(original_pq_curve):
if i%6==0: #plot every 6th hour only
fig.add_trace(go.Scatter(x=curve.index, y=curve.values, name=str(curve.name), mode="lines+markers", line=dict(width=2)))
fig.add_trace(go.Scatter(x=[25,100], y=[gen.p_max.get()]*2, name="P_max", mode="lines", line=dict(dash="dash",width=2,color="black")))
fig.add_trace(go.Scatter(x=[25,100], y=[gen.p_min.get()]*2, name="P_min", mode="lines", line=dict(dash="dash",width=2,color="black")))
fig.update_layout(title="Original PQ curves of "+gen.get_name(), xaxis_title="Discharge (m3/s)", yaxis_title="Production (MW)", colorway=px.colors.sequential.Plasma)
fig.show()
```
## Discharge limits
The default behaviour of SHOP is to find the discharge limits from the minimum discharge points of the [turb_eff_curves](generator:turb_eff_curves). Explicit discharge limits can be set using the head-dependent generator attributes [min_discharge](generator:min_discharge) and [max_discharge](generator:max_discharge). The same attributes are also available on the [needle_combination](needle_combination) object, which will take precedence over the generator attributes if both are defined.
We define and plot head dependent discharge limits below:
```{code-cell} ipython3
q_min=pd.Series([45.0, 69.0], index=[90.0, 100.0])
q_max=pd.Series([85.0, 109.0], index=[90.0, 100.0])
fig=go.Figure()
fig.add_trace(go.Scatter(x=q_min.values, y=q_min.index, name="min discharge"))
fig.add_trace(go.Scatter(x=q_max.values, y=q_max.index, name="max discharge"))
fig.update_layout(title="Head dependent min and max discharge limits for "+gen.get_name(), yaxis_title="Head (m)", xaxis_title="Discharge (m3/s)")
fig.show()
```
We set the discharge limits in SHOP and rerun the model.
```{code-cell} ipython3
#Create a standard ShopSession
shop3=ShopSession()
#Build a basic SHOP model
build_model(shop3)
#set the min_discharge and max_discharge attributes
q_min=pd.Series([45.0, 69.0], index=[90.0, 100.0])
q_max=pd.Series([85.0, 109.0], index=[90.0, 100.0])
for gen in shop3.model.generator:
gen.min_discharge.set(q_min)
gen.max_discharge.set(q_max)
#Run model
run_model(shop3)
```
Now we see that the PQ curves are limited by the head-dependent [min_discharge](generator:min_discharge) and [max_discharge](generator:max_discharge) attributes.
```{code-cell} ipython3
for gen in shop3.model.generator:
original_pq_curve=gen.original_pq_curves.get()
fig = go.Figure()
for i, curve in enumerate(original_pq_curve):
if i%6==0: #plot every 6th hour only
fig.add_trace(go.Scatter(x=curve.index, y=curve.values, name=str(curve.name), mode="lines+markers", line=dict(width=2)))
fig.update_layout(title="Original PQ curves of "+gen.get_name(), xaxis_title="Discharge (m3/s)", yaxis_title="Production (MW)", colorway=px.colors.sequential.Plasma)
fig.show()
```
As shown in the figure below, the optimized discharge of the generator is limited by the set max_discharge limit when the effective generator head is low. For higher head values, the generator discharges 90 m3/s since this is the best operating point for the plant.
Note that the max_discharge limit was not defined for head values lower than 90 m, but the effective head of the generator is lower than this for several time steps. SHOP assumes that the limits defined for the lowest and highest head values are valid for all head values outside the range covered by the input data, which can be seen in the figure below.
```{code-cell} ipython3
for gen in shop3.model.generator:
head = gen.eff_head.get()
discharge = gen.discharge.get()
q_min = gen.min_discharge.get()
q_max = gen.max_discharge.get()
fig = go.Figure()
fig.add_trace(go.Scatter(x=discharge.values, y=head.values, name="discharge",mode="markers"))
fig.add_trace(go.Scatter(x=q_min.values, y=q_min.index, name="min discharge",mode="lines"))
fig.add_trace(go.Scatter(x=q_max.values, y=q_max.index, name="max discharge",mode="lines"))
fig.update_layout(title="Discharge and effective head of "+gen.get_name(), xaxis_title="Discharge (m3/s)", yaxis_title="Effective head (m)", colorway=px.colors.sequential.Plasma)
fig.show()
```