Tutorial 4: Simulating Rotor Performance (module_rotor_simulation)

This tutorial demonstrates how to use the RotorSimulation class to simulate turbine performance using tidal resource data and rotor performance data.

The tutorial compares two rotor configurations with different radii:

  • Rotor 1: radius = 0.5 m

  • Rotor 2: radius = 1.0 m

For simplicity, the two configurations use the same tidal resource, rotor performance curves, drivetrain parameters, and power-conversion model. Only the rotor radius is changed.

This tutorial covers:

  1. Loading tidal resource data.

  2. Loading rotor performance data.

  3. Defining simulation configuration dictionaries.

  4. Running RotorSimulation.

  5. Comparing electrical power, torque, TSR, and rotor speed.

Notes for new users

  • This tutorial uses the built-in simple power-conversion model.

  • The simple model requires generator constants Kt and Rw.

  • The rotor simulation uses the adjusted flow speed at turbine depth.

  • Pelec is the main electrical power output used by later LCOE and constraint tutorials.

  • The drivetrain and generator parameters are held fixed for comparison. In a real design study, parameters such as inertia, rated torque, and generator constants may need to scale with rotor size.

[1]:
import numpy as np
import matplotlib.pyplot as plt
import urllib3

from vital.module_tidal import process_tidal_data
from vital.module_rotor import RotorData
from vital.module_rotor_simulation import RotorSimulation

# Suppress warnings caused by NOAA requests using verify=False internally.
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

1. Load tidal resource data

First, load tidal resource data using process_tidal_data.

This tutorial uses the same basic NOAA workflow introduced in the tidal-data tutorial. If NOAA access is unavailable, see the tidal-data tutorial for local-file workflows.

[2]:
station = "COI0303"
startdate = "2021-09-01"

range_hrs = 2 * 7 * 24
time_step_s = 3600
city_data_file = "../data/AlaskaCityLatLong.txt"

tidal = process_tidal_data(
    station=station,
    startdate=startdate,
    range_hrs=range_hrs,
    time_step_s=time_step_s,
    city_data_file=city_data_file,
)

print(f"Data source: {tidal.source}")
print(f"Station name: {tidal.station_name}")
print(f"Mooring distance: {tidal.mooring_distance:.2f} m")
print(f"Number of time points: {len(tidal.times)}")
Data source: NOAA
Station name: Port Mackenzie, South of
Mooring distance: 31.10 m
Number of time points: 336
[3]:
time_days = tidal.times / (3600 * 24)

plt.figure(figsize=(8, 4))
plt.plot(time_days, tidal.flow_speeds)
plt.xlabel("Time (days)")
plt.ylabel("Flow speed (m/s)")
plt.title(f"Tidal resource at {tidal.station_name}")
plt.grid(True)
plt.tight_layout()
plt.show()
../_images/examples_04_rotor_simulation_4_0.png

2. Load rotor performance data

Next, load rotor performance data using the RotorData class.

The rotor file provides TSR, Ct, and Cq. VITAL computes the power coefficient internally as:

\[C_p = TSR \cdot C_q\]

The resulting rotor object provides coefficient functions used by the simulation:

  • rotor.get_cp

  • rotor.get_cq

  • rotor.get_ct

[4]:
rotor_performance_file = "../data/Sitkana_rotor_data_blade_2.txt"

rotor = RotorData(rotor_performance_file)

print(f"Optimal Cp: {rotor.CpOpt:.4f}")
print(f"Optimal TSR: {rotor.TSROpt:.4f}")
print(f"Estimated TSRmax: {rotor.TSRmax:.4f}")
Optimal Cp: 0.2119
Optimal TSR: 0.7648
Estimated TSRmax: 1.7084

3. Define two rotor configurations

The two configurations below use the same drivetrain, generator, and tidal inputs. The only parameter changed is rotor radius.

This makes the comparison easier to interpret.

[5]:
base_config = {
    "Prated": 5000.0,                 # Rated electrical power (W)
    "Trated": 50.0,                   # Rated generator torque (N m)
    "dHub": 2.0,                      # Hub depth (m)
    "dMoor": tidal.mooring_distance,  # Mooring depth/distance (m)
    "Uinf": tidal.flow_speeds,        # Tidal flow speed time series (m/s)
    "t": tidal.times,                 # Time vector (s)

    # Rotor performance functions
    "CpFunc": rotor.get_cp,
    "CqFunc": rotor.get_cq,
    "CtFunc": rotor.get_ct,
    "CpOpt": rotor.CpOpt,
    "TSROpt": rotor.TSROpt,
    "TSRmax": rotor.TSRmax,

    # Drivetrain and generator parameters
    "Ng": 20,
    "Kt": 1.5,
    "Rw": 0.5,
    "J_d": 5.0,
    "B_d": 1.0,
    "J_r": 10.0,

    # Power-conversion model
    "power_model": "simple",
}

config_rotor1 = base_config.copy()
config_rotor1["Radius"] = 0.5

config_rotor2 = base_config.copy()
config_rotor2["Radius"] = 1.0

4. Run rotor simulations

Now run the dynamic rotor simulation for both configurations.

[6]:
rotor_sim1 = RotorSimulation(config_rotor1)
rotor_sim1.simulate()
results_rotor1 = rotor_sim1.get_results()

