CSM by Example#

To start, we'll import the relevant packages for the whole demonstration. For a more complete definition of all that is made available for each model, please see the CSMBase (base model) documentation, or any of the other available models.

import numpy as np
import pandas as pd

from csm import get_model, Land2015NLR, available_models
from csm.tools import plot

pd.options.display.float_format = "{:,.2f}".format

What Models Are Available and How Do I Set Them Up?#

For absolute beginners, we can see which models are available by printing available_models.

print(available_models)
('CSMBase', 'Land2015NLR', 'Land2020NLR', 'Land2021NLR')

For users that don't want to deal with importing multiple models, simply retrieve them with get_model(), which makes a series of aliases available for convenience.

assert Land2015NLR == get_model(2015) == get_model("land-2015") == get_model("nlr-2015") == get_model("NLR 2015")

Now that we have a variety of convenient means for accessing available models, we can also print out the minimum required inputs to run the model.

nlr2015 = get_model("2015")
print(nlr2015.get_required_inputs())
['turbine_class', 'rated_power_kw', 'rotor_diameter', 'num_bearings', 'blade_has_carbon', 'max_tip_speed', 'tower_length']

Also available with units.

print(Land2015NLR.get_required_inputs(include_units=True))
[('turbine_class', 'unitless'), ('rated_power_kw', 'kW'), ('rotor_diameter', 'm'), ('num_bearings', 'unitless'), ('blade_has_carbon', 'unitless'), ('max_tip_speed', 'm/s'), ('tower_length', 'm')]

Replicating Results from the 2015 WISDEM/Excel Model#

The Land2015NLR model is based on teh WISDEM implementation of the CSM, and the configuration dictionary is also based on the WISDEM example. To create the model itself, we use the from_dict method to pass a configuration dictionary, which has additional error handling to better guide users if their configurations fail to initialize a model.

wisdem_test_inputs = {
    "turbine_class": 1,
    "efficiency_max": 0.9,
    "num_blades": 3,
    "num_bearings": 2,
    "rated_power_kw": 5000,
    "blade_has_carbon": False,
    "has_crane": True,
    "rotor_diameter": 126,
    "max_tip_speed": 80,
    "tower_length": 90,
}

nlr2015 = Land2015NLR.from_dict(wisdem_test_inputs)
nlr2015.run()
print(f"Turbine cost (USD/kW): ${nlr2015.turbine_cost_kw:,.2f}")
Turbine cost (USD/kW): $686.00

For incomplete configurations, this approach will fail, as shown below. However, if we add partial=True, a partial model can be defined. This is useful for parameter sweeps or calculating a limited number of component values.

partial_inputs = {
    "turbine_class": 1,
    "num_blades": 3,
    "rated_power_kw": 5000,
    "blade_has_carbon": False,
    "rotor_diameter": 126,
    "max_tip_speed": 80,
}

partial2015 = Land2015NLR.from_dict(partial_inputs)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[7], line 10
      6     "rotor_diameter": 126,
      7     "max_tip_speed": 80,
      8 }
      9 
---> 10 partial2015 = Land2015NLR.from_dict(partial_inputs)

File /opt/hostedtoolcache/Python/3.12.14/x64/lib/python3.12/site-packages/csm/models/base_model.py:874, in CSMBase.from_dict(cls, data, partial)
    869 if missing and not partial:
    870     msg = (
    871         f"The class definition for {_name} is missing the following"
    872         f" inputs: {', '.join(missing)}"
    873     )
--> 874     raise AttributeError(msg)
    875 return cls(**data)

AttributeError: The class definition for Land2015NLR is missing the following inputs: num_bearings, tower_length

Now, using partial=True, we can create an incomplete model and get the results we desire.

partial2015 = Land2015NLR.from_dict(wisdem_test_inputs, partial=True)
partial2015.calculate("blade_mass", "blade_cost")
print(f"Single blade mass (kg): {partial2015.blade_mass:,.2f}")
print(f"Single blade cost (thousands, USD): ${partial2015.blade_cost/1000:,.2f}")
Single blade mass (kg): 18,590.67
Single blade cost (thousands, USD): $271.42

Getting Results#

Single Model Runs#

For basic dictionary outputs, any of get_results, get_mass_results, get_cost_results, or get_all_results. Below, we demonstrate retrieving the blade results.

nlr2015.get_results(["blade_mass", "blade_cost"])
{'blade_mass': 18590.668206489747, 'blade_cost': 271423.7558147503}

For a more complete breakdown, we can also get a DataFrame of all the major mass and cost results.

