Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 182 additions & 0 deletions ard/farm_aero/flowers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import os

import numpy as np
from flowers import FlowersModel

import ard.utils
import ard.farm_aero.templates as templates


class FLOWERSFarmComponent:
"""
Secondary-inherit component for managing FLOWERS for farm simulations.

This is a base class for farm aerodynamics simulations using FLOWERS, which
should cover all the necessary configuration, reproducibility config file
saving, and output directory management.

It is not a child class of an OpenMDAO components, but it is designed to
mirror the form of the OM component, so that FLOWERS activities are separated
to have run times that correspond to the similarly-named OM component
methods. It is intended to be a second-inherit base class for FLOWERS-based
OpenMDAO components, and will not work unless the calling object is a
specialized class that _also_ specializes `openmdao.api.Component`.

Options
-------
case_title : str
a "title" for the case, used to disambiguate runs in practice
"""

def initialize(self):
"""Initialization-time FLOWERS management."""
self.options.declare("case_title")

def setup(self):
"""Setup-time FLOWERS management."""

# set up FLOWERS
self.flowers_model = FlowersModel("defaults")
self.flowers_model.set(
turbine_type=[
ard.utils.create_FLORIS_turbine(self.modeling_options["turbine"])
],
)

self.case_title = self.options["case_title"]
self.dir_floris = os.path.join("case_files", self.case_title, "floris_inputs")
os.makedirs(self.dir_floris, exist_ok=True)

def compute(self, inputs):
"""
Compute-time FLORIS management.

Compute-time FLORIS management should be specialized based on use case.
If the base class is not specialized, an error will be raised.
"""

raise NotImplementedError("compute must be specialized,")

def setup_partials(self):
"""Derivative setup for OM component."""
# for FLORIS, no derivatives. use FD because FLORIS is cheap
self.declare_partials("farm_AEP", ["x_turbines", ], method="fd")

def get_AEP_farm(self):
"""Get the AEP of a FLORIS farm."""
return self.flowers_model.calculate_aep()

def get_power_farm(self):
"""Get the farm power of a FLOWERS farm at each wind condition."""
raise NotImplementedError("Not implemented for the FLOWERS model.")

def get_power_turbines(self):
"""Get the turbine powers of a FLOWERS farm at each wind condition."""
raise NotImplementedError("Not implemented for the FLOWERS model.")

def get_thrust_turbines(self):
"""Get the turbine thrusts of a FLOWERS farm at each wind condition."""
raise NotImplementedError("Not implemented for the FLOWERS model.")

# def dump_floris_yamlfile(self, dir_output=None):
# """
# Export the current FLORIS inputs to a YAML file file for reproducibility of the analysis.
# The file will be saved in the `dir_output` directory, or in the current working directory
# if `dir_output` is None.
# """
# if dir_output is None:
# dir_output = self.dir_floris
# self.fmodel.core.to_file(os.path.join(dir_output, "batch.yaml"))


class FLOWERSAEP(templates.FarmAEPTemplate, FLOWERSFarmComponent):
"""
Component class for computing an AEP analysis using FLORIS.

A component class that evaluates a series of farm power and associated
quantities using FLORIS with a wind rose to make an AEP estimate. Inherits
the interface from `templates.FarmAEPTemplate` and the computational guts
from `FLORISFarmComponent`.

Options
-------
case_title : str
a "title" for the case, used to disambiguate runs in practice (inherited
from `FLORISFarmComponent`)
modeling_options : dict
a modeling options dictionary (inherited via
`templates.FarmAEPTemplate`)
wind_query : floris.wind_data.WindRose
a WindQuery objects that specifies the wind conditions that are to be
computed (inherited from `templates.FarmAEPTemplate`)

Inputs
------
x_turbines : np.ndarray
a 1D numpy array indicating the x-dimension locations of the turbines,
with length `N_turbines` (inherited via `templates.FarmAEPTemplate`)
y_turbines : np.ndarray
a 1D numpy array indicating the y-dimension locations of the turbines,
with length `N_turbines` (inherited via `templates.FarmAEPTemplate`)
yaw_turbines : np.ndarray
a numpy array indicating the yaw angle to drive each turbine to with
respect to the ambient wind direction, with length `N_turbines`
(inherited via `templates.FarmAEPTemplate`)

Outputs
-------
AEP_farm : float
the AEP of the farm given by the analysis (inherited from
`templates.FarmAEPTemplate`)
power_farm : np.ndarray
an array of the farm power for each of the wind conditions that have
been queried (inherited from `templates.FarmAEPTemplate`)
power_turbines : np.ndarray
an array of the farm power for each of the turbines in the farm across
all of the conditions that have been queried on the wind rose
(`N_turbines`, `N_wind_conditions`) (inherited from
`templates.FarmAEPTemplate`)
thrust_turbines : np.ndarray
an array of the wind turbine thrust for each of the turbines in the farm
across all of the conditions that have been queried on the wind rose
(`N_turbines`, `N_wind_conditions`) (inherited from
`templates.FarmAEPTemplate`)
"""

