Cost of Wind Energy Review 2022#

Be sure to install pip install "waves[examples]" (or pip install ".[examples]") to work with this example.

This example will walk through the process of running a subset of the 2022 Cost of Wind Energy Review (COWER) analysis to demonstrate an analysis workflow. Please note, that this is not the exact workflow because it has been broken down to highlight some of the key features of WAVES. Similarly, this will stay up to date with WAVES's dependencies, namely ORBIT, WOMBAT, and FLORIS, so results may change slightly between this example relying on the configurations and the published results.

Note

To run these examples from the command line, the below command can be used, which will dipslay and save the results by default, with an option to turn those features off. Use waves --help for more information in the command line wherever WAVES is installed.

# NOTE: This is run from the top level of WAVES/

# Run one example
waves library/base_2022 base_fixed_bottom_2022.yaml

# Run both examples, but don't save the results
waves library/base_2022 base_fixed_bottom_2022.yaml base_floating_2022.yaml --no-save-report

Imports and Styling#

from time import perf_counter
from pathlib import Path

import pandas as pd

from waves import Project
from waves.utilities import load_yaml

# Update core Pandas display settings
pd.options.display.float_format = "{:,.2f}".format
pd.options.display.max_columns = 100
pd.options.display.max_rows = 100

Configuration#

First, we need to set the library path, and then we'll load the configuration file, to show some of the configurations. For a complete guide and definition, please see either the API documentation or the How to use WAVES guide.

Warning

If your FLORIS installation is <3.6, then the FLORIS configuration files in library/base_2022/project/config/ will have to be updated so that line 107 (same line number for fixed bottom and floating) is using an absolute path like the example below.

# original, set to work with FLORIS >= 3.6
turbine_library_path: ../../turbines

# updated absolute path, replace <path_to_waves> in your own files
turbine_library_path: <path_to_waves>/WAVES/library/base_2022/turbines/
library_path = Path("../library/base_2022/")
config_fixed = load_yaml(library_path / "project/config", "base_fixed_bottom_2022.yaml")
config_floating = load_yaml(library_path / "project/config", "base_floating_2022.yaml")

# This example was designed prior to the FLORIS 3.6 release, so the path to the turbine library in
# FLORIS must be manually updated, but this example must work for all users, so a dynamic method
# is used below, ensuring this works for all users.
config_fixed["floris_config"] = load_yaml(library_path / "project/config", config_fixed["floris_config"])
config_floating["floris_config"] = load_yaml(library_path / "project/config", config_floating["floris_config"])

config_fixed["floris_config"]["farm"]["turbine_library_path"] = library_path / "turbines"
config_floating["floris_config"]["farm"]["turbine_library_path"] = library_path / "turbines"

Now, we'll create a Project for each of the fixed bottom and floating offshore scenarios, showing the time it takes to initialize each project. Note that we're initializing using the Project.from_dict() classmethod because the configurations are designed to also work with the WAVES command line interface (CLI).

# Add in the library path for both configurations
config_fixed.update({"library_path": library_path,})
config_floating.update({"library_path": library_path,})

start1 = perf_counter()

project_fixed = Project.from_dict(config_fixed)

end1 = perf_counter()

start2 = perf_counter()

project_floating = Project.from_dict(config_floating)

end2 = perf_counter()
print(f"Fixed bottom loading time: {(end1-start1):,.2f} seconds")
print(f"Floating loading time: {(end2-start2):,.2f} seconds")
ORBIT library intialized at '/home/runner/work/WAVES/WAVES/library/base_2022'
Fixed bottom loading time: 3.02 seconds
Floating loading time: 2.85 seconds
FutureWarning: /opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/waves/project.py:114
'H' is deprecated and will be removed in a future version, please use 'h' instead.FutureWarning: /opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/wombat/core/environment.py:503
'H' is deprecated and will be removed in a future version, please use 'h' instead.UserWarning: /opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/ORBIT/phases/design/array_system_design.py:906
Missing data in columns ['bury_speed']; all values will be calculated.FutureWarning: /opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/waves/project.py:114
'H' is deprecated and will be removed in a future version, please use 'h' instead.FutureWarning: /opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/wombat/core/environment.py:503
'H' is deprecated and will be removed in a future version, please use 'h' instead.UserWarning: /opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/ORBIT/phases/design/array_system_design.py:906
Missing data in columns ['bury_speed']; all values will be calculated.

Visualize the wind farm#