df = nlr2015.get_component_breakdown()
df["Cost (USD)"] /= 1000
df = df.rename(columns={"Cost (USD)": "Cost (thousands USD)"})
df
Mass (kg) Cost (thousands USD)
Component
bearing 2,245.42 10.10
bedplate 41,765.26 121.12
blade 18,590.67 271.42
blade_transport NaN 0.00
brake 5,337.50 19.35
controls 0.00 105.75
converter 0.00 0.00
drivetrain_transport NaN 0.00
electrical_connection 0.00 209.25
gearbox 21,875.00 218.75
generator 14,900.00 184.76
high_speed_shaft 994.70 6.76
hub 44,078.54 171.91
hub_system 55,850.44 421.36
hub_transport NaN 0.00
hydraulic_cooling 400.00 49.60
low_speed_shaft 22,820.28 271.56
nacelle 151,455.88 1,665.61
nacelle_cover 6,836.69 38.97
parts_transport NaN 0.00
pitch_system 10,798.91 238.66
platform_mainframe 8,220.66 101.27
power_electronics_transport NaN 0.00
rotor 111,622.45 1,235.63
spinner 973.00 10.80
tower 182,336.48 528.78
tower_transport NaN 0.00
transformer 11,485.00 215.92
transport NaN 0.00
turbine 445,414.81 3,430.02
turbine_kw NaN 0.69
yaw_system 12,329.96 102.34

IRS Domestic Content Tables#

For helping with calculating the total domestic content, both irs_mpc_breakdown and total_domestic_content are made available. Note that in both the below examples, the data are purely expository.

nlr2015.irs_mpc_breakdown(
    turbine_production_cost=1500,
    tower_flange_material_cost=2000,
    tower_flange_production_cost=1000,
)
cost Value
APC MPC
Wind Turbine Blade 814,271.27 28.02
Rotor Hub 421,362.42 14.50
Nacelle 1,665,612.93 57.32
Power Converter - -
Production 1,500.00 0.05
Wind Tower Flange Material 2,000.00 0.07
Production 1,000.00 0.03
Tower - - -
Steel or iron products in foundation - - -
Total - 2,905,746.61 100.00
nlr2015.total_domestic_content(
    turbine_production_cost=1500,
    tower_flange_material_cost=2000,
    tower_flange_production_cost=1000,
    domestic=["tower_flange_material", "tower_flange_production", "nacelle"],
    return_table=True,
)
category cost Value Domestic
APC MPC
Wind Turbine Blade blade 814,271.27 28.02 0.00
Rotor Hub hub 421,362.42 14.50 0.00
Nacelle nacelle 1,665,612.93 57.32 0.00
Power Converter power_converter 0.00 0.00 0.00
Production turbine_production 1,500.00 0.05 0.00
Wind Tower Flange Material tower_flange_material 2,000.00 0.07 0.07
Production tower_flange_production 1,000.00 0.03 0.03
Tower - - 0.00 0.00 0.00
Steel or iron products in foundation - - 0.00 0.00 0.00
Total - - 2,905,746.61 100.00 0.10

Parameter Sweeps#

For experimental designs to understand tradeoffs in cost and mass, users can run parameter sweeps using either the parameterize or parameterize_subset. For parameterize, the entire model is run, but for parameterize_subset only a single input can be parameterized.

Note that in the below example, the model does not have to be created first. Additionally, we can provide parameterizations as either a "range" of inputs defining the minimum value, maximum value, and the number of points to generate in between them, or the "inputs," which uses the values as provided.

parameters = {
    "max_tip_speed": ("range", 80, 90, 3),
    "tower_length": ("inputs", 80, 110),
}
results = ["nacelle_mass", "nacelle_cost", "tower_mass", "tower_cost", "turbine_cost_kw"]
Land2015NLR.parameterize(
    base_kwargs=wisdem_test_inputs,
    parameterized_kwargs=parameters,
    results=results
)
max_tip_speed 80.00 85.00 90.00
tower_length 80 110 80 110 80 110
nacelle_cost 1,665,612.93 1,665,612.93 1,651,607.01 1,651,607.01 1,639,157.31 1,639,157.31
nacelle_mass 151,455.88 151,455.88 149,855.15 149,855.15 148,432.27 148,432.27
tower_cost 416,412.75 794,382.29 416,412.75 794,382.29 416,412.75 794,382.29
tower_mass 143,590.60 273,924.93 143,590.60 273,924.93 143,590.60 273,924.93
turbine_cost_kw 663.53 739.13 660.73 736.32 658.24 733.83

Plotting#