def initialize(self):
super().initialize() # run super class script first!
FLOWERSFarmComponent.initialize(self) # add on FLORIS superclass

def setup(self):
super().setup() # run super class script first!
FLOWERSFarmComponent.setup(self) # setup a FLORIS run

def setup_partials(self):
super().setup_partials()

def compute(self, inputs, outputs):

# set up and run the floris model
self.flowers_model.set(
layout_x=inputs["x_turbines"],
layout_y=inputs["y_turbines"],
wind_data=self.wind_data,
)

self.flowers_model.run()

# dump the yaml to re-run this case on demand
# FLOWERSFarmComponent.dump_flowers_yamlfile(self, self.dir_flowers)

# FLORIS computes the powers
outputs["AEP_farm"] = FLOWERSFarmComponent.get_AEP_farm(self)
# outputs["power_farm"] = FLOWERSFarmComponent.get_power_farm(self)
# outputs["power_turbines"] = FLOWERSFarmComponent.get_power_turbines(self)
# outputs["thrust_turbines"] = FLOWERSFarmComponent.get_thrust_turbines(self)

# def compute_partials(self, inputs, partials):

# partials # modify in place

def setup_partials(self):
FLOWERSFarmComponent.setup_partials(self)
104 changes: 104 additions & 0 deletions test/system/AEP_stack/test_AEP_stack.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
from pathlib import Path

import numpy as np

import floris
import openmdao.api as om

from wisdem.optimization_drivers.nlopt_driver import NLoptDriver

import ard
import ard.test_utils
import ard.utils
import ard.wind_query as wq
import ard.glue.prototype as glue
import ard.cost.wisdem_wrap as cost_wisdem


class TestAEPstack:

def setup_method(self):

# create the wind query
wind_rose_wrg = floris.wind_data.WindRoseWRG(
Path(
Path(ard.__file__).parent,
"..",
"examples",
"data",
"wrg_example.wrg",
)
)
wind_rose_wrg.set_wd_step(90.0)
wind_rose_wrg.set_wind_speeds(np.array([5.0, 10.0, 15.0, 20.0]))
wind_rose = wind_rose_wrg.get_wind_rose_at_point(0.0, 0.0)
wind_query = wq.WindQuery.from_FLORIS_WindData(wind_rose)

# specify the configuration/specification files to use
filename_turbine_spec = Path(
Path(ard.__file__).parent,
"..",
"examples",
"data",
"turbine_spec_IEA-3p4-130-RWT.yaml",
) # toolset generalized turbine specification
data_turbine_spec = ard.utils.load_turbine_spec(filename_turbine_spec)

# set up the modeling options
self.modeling_options = {
"farm": {"N_turbines": 25},
"turbine": data_turbine_spec,
}
print(self.modeling_options)
lkj

# create the OM problem
self.prob = glue.create_setup_OM_problem(
modeling_options=self.modeling_options,
wind_rose=wind_rose,
aero_backend='FLOWERS',
)

def test_model(self):

# setup the latent variables for LandBOSSE and FinanceSE
cost_wisdem.LandBOSSE_setup_latents(self.prob, self.modeling_options)
cost_wisdem.FinanceSE_setup_latents(self.prob, self.modeling_options)

# set up the working/design variables
self.prob.set_val("spacing_primary", 7.0)
self.prob.set_val("spacing_secondary", 7.0)
self.prob.set_val("angle_orientation", 0.0)
self.prob.set_val("angle_skew", 0.0)

# run the model
self.prob.run_model()

# collapse the test result data
test_data = {
"AEP_val": float(self.prob.get_val("AEP_farm", units="GW*h")[0]),
"CapEx_val": float(self.prob.get_val("tcc.tcc", units="MUSD")[0]),
"BOS_val": float(
self.prob.get_val("landbosse.total_capex", units="MUSD")[0]
),
"OpEx_val": float(self.prob.get_val("opex.opex", units="MUSD/yr")[0]),
"LCOE_val": float(self.prob.get_val("financese.lcoe", units="USD/MW/h")[0]),
}

# check the data against a pyrite file
ard.test_utils.pyrite_validator(
test_data,
Path(
Path(ard.__file__).parent,
"..",
"test",
"system",
"LCOE_stack",
"test_LCOE_stack_pyrite.npz",
),
# rewrite=True, # uncomment to write new pyrite file
rtol_val=5e-3,
)


#
Loading