02: Monopile

02: Monopile#

In this example, we show Ard handling gradient computation.

As in Example 01, we start by loading what we need to run the problem.

from pathlib import Path  # optional, for nice path specifications

import pprint as pp  # optional, for nice printing
import numpy as np  # numerics library
import matplotlib.pyplot as plt  # plotting capabilities

import ard  # technically we only really need this
from ard.utils.io import load_yaml  # we grab a yaml loader here
from ard.api import set_up_ard_model  # the secret sauce
from ard.viz.layout import plot_layout  # a plotting tool!

import openmdao.api as om  # for N2 diagrams from the OpenMDAO backend

# import optiwindnet.plotting

%matplotlib inline

Now, we can set up different case. As before, we do it a little verbosely so that our documentation system can grab it, you can generally just use relative paths. We grab the file at inputs/ard_system.yaml, which describes the Ard system for this problem. It references, in turn, the inputs/windio.yaml file, which is where we define the plant we want to optimize, and an initial setup for it.

# load input
path_inputs = Path.cwd().absolute() / "inputs"
input_dict = load_yaml(path_inputs / "ard_system.yaml")

# set up system
prob = set_up_ard_model(input_dict=input_dict, root_data_path=path_inputs)
Running OpenMDAO util to clean the output directories...
	No OpenMDAO output directories found.
... done.

Created top-level OpenMDAO problem: top_level.
Adding top_level.
    Adding layout2aep.
        Adding layout to layout2aep.
        Adding aepFLORIS to layout2aep.
	Activating approximate totals on layout2aep
    Adding landuse.
    Adding collection.
    Adding spacing_constraint.
    Adding tcc.
    Adding orbit.
    Adding opex.
    Adding financese.
System top_level built.
System top_level set up.

Here, you should see each of the groups or components described as they are added to the Ard model and, occasionally, some options being turned on on them. Comparing to Example 01, you can notice that the balance-of-station cost model (BOS) is different: before it was landbosse and now it is orbit, and this is so we can generate an estimate for offshore BOS costs.

As before, we leave in some turned-off-by-default code here in case you want to see what the connections of the system look like with an N2 diagram vizualization.

if False:
    # visualize model
    om.n2(prob)

Here's the one-shot analysis.

# run the model
prob.run_model()

# collapse the test result data
test_data = {
    "AEP_val": float(prob.get_val("AEP_farm", units="GW*h")[0]),
    "CapEx_val": float(prob.get_val("tcc.tcc", units="MUSD")[0]),
    "BOS_val": float(prob.get_val("orbit.total_capex", units="MUSD")[0]),
    "OpEx_val": float(prob.get_val("opex.opex", units="MUSD/yr")[0]),
    "LCOE_val": float(prob.get_val("financese.lcoe", units="USD/MW/h")[0]),
    "area_tight": float(prob.get_val("landuse.area_tight", units="km**2")[0]),
    "coll_length": float(prob.get_val("collection.total_length_cables", units="km")[0]),
    "turbine_spacing": float(
        np.min(prob.get_val("spacing_constraint.turbine_spacing", units="km"))
    ),
}

print("\n\nRESULTS:\n")
pp.pprint(test_data)
print("\n\n")
UserWarning: /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/floris/core/flow_field.py:169
'where' used without 'out', expect unitialized memory in output. If this is intentional, use out=None.
ORBIT library intialized at '/opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/library'
RESULTS:

{'AEP_val': 2494.7668991169503,
 'BOS_val': 1275.5640171891396,
 'CapEx_val': 768.4437570425,
 'LCOE_val': 85.69962313635394,
 'OpEx_val': 60.50000000000001,
 'area_tight': 63.234304,
 'coll_length': 47.761107521256534,
 'turbine_spacing': 1.988}

Now, we can optimize! The optimization details are set under the analysis_options header in inputs/ard_system.yaml. As before, we use a four-dimensional rectilinear layout parameterization (\(\theta\)) as design variables, and constrain the farm such that the turbines are in the boundaries and satisfactorily spaced. However, the derivatives of AEP with respect to layout variables are known to be non-smooth, and FLORIS doesn't provide analytical derivatives; to use finite differencing we would be adding noise on top of the noise, making useful gradients even harder to detect! Ard is designed to make overcoming this easier, and more about that is in the pipeline, but for now we will avoid the complexities of that discussion. Instead, we optimize to minimize the cable length here, while $\( \begin{aligned} \textrm{minimize}_\theta \quad & \mathrm{LCOE}(\theta, \ldots) \\ \textrm{subject to} \quad & \mathrm{AEP}(\theta, \ldots) > \mathrm{AEP}_{\min} \\ & f_{\mathrm{spacing}}(\theta, \ldots) < 0 \\ & f_{\mathrm{boundary}}(\theta, \ldots) < 0 \end{aligned} \)$