Both projects use the same layout, so we'll plot just the fixed bottom plant, noting that the self-connected line at the "OSS1" indicates the unmodeled interconnection point via a modeled export cable.

project_fixed.plot_farm()
_images/70db8c994391423c8008070bbb0a394ec6efc27d0cb2c51de77aee4b94ca1b3d.png

Run the Projects#

Now we'll, run all both the fixed-bottom and floating offshore wind scenarios. Notice that there are additional parameters to use for running the FLORIS model in WAVES: "wind_rose" and "time_series". While time series is more accurate, it can take multiple hours to run for a 20-year, hourly timeseries, and lead to similar results, so we choose the model that will take only a few minutes to run, instead.

Additionally, the wind rose can be computed based on the full weather profile, full_wind_rose=True, for little added computation since WAVES computes a wind rose for each month of the year, for a more accurate energy output. However, we're using just the weather profile used in the O&M phase: full_wind_rose=False.

start1 = perf_counter()
project_fixed.run(
    which_floris="wind_rose",  # month-based wind rose wake analysis
    full_wind_rose=False,  # use the WOMBAT date range
    floris_reinitialize_kwargs={"cut_in_wind_speed": 3.0, "cut_out_wind_speed": 25.0}  # standard ws range
)
project_fixed.wombat.env.cleanup_log_files()  # Delete logging data from the WOMBAT simulations
end1 = perf_counter()

start2 = perf_counter()
project_floating.run(
    which_floris="wind_rose",
    full_wind_rose=False,
    floris_reinitialize_kwargs=dict(cut_in_wind_speed=3.0, cut_out_wind_speed=25.0)
)
project_floating.wombat.env.cleanup_log_files()  # Delete logging data from the WOMBAT simulations
end2 = perf_counter()

print("-" * 29)  # separate our timing from the ORBIT and FLORIS run-time warnings
print(f"Fixed run time: {end1 - start1:,.2f} seconds")
print(f"Floating run time: {end2 - start2:,.2f} seconds")
Correcting negative Overhang:-2.5
Correcting negative Overhang:-2.5
Correcting negative Overhang:-2.5
Correcting negative Overhang:-2.5
Correcting negative Overhang:-2.5
Correcting negative Overhang:-2.5
Correcting negative Overhang:-2.5
Correcting negative Overhang:-2.5
Correcting negative Overhang:-2.5
Correcting negative Overhang:-2.5
Correcting negative Overhang:-2.5
Correcting negative Overhang:-2.5
Correcting negative Overhang:-2.5
Correcting negative Overhang:-2.5
Correcting negative Overhang:-2.5
Correcting negative Overhang:-2.5
Correcting negative Overhang:-2.5
Correcting negative Overhang:-2.5
Correcting negative Overhang:-2.5
Correcting negative Overhang:-2.5
Correcting negative Overhang:-2.5
Correcting negative Overhang:-2.5
Correcting negative Overhang:-2.5
Correcting negative Overhang:-2.5
Correcting negative Overhang:-2.5
Correcting negative Overhang:-2.5
-----------------------------
Fixed run time: 103.74 seconds
Floating run time: 201.99 seconds
UserWarning: /opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/ORBIT/phases/design/array_system_design.py:906
Missing data in columns ['bury_speed']; all values will be calculated.UserWarning: /opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/ORBIT/phases/design/array_system_design.py:906
Missing data in columns ['bury_speed']; all values will be calculated.

Both of these examples can also be run via the CLI, though the FLORIS turbine_library_path configuration will have to be manually updated in each file to ensure the examples run.

waves path/to/library/base_2022/ base_fixed_bottom_2022.yaml base_floating_bottom_2022.yaml --no-save-report

Gather the results#

Another of the conveniences with using WAVES to run all three models is that some of the core metrics are wrapped in the Project API, with the ability to generate a report of a selection of the metrics.

Below, we define the inputs for the report by the following paradigm, where the "metric" and "kwargs" keys must not be changed to ensure their values are read correctly. See the following setup for details.

configuration_dictionary = {
    "Descriptive Name of Metric": {
        "metric": "metric_method_name",
        "kwargs": {
            "metric_kwarg_1": "kwarg_1_value", ...
        }
    }
}

Below, it can be seen that many metrics do not have the "kwargs" dictionary item. This is because an empty dictionary can be assumed to be used when no values need to be configured. In other words, the default method configurations will be relied on, if not otherwise specified.

