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()
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'sProjectManager
project.wombat
provides access to WOMBAT'sSimulation
project.floris
provides access to FLORIS'sFlorisInterface
# 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 |