rotor_sim2 = RotorSimulation(config_rotor2)
rotor_sim2.simulate()
results_rotor2 = rotor_sim2.get_results()
Solving the initial value problem (IVP) for turbine dynamics...
IVP solving complete.
Solving the initial value problem (IVP) for turbine dynamics...
Rated torque exceeded. Switching to open-loop dynamics...
IVP solving complete.

5. Summarize simulation results

The simulation returns time histories for rotor speed, generator torque, thrust, hydrodynamic power, mechanical power, electrical power, and other quantities.

Here we print a few simple summary metrics.

[7]:
def summarize_results(name, config, results):
    print(name)
    print("-" * len(name))
    print(f"Radius: {config['Radius']:.2f} m")
    print(f"Mean adjusted flow speed: {np.mean(results['Uinf_adjusted']):.3f} m/s")
    print(f"Max adjusted flow speed: {np.max(results['Uinf_adjusted']):.3f} m/s")
    print(f"Mean Pelec: {np.mean(results['Pelec']) / 1000:.3f} kW")
    print(f"Max Pelec: {np.max(results['Pelec']) / 1000:.3f} kW")
    print(f"Mean TSR: {np.mean(results['TSR']):.3f}")
    print(f"Max TSR: {np.max(results['TSR']):.3f}")
    print(f"Mean generator torque: {np.mean(results['Tg']):.3f} N m")
    print(f"Max generator torque: {np.max(results['Tg']):.3f} N m")
    print()

summarize_results("Rotor 1", config_rotor1, results_rotor1)
summarize_results("Rotor 2", config_rotor2, results_rotor2)
Rotor 1
-------
Radius: 0.50 m
Mean adjusted flow speed: 1.177 m/s
Max adjusted flow speed: 2.988 m/s
Mean Pelec: 0.010 kW
Max Pelec: 0.138 kW
Mean TSR: 0.144
Max TSR: 0.765
Mean generator torque: 0.457 N m
Max generator torque: 3.904 N m

Rotor 2
-------
Radius: 1.00 m
Mean adjusted flow speed: 1.177 m/s
Max adjusted flow speed: 2.988 m/s
Mean Pelec: 0.463 kW
Max Pelec: 2.718 kW
Mean TSR: 0.619
Max TSR: 1.096
Mean generator torque: 23.266 N m
Max generator torque: 50.000 N m

6. Compare time-series outputs

The plots below compare electrical power, generator torque, tip-speed ratio, and rotor speed for the two rotor radii.

[8]:
time_days = results_rotor1["t"] / (3600 * 24)

fig, ax = plt.subplots(4, 1, figsize=(10, 10), sharex=True)

# Electrical power
ax[0].plot(time_days, results_rotor1["Pelec"] / 1000, label="Rotor 1: R = 0.5 m")
ax[0].plot(time_days, results_rotor2["Pelec"] / 1000, label="Rotor 2: R = 1.0 m")
ax[0].axhline(base_config["Prated"] / 1000, color="k", linestyle="--", linewidth=1, label="Rated power")
ax[0].axhline(0.0, color="gray", linestyle=":", linewidth=1)
ax[0].set_ylabel("Pelec (kW)")
ax[0].set_title("Electrical Power")
ax[0].legend()
ax[0].grid(True)

# Generator torque
ax[1].plot(time_days, results_rotor1["Tg"], label="Rotor 1: R = 0.5 m")
ax[1].plot(time_days, results_rotor2["Tg"], label="Rotor 2: R = 1.0 m")
ax[1].axhline(base_config["Trated"], color="k", linestyle="--", linewidth=1, label="Rated torque")
ax[1].set_ylabel("Tg (N m)")
ax[1].set_title("Generator Torque")
ax[1].legend()
ax[1].grid(True)

# Tip-speed ratio
ax[2].plot(time_days, results_rotor1["TSR"], label="Rotor 1: R = 0.5 m")
ax[2].plot(time_days, results_rotor2["TSR"], label="Rotor 2: R = 1.0 m")
ax[2].axhline(rotor.TSROpt, color="k", linestyle="--", linewidth=1, label="Optimal TSR")
ax[2].set_ylabel("TSR")
ax[2].set_title("Tip-Speed Ratio")
ax[2].legend()
ax[2].grid(True)

# Rotor angular speed
ax[3].plot(time_days, results_rotor1["wr"], label="Rotor 1: R = 0.5 m")
ax[3].plot(time_days, results_rotor2["wr"], label="Rotor 2: R = 1.0 m")
ax[3].set_ylabel("Rotor speed (rad/s)")
ax[3].set_xlabel("Time (days)")
ax[3].set_title("Rotor Angular Speed")
ax[3].legend()
ax[3].grid(True)

plt.tight_layout()
plt.show()
../_images/examples_04_rotor_simulation_14_0.png

Summary

This tutorial showed how to:

  • Load tidal resource data.

  • Load rotor performance data.

  • Configure the RotorSimulation model.

  • Run simulations for two rotor radii.

  • Compare electrical power, torque, TSR, and rotor speed.

The next tutorial uses the simulation results to evaluate design constraints such as power limits, rotor submergence, cavitation, and vessel pitch stability.