diff --git a/examples/00b_wind_farm_scada_power/README.md b/examples/00b_wind_farm_scada_power/README.md new file mode 100644 index 00000000..4835ae49 --- /dev/null +++ b/examples/00b_wind_farm_scada_power/README.md @@ -0,0 +1,27 @@ +# Example 00b: Wind farm SCADA power + +## Description + +Demonstrate the use of `WindFarmSCADAPower` to simulate a wind farm using SCADA power data. `WindFarmSCADAPower` is useful when the input available is not wind speeds but rather SCADA power data. + +## Setup + +As in example 00, the wind farm is a small 3 turbine farm and the input is automatically generated. For `WindFarmSCADAPower` this input is a history of pre-recorded turbine power data in `inputs/scada_input.ftr`. + + Also as in example 00, turbine 0's power is toggled every 100 seconds. This means it will sometimes follow the pre-recorded power data and sometimes be curtailed below it. + +## Running + +To run the example, execute the following command in the terminal: + +```bash +python hercules_runscript.py +``` + +## Outputs + +To plot the outputs run the following command in the terminal: + +```bash +python plot_outputs.py +``` \ No newline at end of file diff --git a/examples/00b_wind_farm_scada_power/hercules_input.yaml b/examples/00b_wind_farm_scada_power/hercules_input.yaml new file mode 100644 index 00000000..8512ce96 --- /dev/null +++ b/examples/00b_wind_farm_scada_power/hercules_input.yaml @@ -0,0 +1,35 @@ +# Input YAML for hercules + +# Name +name: example_00b + +### +# Describe this simulation setup +description: Wind Farm SCADA Power, Logging All Turbine Data + +dt: 1.0 +starttime_utc: "2020-01-01T00:00:00Z" # Midnight Jan 1, 2020 UTC (Zulu time) +endtime_utc: "2020-01-01T00:15:50Z" # 15 minutes 50 seconds later +verbose: False + +plant: + interconnect_limit: 15000 # kW + +wind_farm: + component_type: WindFarmSCADAPower + scada_filename: ../inputs/scada_input.ftr + log_file_name: outputs/log_wind_sim.log + log_channels: + - power + - turbine_powers + - turbine_power_setpoints + + +controller: + + + + + + + diff --git a/examples/00b_wind_farm_scada_power/hercules_runscript.py b/examples/00b_wind_farm_scada_power/hercules_runscript.py new file mode 100644 index 00000000..708f90ef --- /dev/null +++ b/examples/00b_wind_farm_scada_power/hercules_runscript.py @@ -0,0 +1,58 @@ +import numpy as np +from hercules.hercules_model import HerculesModel +from hercules.utilities_examples import ensure_example_inputs_exist, prepare_output_directory + +prepare_output_directory() + +# Ensure example inputs exist +ensure_example_inputs_exist() + +# Initialize the Hercules model +hmodel = HerculesModel("hercules_input.yaml") + + +# Define a simple controller that sets all deratings to full rating +# and then sets the derating of turbine 000 to 500, toggling every other 100 seconds. +class ControllerToggleTurbine000: + """A simple controller that toggles the derating of turbine 000 every other 100 seconds. + + This controller sets all turbines to full rating (5000) and then lowers + the derating of turbine 000 to 500 every other 100 seconds. + """ + + def __init__(self, h_dict): + """Initialize the controller. + + Args: + h_dict (dict): The hercules input dictionary. + """ + pass + + def step(self, h_dict): + """Execute one control step. + + Args: + h_dict (dict): The hercules input dictionary. + + Returns: + dict: The updated hercules input dictionary. + """ + # Set deratings to full rating + h_dict["wind_farm"]["turbine_power_setpoints"] = 5000 * np.ones( + h_dict["wind_farm"]["n_turbines"] + ) + + # Lower t0 derating to 500 every other 100 seconds + if h_dict["time"] % 200 < 100: + h_dict["wind_farm"]["turbine_power_setpoints"][0] = 500 + + return h_dict + + +# Instantiate the controller and assign to the Hercules model +hmodel.assign_controller(ControllerToggleTurbine000(hmodel.h_dict)) + +# Run the simulation +hmodel.run() + +hmodel.logger.info("Process completed successfully") diff --git a/examples/00b_wind_farm_scada_power/plot_outputs.py b/examples/00b_wind_farm_scada_power/plot_outputs.py new file mode 100644 index 00000000..145ad6ff --- /dev/null +++ b/examples/00b_wind_farm_scada_power/plot_outputs.py @@ -0,0 +1,51 @@ +# Plot the outputs of the simulation + +import matplotlib.pyplot as plt +from hercules import HerculesOutput + +# Read the Hercules output file using HerculesOutput +ho = HerculesOutput("outputs/hercules_output.h5") + +# Print metadata information +print("Simulation Metadata:") +ho.print_metadata() +print() + +# Create a shortcut to the dataframe +df = ho.df + +# Set number of turbines +n_turbines = 3 + +# Define a consistent color map with 3 entries +colors = ["tab:blue", "tab:orange", "tab:green"] + +fig, ax = plt.subplots(1, 1, sharex=True) + + +# Plot the power +for t_idx in range(3): + if f"wind_farm.turbine_powers.{t_idx:03}" in df.columns: + ax.plot( + df["time"], + df[f"wind_farm.turbine_powers.{t_idx:03}"], + label=f"Turbine {t_idx}", + color=colors[t_idx], + ) + +# Check if derating columns exist and plot them if they do +for t_idx in range(3): + if f"wind_farm.turbine_power_setpoints.{t_idx:03}" in df.columns: + ax.plot( + df["time"], + df[f"wind_farm.turbine_power_setpoints.{t_idx:03}"], + label=f"Power Setpoint {t_idx}", + linestyle="--", + color=colors[t_idx], + ) + +ax.grid(True) +ax.legend() +ax.set_xlabel("Time [s]") +ax.set_ylabel("Power [kW]") +plt.show() diff --git a/examples/inputs/00_generate_wind_history_small.py b/examples/inputs/00_generate_wind_history_small.py index f7d3a136..1528e0f3 100644 --- a/examples/inputs/00_generate_wind_history_small.py +++ b/examples/inputs/00_generate_wind_history_small.py @@ -1,5 +1,7 @@ # # Generate wind history for a small farm for early examples # Generate a small demonstration wind history using the example FLORIS model +# Additionally, generate a history of wind power data for a small farm for use +# in the example with WindFarmSCADAPower. import floris.layout_visualization as layoutviz import matplotlib.pyplot as plt import numpy as np @@ -100,5 +102,22 @@ print(f"First time (UTC): {df['time_utc'].iloc[0]}") print(f"Last time (UTC): {df['time_utc'].iloc[-1]}") +# Now generate rough wind power approximations +pow_000 = 4 * ws_0**3 +pow_001 = 4 * ws_1**3 +pow_002 = 4 * ws_2**3 + +# Clip the powers to be less than the rated power +rated_power = 5000 +pow_000 = np.minimum(pow_000, rated_power) +pow_001 = np.minimum(pow_001, rated_power) +pow_002 = np.minimum(pow_002, rated_power) + +df["pow_000"] = pow_000 +df["pow_001"] = pow_001 +df["pow_002"] = pow_002 + +df.to_feather("scada_input.ftr") + if show_plots: plt.show() diff --git a/hercules/utilities_examples.py b/hercules/utilities_examples.py index 7c85e457..8a75df3b 100644 --- a/hercules/utilities_examples.py +++ b/hercules/utilities_examples.py @@ -13,6 +13,7 @@ def generate_example_inputs(): produced under `examples/inputs`: - examples/inputs/00_generate_wind_history_small.py -> wind_input_small.ftr + and scada_input.ftr - examples/inputs/01_generate_wind_history_large.py -> wind_input_large.ftr - examples/inputs/02_generate_solar_history.py -> solar_input.ftr @@ -53,6 +54,7 @@ def ensure_example_inputs_exist(): inputs_dir / "wind_input_small.ftr", inputs_dir / "wind_input_large.ftr", inputs_dir / "solar_input.ftr", + inputs_dir / "scada_input.ftr", ] if not all(p.exists() for p in expected_files):