apply_rulecurve Scheme Example

This example shows how to use the ReservoirModel.apply_rulecurve() scheme when modelling a single reservoir model.

Note

For details about the full model file structure please see Basic Single Reservoir.

We consider a reservoir with a single inflow, Q_in, and an outflow Q_out. Q_out is comprised of a single component, a turbine, Q_turbine. The reservoir outflow should be determined to achieve the rulecurve (elevation of 1600m). The rulecurve elevation should be achieved in single timestep with a maximum outflow of 10m3/s.

The ReservoirModel.apply_rulecurve() scheme can be applied to model these operations.

Main Model (python) File

An example of the main model file rulecurve_example.py is given below.

 1"""Example that illustrates use of the rulecurve scheme.
 2
 3This example shows two variants:
 4- SingleReservoir: basic rule curve control
 5- SingleReservoirWithQmin: rule curve with minimum discharge enforcement
 6
 7To switch between them, change the class passed to run_simulation_problem.
 8"""
 9from pathlib import Path
10
11from rtctools.util import run_simulation_problem
12
13from rtctools_simulation.reservoir.model import ModelConfig, ReservoirModel
14
15CONFIG = ModelConfig(base_dir=Path(__file__).parent)
16
17
18class SingleReservoir(ReservoirModel):
19    """Example single reservoir model."""
20
21    def pre(self, *args, **kwargs):
22        super().pre(*args, **kwargs)
23        self.calculate_rule_curve_deviation(periods=3, h_var="H_observed")
24        self.adjust_rulecurve(
25            periods=3,
26            extrapolate_trend_linear=False,
27        )
28
29    def apply_schemes(self):
30        """Apply schemes for controlling the reservoir."""
31        self.apply_rulecurve()
32
33
34class SingleReservoirWithQmin(SingleReservoir):
35    """Example reservoir with minimum discharge enforcement.
36
37    Extends SingleReservoir by enforcing a minimum outflow as
38    configured by Reservoir_Qmin. When the reservoir level drops
39    toward dead storage (Reservoir_Hdead, with buffer Reservoir_Hbuffer),
40    the effective Qmin is linearly reduced to prevent over-release.
41
42    Requires parameters Reservoir_Qmin, Reservoir_Hdead,
43    and Reservoir_Hbuffer in rtcParameterConfig.xml.
44    """
45
46    def apply_schemes(self):
47        """Apply rule curve with Qmin enforcement."""
48        self.apply_rulecurve(enforce_qmin=True)
49
50
51# Create and run the model.
52# Change to SingleReservoir to disable Qmin enforcement.
53if __name__ == "__main__":
54    run_simulation_problem(SingleReservoirWithQmin, config=CONFIG)

The template file mentioned in the Basic Single Reservoir will look very similar to this file, except that the apply_schemes() method still needs to be filled out.

The line

CONFIG = ModelConfig(base_dir=Path(__file__).parent)

sets the model configuration. This model configuration is defined by the base directory base_dir. In most cases, the base directory is Path(__file__).parent, which is the directory of the current file.

The line

To switch between them, change the class passed to run_simulation_problem.

defines a class SingleReservoir that inherits all properties and functionalities of the predefined class ReservoirModel. An overview of this class can be found in Reservoir API and details of the underlying model it uses can be found in Single Reservoir Model.

The method ReservoirModel.apply_schemes() is called every timestep and contains the logic for which schemes are applied. The first argument self is the SingleReservoir object itself. Since SingleReservoir inherits from ReservoirModel, self can call any of the ReservoirModel methods, such as ReservoirModel.apply_rulecurve(). An overview of all available ReservoirModel methods can be found in Reservoir API.

The ReservoirModel.apply_rulecurve() scheme is then applied to set the reservoir outflow through the turbine. It will aim to match the simulated elevation to the provided rule curve. There are functions provided that can alter the originally provided rule curve to account for differences with observations (e.g. during a very dry year, or after some maintenance project). This will need to be computed before model simulation.

In the method ReservoirModel.pre() functions are called that accomplish certain pre-processing objectives. In this model, we compute the deviation of the observed elevations to the provided rule curve. Based on these deviations, the original rule curve is adjusted. In this case, we take the latest known deviation and apply that to all timesteps after the end of H_observed. There is also functionality to provide an application time, average deviations over a moving window, or extrapolate the deviations linearly after the application time.

Qmin Enforcement

The ReservoirModel.apply_rulecurve() method supports minimum discharge (Qmin) enforcement via the enforce_qmin parameter:

class SingleReservoirWithQmin(SingleReservoir):
    """Example reservoir with minimum discharge enforcement.

    Extends SingleReservoir by enforcing a minimum outflow as
    configured by Reservoir_Qmin. When the reservoir level drops
    toward dead storage (Reservoir_Hdead, with buffer Reservoir_Hbuffer),
    the effective Qmin is linearly reduced to prevent over-release.

    Requires parameters Reservoir_Qmin, Reservoir_Hdead,
    and Reservoir_Hbuffer in rtcParameterConfig.xml.
    """

    def apply_schemes(self):
        """Apply rule curve with Qmin enforcement."""
        self.apply_rulecurve(enforce_qmin=True)

When enforce_qmin=True, the method uses ReservoirModel.get_feasible_qmin() internally to compute a feasible minimum outflow as the minimum of two constraints:

  1. Policy constraint: Qmin reduces linearly between Reservoir_Hbuffer and Reservoir_Hdead

  2. Physical constraint: Cannot release more than available above dead storage

If the rule curve discharge is below the feasible Qmin, the discharge is raised to the feasible Qmin. This ensures that Qmin enforcement doesn’t attempt to release more water than physically available, which could occur when the reservoir level is near dead storage.