metrics_configuration = {
    "# Turbines": {"metric": "n_turbines"},
    "Turbine Rating (MW)": {"metric": "turbine_rating"},
    "Project Capacity (MW)": {
        "metric": "capacity",
        "kwargs": {"units": "mw"}
    },
    "# OSS": {"metric": "n_substations"},
    "Total Export Cable Length (km)": {"metric": "export_system_total_cable_length"},
    "Total Array Cable Length (km)": {"metric": "array_system_total_cable_length"},
    "CapEx ($)": {"metric": "capex"},
    "CapEx per kW ($/kW)": {
        "metric": "capex",
        "kwargs": {"per_capacity": "kw"}
    },
    "OpEx ($)": {"metric": "opex"},
    "OpEx per kW ($/kW)": {"metric": "opex", "kwargs": {"per_capacity": "kw"}},
    "AEP (MWh)": {
        "metric": "energy_production",
        "kwargs": {"units": "mw", "aep": True, "with_losses": True}
    },
    "AEP per kW (MWh/kW)": {
        "metric": "energy_production",
        "kwargs": {"units": "mw", "per_capacity": "kw", "aep": True, "with_losses": True}
    },
    "Net Capacity Factor With Wake Losses (%)": {
        "metric": "capacity_factor",
        "kwargs": {"which": "net"}
    },
    "Net Capacity Factor With All Losses (%)": {
        "metric": "capacity_factor",
        "kwargs": {"which": "net", "with_losses": True}
    },
    "Gross Capacity Factor (%)": {
        "metric": "capacity_factor",
        "kwargs": {"which": "gross"}
    },
    "Energy Availability (%)": {
        "metric": "availability",
        "kwargs": {"which": "energy"}
    },
    "LCOE ($/MWh)": {"metric": "lcoe"},
}


# Define the final order of the metrics in the resulting dataframes
metrics_order = [
    "# Turbines",
    "Turbine Rating (MW)",
    "Project Capacity (MW)",
    "# OSS",
    "Total Export Cable Length (km)",
    "Total Array Cable Length (km)",
    "FCR (%)",
    "Offtake Price ($/MWh)",
    "CapEx ($)",
    "CapEx per kW ($/kW)",
    "OpEx ($)",
    "OpEx per kW ($/kW)",
    "Annual OpEx per kW ($/kW)",
    "Energy Availability (%)",
    "Gross Capacity Factor (%)",
    "Net Capacity Factor With Wake Losses (%)",
    "Net Capacity Factor With All Losses (%)",
    "AEP (MWh)",
    "AEP per kW (MWh/kW)",
    "LCOE ($/MWh)",
]

capex_order = [
    "Array System",
    "Export System",
    "Offshore Substation",
    "Substructure",
    "Scour Protection",
    "Mooring System",
    "Turbine",
    "Array System Installation",
    "Export System Installation",
    "Offshore Substation Installation",
    "Substructure Installation",
    "Scour Protection Installation",
    "Mooring System Installation",
    "Turbine Installation",
    "Soft",
    "Project",
]

Before we generate the report, let's see a CapEx breakdown of each scenario. To do this, we'll access ORBIT's ProjectManager object directly to access model-specific functionality. This is available for each model via:

  • project.orbit: provides access to ORBIT's ProjectManager

  • project.wombat provides access to WOMBAT's Simulation

  • project.floris provides access to FLORIS's FlorisInterface

# Capture the CapEx breakdown from each scenario
df_capex_fixed = pd.DataFrame(
    project_fixed.orbit.capex_breakdown.items(),
    columns=["Component", "CapEx ($) - Fixed"]
)
df_capex_floating = pd.DataFrame(
    project_floating.orbit.capex_breakdown.items(),
    columns=["Component", "CapEx ($) - Floating"]
)

# Compute the normalized CapEx for each scenario
df_capex_fixed["CapEx ($/kW) - Fixed"] = df_capex_fixed["CapEx ($) - Fixed"] / project_fixed.capacity("kw")
df_capex_floating["CapEx ($/kW) - Floating"] = df_capex_floating["CapEx ($) - Floating"] / project_floating.capacity("kw")