optimize = True  # set to False to skip optimization
if optimize:
    # run the optimization
    prob.run_driver()
    prob.cleanup()

    # collapse the test result data
    test_data = {
        "AEP_val": float(prob.get_val("AEP_farm", units="GW*h")[0]),
        "CapEx_val": float(prob.get_val("tcc.tcc", units="MUSD")[0]),
        "BOS_val": float(prob.get_val("orbit.total_capex", units="MUSD")[0]),
        "OpEx_val": float(prob.get_val("opex.opex", units="MUSD/yr")[0]),
        "LCOE_val": float(prob.get_val("financese.lcoe", units="USD/MW/h")[0]),
        "area_tight": float(prob.get_val("landuse.area_tight", units="km**2")[0]),
        "coll_length": float(
            prob.get_val("collection.total_length_cables", units="km")[0]
        ),
        "turbine_spacing": float(
            np.min(prob.get_val("spacing_constraint.turbine_spacing", units="km"))
        ),
    }

    # clean up the recorder
    prob.cleanup()

    # print the results
    print("\n\nRESULTS (opt):\n")
    pp.pprint(test_data)
    print("\n\n")
    # plot convergence
    ## read cases
    cr = om.CaseReader(
        prob.get_outputs_dir() / input_dict["analysis_options"]["recorder"]["filepath"]
    )

    # Extract the driver cases
    cases = cr.get_cases("driver")

    # Initialize lists to store iteration data
    iterations = []
    objective_values = []

    # Loop through the cases and extract iteration number and objective value
    for i, case in enumerate(cases):
        iterations.append(i)
        obj_keys = input_dict["analysis_options"]["objectives"].keys()
        assert (len(obj_keys)) == 1
        objective_values.append(
            case.get_objectives()[next(iter(obj_keys))]  # get the unique entry
        )

    # Plot the convergence
    plt.figure(figsize=(8, 6))
    plt.plot(iterations, objective_values, marker="o", label="Objective (LCOE)")
    plt.xlabel("Iteration")
    plt.ylabel("Objective Value (Total Cable Length (m))")
    plt.title("Convergence Plot")
    plt.legend()
    plt.grid()
    plt.show()
Driver debug print for iter coord: rank0:ScipyOptimize_SLSQP|0
--------------------------------------------------------------
Design Vars
{'angle_orientation': array([0.]),
 'angle_skew': array([0.]),
 'spacing_primary': array([7.]),
 'spacing_secondary': array([7.])}
UserWarning: /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/floris/core/flow_field.py:169
'where' used without 'out', expect unitialized memory in output. If this is intentional, use out=None.
Objectives
{'collection.total_length_cables': array([47761.10752126])}
Driver debug print for iter coord: rank0:ScipyOptimize_SLSQP|1
--------------------------------------------------------------
Design Vars
{'angle_orientation': array([0.]),
 'angle_skew': array([0.]),
 'spacing_primary': array([7.]),
 'spacing_secondary': array([7.])}
UserWarning: /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/floris/core/flow_field.py:169
'where' used without 'out', expect unitialized memory in output. If this is intentional, use out=None.
Objectives
{'collection.total_length_cables': array([47761.10752126])}
Driver debug print for iter coord: rank0:ScipyOptimize_SLSQP|2
--------------------------------------------------------------
Design Vars
{'angle_orientation': array([-0.001023]),
 'angle_skew': array([-0.00180114]),
 'spacing_primary': array([6.95385903]),
 'spacing_secondary': array([6.96095497])}
Objectives
{'collection.total_length_cables': array([47468.82258446])}
Driver debug print for iter coord: rank0:ScipyOptimize_SLSQP|3
--------------------------------------------------------------
Design Vars
{'angle_orientation': array([0.00071777]),
 'angle_skew': array([-0.00564867]),
 'spacing_primary': array([6.72316838]),
 'spacing_secondary': array([6.76574159])}