Mass vs Cost Comparison for a Single Parameter Sweep#

In the following example, we use each of the available models to create their cost and scaling curves for a single rotor blade as we modify the rotor diameter. We additionally, plot 2 ATB reference turbines and 3 industry reference turbines as examples for each of the models. Finally, we add some synthetic empirical data to visually inspect how the curves fit the empirical data.

In addition to the model and data inputs, we can provide many formatting arguments to control display elements such as the line colors, reference turbine markers, empirical data scatter markers, axis limits, and legend formatting.

Please see the plot_mass_cost_comparison documentation for full details on available settings.

model_base = {
    "NLR 2015": {"turbine_class": 1, "blade_has_carbon": False},
    "NLR 2020": {},
    "NLR 2021": {},
}
parameterization = {"rotor_diameter": ["range", 20, 200, 21]}
reference_turbs = {
    "ATB T3 (3.3MW)": {
        "rated_power_kw": 3300,
        "rotor_diameter": 148.0,
        "tower_length": 100,
        "max_tip_speed": 90,
        "num_bearings": 1,
        "num_blades": 3,
        "efficiency_max": 0.9,
        "turbine_class": 1,
        "blade_has_carbon": False,
    },
    "ATB T1 (6.0MW)": {
        "rated_power_kw": 6000,
        "rotor_diameter": 170.0,
        "tower_length": 115.0,
        "max_tip_speed": 90.0,
        "num_bearings": 1,
        "num_blades": 3,
        "efficiency_max": 0.9,
        "turbine_class": 1,
        "blade_has_carbon": False,
    },
    "SG 5.2-165": {
        "rated_power_kw": 5200,
        "rotor_diameter": 165.0,
        "tower_length": 105.0,
        "max_tip_speed": 90.0,
        "num_bearings": 1,
        "num_blades": 3,
        "efficiency_max": 0.9,
        "turbine_class": 1,
        "blade_has_carbon": False,
    },
    "SG 7.0-170": {
        "rated_power_kw": 7000,
        "rotor_diameter": 170.0,
        "tower_length": 135.0,
        "max_tip_speed": 90.0,
        "num_bearings": 1,
        "num_blades": 3,
        "efficiency_max": 0.9,
        "turbine_class": 1,
        "blade_has_carbon": False,
    },
    "V150-4.5": {
        "rated_power_kw": 4500,
        "rotor_diameter": 150.0,
        "tower_length": 105.0,
        "max_tip_speed": 82.0,
        "num_bearings": 1,
        "num_blades": 3,
        "efficiency_max": 0.9,
        "turbine_class": 1,
        "blade_has_carbon": False,
    },
}
data = [
    [47, 2400, np.nan],
    [47, 1300, np.nan],
    [23, 300, np.nan],
    [80, 6500, np.nan],
    [149, 21500, np.nan],
    [92, 8000, np.nan],
    [96.1, 11800, np.nan],
]
empirical_data = pd.DataFrame(data, columns=["rotor_diameter", "blade_mass", "blade_cost"])

model_line_fmt = {
    "NLR 2020": {"c": "tab:blue"},
    "NLR 2021": {"c": "tab:orange", "ls": "dashed"},
    "NLR 2015": {"c": "tab:green"},
}
turbine_formatting = {
    "ATB T3 (3.3MW)": {"marker": "*"},
    "ATB T1 (6.0MW)": {"marker": "^"},
    "SG 5.2-165": {
        "marker": "s",
    },
    "SG 7.0-170": {
        "marker": "D",
    },
    "V150-4.5": {
        "marker": "o",
    },
}
legend_formatting = {
    "loc": "outside lower center",
    "ncols": 6,
    "bbox_to_anchor": (0.5, -0.1),
    "frameon": False,
}

plot.plot_mass_cost_comparison(
    base_kwargs=model_base,
    parameterization=parameterization,
    component="blade",
    parameter_label="Rotor Diameter (m)",
    cost_basis="thousands",
    reference_turbines=reference_turbs,
    background_data=empirical_data,
    mass_xlim=(0, 200),
    mass_ylim=(0, 40),
    cost_xlim=(0, 40),
    cost_ylim=(0, 700),
    model_plot_settings=model_line_fmt,
    turbine_scatter_settings=turbine_formatting,
    reference_scatter_kwargs={"c": "none", "edgecolor": "dimgray"},
    background_scatter_kwargs={"marker": ".", "c": "lightgray"},
    legend_kwargs=legend_formatting,
)
../_images/4e0a58733160d5bf92e03655c5269a73df29b6e72f24ad554dfe5756576f9e4e.png