The SingleReservoirWithQmin class in examples/rulecurve_example/rulecurve_example.py demonstrates this usage. For detailed test scenarios covering linear reduction and physical constraints, see tests/feasible_qmin_test.py.

Lookup tables

The ReservoirModel.apply_rulecurve() scheme uses a lookup table v_from_h. This uses the same data as the h_from_v lookup table, the data mapping can be achieved in the lookup_tables.csv file.

<base_dir>/lookup_tables/lookup_tables.csv

name

data

var_in

var_out

h_from_v

v_h.csv

volume_m3

height_m

v_from_h

v_h.csv

height_m

volume_m3

area_from_v

v_area.csv

volume_m3

area_m2

qout_from_v

qout_v.csv

day volume_m3

qout_m3_per_s

qspill_from_h

h_qspill.csv

height_m

qspill_m3_per_s

This model also uses the standard lookup table h_from_v. For other lookup tables, defaults from the generated template files can be used.

Note

For further details about the lookup tables please see Basic Single Reservoir.

Input Data Files

The ReservoirModel.apply_rulecurve() scheme requires the following parameters from the rtcParameterConfig.xml file:

  • Reservoir_Qmax: Upper limiting discharge while blending pool elevation (m³/s)

  • rule_curve_blend: Number of timesteps over which to converge the reservoir elevation to the rule curve target. The discharge is computed as Q = (V_current - V_target) / rule_curve_blend. A value of 1 aims to match the rule curve elevation at each timestep, while values > 1 cause gradual convergence.

When using enforce_qmin=True, the following additional parameters must be configured in rtcParameterConfig.xml:

  • Reservoir_Qmin (required): Full minimum outflow (m³/s) when reservoir is above Reservoir_Hbuffer. A ValueError is raised if this parameter is missing.

  • Reservoir_Hdead (optional, default: 0): Dead storage elevation (m). Qmin is zero at or below this level.

  • Reservoir_Hbuffer (optional, default: Reservoir_Hdead): Elevation (m) where Qmin reduction begins. Must be >= Reservoir_Hdead. When Reservoir_Hbuffer equals Reservoir_Hdead (the default), there is no gradual reduction - full Qmin applies above Reservoir_Hdead and drops to zero at or below it.

An example showing all parameters for enforce_qmin usage:

        <parameter id="rule_curve_blend">
            <dblValue>1</dblValue>
        </parameter>
        <parameter id="Reservoir_Qmax">
            <dblValue>10</dblValue>
        </parameter>
        <parameter id="Reservoir_Qmin">
            <dblValue>1</dblValue>
        </parameter>
        <parameter id="Reservoir_Hdead">
            <dblValue>1550</dblValue>
        </parameter>
        <parameter id="Reservoir_Hbuffer">
            <dblValue>1570</dblValue>
        </parameter>

The scheme also requires an additional input timeseries, rulecurve. This data is provided in the timeseries_import.xml.

	<series>
		<header>
			<type>instantaneous</type>
			<moduleInstanceId>reservoir</moduleInstanceId>
			<locationId>reservoir</locationId>
			<parameterId>rulecurve</parameterId>
			<timeStep unit="second" multiplier="3600"/>
			<startDate date="2022-06-07" time="06:00:00"/>
			<endDate date="2022-06-27" time="06:00:00"/>
			<forecastDate date="2022-06-07" time="06:00:00"/>
			<missVal>-999.0</missVal>
			<stationName>Reservoir1</stationName>
			<units>m</units>
		</header>
		<event date="2022-06-07" time="06:00:00" value="1600" flag="8"/>
		<event date="2022-06-07" time="07:00:00" value="1600" flag="8"/>
		<event date="2022-06-07" time="08:00:00" value="1600" flag="8"/>
		<event date="2022-06-07" time="09:00:00" value="1600" flag="8"/>
		<event date="2022-06-07" time="10:00:00" value="1600" flag="8"/>
		<event date="2022-06-07" time="11:00:00" value="1600" flag="8"/>
		<event date="2022-06-07" time="12:00:00" value="1600" flag="8"/>
		<event date="2022-06-07" time="13:00:00" value="1600" flag="8"/>
		<event date="2022-06-07" time="14:00:00" value="1600" flag="8"/>
		<event date="2022-06-07" time="15:00:00" value="1600" flag="8"/>
		<event date="2022-06-07" time="16:00:00" value="1600" flag="8"/>
		<event date="2022-06-07" time="17:00:00" value="1600" flag="8"/>
		<event date="2022-06-07" time="18:00:00" value="1600" flag="8"/>
		<event date="2022-06-07" time="19:00:00" value="1600" flag="8"/>
		<event date="2022-06-07" time="20:00:00" value="1600" flag="8"/>
		<event date="2022-06-07" time="21:00:00" value="1600" flag="8"/>
		<event date="2022-06-07" time="22:00:00" value="1600" flag="8"/>

The data is mapped to the variable, rulecurve via the rtcDataConfig.xml.

	<timeSeries id="rule_curve">
		<PITimeSeries>
			<locationId>reservoir</locationId>
			<parameterId>rulecurve</parameterId>
		</PITimeSeries>
	</timeSeries>

Note

For further details about input file structure please see Basic Single Reservoir.

Output Data

The results of the simulation will appear in the output folder in a file called timeseries_export.xml. The data is linked to model variables via the rtcDataConfig.xml in the same way as with timeseries_import.xml.

Automatic Plotting

You can optionally include a plot_table.csv in the input folder. This is used by the rtc-tools-interfaces module (automatically installed with this package) to plot the model output. For more details on how to use this file and visualize results, see RTC-Tools-Interface.

The results of the simulation run can be seen in the plot below.