Objectives
{'collection.total_length_cables': array([46007.5176839])}
Driver debug print for iter coord: rank0:ScipyOptimize_SLSQP|4
--------------------------------------------------------------
Design Vars
{'angle_orientation': array([0.09933031]),
 'angle_skew': array([0.3420111]),
 'spacing_primary': array([5.55158778]),
 'spacing_secondary': array([5.77433395])}
Objectives
{'collection.total_length_cables': array([38587.62876312])}
Driver debug print for iter coord: rank0:ScipyOptimize_SLSQP|5
--------------------------------------------------------------
Design Vars
{'angle_orientation': array([0.31671648]),
 'angle_skew': array([1.09348656]),
 'spacing_primary': array([3.00046597]),
 'spacing_secondary': array([3.57794263])}
Objectives
{'collection.total_length_cables': array([21494.11934935])}
Driver debug print for iter coord: rank0:ScipyOptimize_SLSQP|6
--------------------------------------------------------------
Design Vars
{'angle_orientation': array([0.31927714]),
 'angle_skew': array([1.09436955]),
 'spacing_primary': array([3.00000021]),
 'spacing_secondary': array([3.54204177])}
Objectives
{'collection.total_length_cables': array([21430.65803982])}
Driver debug print for iter coord: rank0:ScipyOptimize_SLSQP|7
--------------------------------------------------------------
Design Vars
{'angle_orientation': array([0.31927716]),
 'angle_skew': array([1.09436951]),
 'spacing_primary': array([3.]),
 'spacing_secondary': array([3.36469511])}
Objectives
{'collection.total_length_cables': array([21128.9645046])}
Driver debug print for iter coord: rank0:ScipyOptimize_SLSQP|8
--------------------------------------------------------------
Design Vars
{'angle_orientation': array([0.22226742]),
 'angle_skew': array([1.19865073]),
 'spacing_primary': array([3.00001296]),
 'spacing_secondary': array([3.])}
Objectives
{'collection.total_length_cables': array([20509.22319355])}
Driver debug print for iter coord: rank0:ScipyOptimize_SLSQP|9
--------------------------------------------------------------
Design Vars
{'angle_orientation': array([0.22419231]),
 'angle_skew': array([1.19633765]),
 'spacing_primary': array([3.00000012]),
 'spacing_secondary': array([3.])}
Objectives
{'collection.total_length_cables': array([20509.14935112])}
Driver debug print for iter coord: rank0:ScipyOptimize_SLSQP|10
---------------------------------------------------------------
Design Vars
{'angle_orientation': array([0.24655967]),
 'angle_skew': array([1.17808456]),
 'spacing_primary': array([3.00000125]),
 'spacing_secondary': array([3.])}
Objectives
{'collection.total_length_cables': array([20509.07944438])}
Driver debug print for iter coord: rank0:ScipyOptimize_SLSQP|11
---------------------------------------------------------------
Design Vars
{'angle_orientation': array([0.24655965]),
 'angle_skew': array([1.17808452]),
 'spacing_primary': array([3.]),
 'spacing_secondary': array([3.])}
Objectives
{'collection.total_length_cables': array([20509.07305338])}
Iteration limit reached    (Exit mode 9)
            Current function value: 1.0254536526687552
            Iterations: 10
            Function evaluations: 11
            Gradient evaluations: 11
Optimization FAILED.
Iteration limit reached
-----------------------------------
RESULTS (opt):

{'AEP_val': 2075.351527832994,
 'BOS_val': 1253.3568199762658,
 'CapEx_val': 768.4437570425,
 'LCOE_val': 102.21643920628281,
 'OpEx_val': 60.50000000000001,
 'area_tight': 11.614464000071358,
 'coll_length': 20.5090730533751,
 'turbine_spacing': 0.8520000000052291}
../../_images/0346d6e1283df7364894fe6fc52e72bbd27cdd16005f4d6909cf40b4c104a153.png
plot_layout(
    prob,
    input_dict=input_dict,
    show_image=True,
    include_cable_routing=True,
)
plt.show()
../../_images/1501e396015227b4cbc0b5fa51a18133251c0d9e2f9cfa61081a556a10446b98.png