# Combine the results into one, easy to view dataframe
df_capex = df_capex_fixed.merge(
    df_capex_floating,
    on="Component",
    how="outer",
).fillna(0.0).set_index("Component")
df_capex = df_capex.iloc[pd.Categorical(df_capex.index, capex_order).argsort()]
df_capex
CapEx ($) - Fixed CapEx ($/kW) - Fixed CapEx ($) - Floating CapEx ($/kW) - Floating
Component
Array System 111,193,235.71 185.32 133,234,144.17 222.06
Export System 100,357,800.00 167.26 75,794,538.61 126.32
Offshore Substation 99,479,100.00 165.80 99,479,100.00 165.80
Substructure 307,153,308.59 511.92 630,709,636.60 1,051.18
Scour Protection 10,242,000.00 17.07 0.00 0.00
Mooring System 0.00 0.00 275,612,740.33 459.35
Turbine 1,020,000,000.00 1,700.00 1,020,000,000.00 1,700.00
Array System Installation 64,007,716.58 106.68 88,227,606.73 147.05
Export System Installation 135,777,423.05 226.30 144,141,926.89 240.24
Offshore Substation Installation 7,424,892.50 12.37 15,152,770.85 25.25
Substructure Installation 44,655,354.55 74.43 88,975,886.55 148.29
Scour Protection Installation 44,131,310.50 73.55 0.00 0.00
Mooring System Installation 0.00 0.00 69,384,372.60 115.64
Turbine Installation 58,007,701.61 96.68 0.00 0.00
Soft 325,896,000.00 543.16 325,896,000.00 543.16
Project 151,250,000.00 252.08 151,250,000.00 252.08

Now, let's generate the report, and then add in some additional reporting variables.

project_name_fixed = "COE 2022 - Fixed"
project_name_floating = "COE 2022 - Floating"

# Generate the reports using WAVES and the above configurations
# NOTE: the results are transposed to view them more easily for the example, otherwise
# each row would be a project, which is helpful for combining the results of many scenarios
report_df_fixed = project_fixed.generate_report(metrics_configuration, project_name_fixed).T
report_df_floating = project_floating.generate_report(metrics_configuration, project_name_floating).T

# Gather some additional metadata and results from the projects
n_years_fixed = project_fixed.operations_years
n_years_floating = project_floating.operations_years
additional_reporting_fixed = pd.DataFrame(
    [
        ["FCR (%)", project_fixed.fixed_charge_rate],
        ["Offtake Price ($/MWh)", project_fixed.offtake_price],
        [
            "Annual OpEx per kW ($/kW)",
            report_df_fixed.loc["OpEx per kW ($/kW)", project_name_fixed] / n_years_fixed
        ],
    ],
    columns=["Project"] + report_df_fixed.columns.tolist(),
).set_index("Project")

additional_reporting_floating = pd.DataFrame(
    [
        ["FCR (%)", project_floating.fixed_charge_rate],
        ["Offtake Price ($/MWh)", project_floating.offtake_price],
        [
            "Annual OpEx per kW ($/kW)",
            report_df_floating.loc["OpEx per kW ($/kW)", project_name_floating] / n_years_floating
        ],
    ],
    columns=["Project"] + report_df_floating.columns.tolist(),
).set_index("Project")

# Combine the additional metrics to the generated report
report_df_fixed = pd.concat((report_df_fixed, additional_reporting_fixed), axis=0).loc[metrics_order]
report_df_floating = pd.concat((report_df_floating, additional_reporting_floating), axis=0).loc[metrics_order]

# Combine both reports into one, easy to view dataframe
report_df = report_df_fixed.join(
    report_df_floating,
    how="outer",
).fillna(0.0)
report_df.index.name = "Metrics"

# Format percent-based rows to show as such, not as decimals
report_df.loc[report_df.index.str.contains("%")] *= 100

report_df
COE 2022 - Fixed COE 2022 - Floating
Metrics
# OSS 1.00 1.00
# Turbines 50.00 50.00
AEP (MWh) 2,181,361.26 2,376,659.97
AEP per kW (MWh/kW) 3.64 3.96
Annual OpEx per kW ($/kW) 82.94 74.13
CapEx ($) 2,479,575,843.10 3,117,858,723.33
CapEx per kW ($/kW) 4,132.63 5,196.43
Energy Availability (%) 94.41 90.48
FCR (%) 6.48 6.48
Gross Capacity Factor (%) 52.91 59.35
LCOE ($/MWh) 96.47 103.72
Net Capacity Factor With All Losses (%) 41.47 45.19
Net Capacity Factor With Wake Losses (%) 46.76 51.12
Offtake Price ($/MWh) 83.30 83.30
OpEx ($) 1,044,991,158.07 934,003,093.68
OpEx per kW ($/kW) 1,741.65 1,556.67
Project Capacity (MW) 600.00 600.00
Total Array Cable Length (km) 277.98 333.09
Total Export Cable Length (km) 118.07 89.17
Turbine Rating (MW) 12.00 12.00