import importlib
import openmdao.api as om
from openmdao.drivers.doe_driver import DOEGenerator
from ard.utils.io import load_yaml, replace_key_value
from ard.cost.wisdem_wrap import (
LandBOSSE_setup_latents,
ORBIT_setup_latents,
FinanceSE_setup_latents,
)
import windIO
from ard import ASSET_DIR
from typing import Union
[docs]
def set_up_ard_model(input_dict: Union[str, dict], root_data_path: str = None):
"""
Set up an OpenMDAO model for Ard based on the provided input dictionary or YAML file.
This function initializes and configures an OpenMDAO problem using a system
specification, modeling options, and analysis options. It supports default
system configurations (e.g., "onshore", "offshore_floating") and allows for
recursive setup of subsystems and connections.
Parameters
----------
input_dict : Union[str, dict]
A dictionary or a path to a YAML file containing the configuration for the Ard model.
The dictionary or YAML file must include:
- "system" : str or dict
The name of the default system to use (e.g., "onshore") or a custom system specification.
- "modeling_options" : dict
A dictionary defining the modeling options for the system (e.g., turbine specs, farm layout).
- "analysis_options" : dict
A dictionary defining the analysis options, including driver settings, design variables,
constraints, objectives, and recorder configuration.
root_data_path : str, optional
The root path for resolving relative paths in the system configuration. Defaults to None.
Returns
-------
om.Problem
An OpenMDAO problem instance with the defined system hierarchy, modeling options,
and analysis options.
Raises
------
ValueError
If an invalid default system is specified or if required keys are missing in the input dictionary.
Notes
-----
- The function uses `set_up_system_recursive` to recursively build the system hierarchy.
- Latent variables for LandBOSSE, ORBIT, and FinanceSE are automatically set up if their
respective components are present in the model.
"""
# load dictionary if string is given
if isinstance(input_dict, str):
input_dict, root_data_path = load_yaml(input_dict, return_path=True)
# load default system if requested and available
available_default_systems = [
"onshore",
"onshore_batch",
"onshore_no_cable_design",
"offshore_monopile",
"offshore_monopile_no_cable_design",
"offshore_floating",
"offshore_floating_no_cable_design",
]
if isinstance(input_dict["system"], str):
if input_dict["system"] in available_default_systems:
system = load_yaml(ASSET_DIR / f"ard_system_{input_dict['system']}.yaml")
input_dict["system"] = replace_key_value(
target_dict=system,
target_key="modeling_options",
new_value=input_dict["modeling_options"],
)
else:
raise (
ValueError(
f"invalid default system '{input_dict['system']}' specified. Must be one of {available_default_systems}"
)
)
# replace empty data_path specs
input_dict["system"] = replace_key_value(
target_dict=input_dict["system"],
target_key="data_path",
new_value=root_data_path,
replace_none_only=True,
)
# validate windIO dictionary
windIO_dict = input_dict["modeling_options"]["windIO_plant"]
windIO.validate(windIO_dict, schema_type="plant/wind_energy_system")
# set up the openmdao problem
prob = set_up_system_recursive(
input_dict=input_dict["system"],
modeling_options=input_dict["modeling_options"],
analysis_options=input_dict["analysis_options"],
)
return prob
[docs]
def set_up_system_recursive(
input_dict: dict,
system_name: str = "top_level",
work_dir: str = "ard_prob_out",
parent_group=None,
modeling_options: dict = None,
analysis_options: dict = None,
_depth: int = 0,
):
"""
Recursively sets up an OpenMDAO system based on the input dictionary.
Args:
input_dict (dict): Dictionary defining the system hierarchy.
parent_group (om.Group, optional): The parent group to which subsystems are added.
Defaults to None, which initializes the top-level problem.
Returns:
om.Problem: The OpenMDAO problem with the defined system hierarchy.
"""
# Initialize the top-level problem if no parent group is provided
if parent_group is None:
prob = om.Problem(work_dir=work_dir)
parent_group = prob.model
# parent_group.name = "ard_model"
else:
prob = None
# Add subsystems directly from the input dictionary
if hasattr(parent_group, "name") and (parent_group.name != ""):
print(f"Adding {system_name} to {parent_group.name}")
else:
print(f"Adding {system_name}")
if "systems" in input_dict: # Recursively add nested subsystems]
if _depth > 0:
group = parent_group.add_subsystem(
name=system_name,
subsys=om.Group(),
promotes=input_dict.get("promotes", None),
)
else:
group = parent_group
for subsystem_key, subsystem_data in input_dict["systems"].items():
set_up_system_recursive(
subsystem_data,
parent_group=group,
system_name=subsystem_key,
modeling_options=modeling_options,
analysis_options=None,
_depth=_depth + 1,
)
if "approx_totals" in input_dict:
print(f"\tActivating approximate totals on {system_name}")
group.approx_totals(**input_dict["approx_totals"])
else:
subsystem_data = input_dict
if "object" not in subsystem_data:
raise ValueError(f"Ard subsystem '{system_name}' missing 'object' spec.")
if "promotes" not in subsystem_data:
raise ValueError(f"Ard subsystem '{system_name}' missing 'promotes' spec.")
# Dynamically import the module and get the subsystem class
Module = importlib.import_module(subsystem_data["module"])
SubSystem = getattr(Module, subsystem_data["object"])
# Convert specific promotes to tuples
promotes = [
tuple(p) if isinstance(p, list) else p for p in subsystem_data["promotes"]
]
# Add the subsystem to the parent group with kwargs
parent_group.add_subsystem(
name=system_name,
subsys=SubSystem(**subsystem_data.get("kwargs", {})),
promotes=promotes,
)
# Handle connections within the parent group
if "connections" in input_dict:
for connection in input_dict["connections"]:
src, tgt = connection # Unpack the connection as [src, tgt]
parent_group.connect(src, tgt)
# Set up the problem if this is the top-level call
if prob is not None:
if analysis_options:
# set up driver
if "driver" in analysis_options:
Driver = getattr(om, analysis_options["driver"]["name"])
# handle DOE drivers with special treatment
if Driver == om.DOEDriver:
generator = None
if "generator" in analysis_options["driver"]:
if type(analysis_options["driver"]["generator"]) == dict:
gen_dict = analysis_options["driver"]["generator"]
generator = getattr(om, gen_dict["name"])(
**gen_dict["args"]
)
elif isinstance(
analysis_options["driver"]["generator"], DOEGenerator
):
generator = analysis_options["driver"]["generator"]
else:
raise NotImplementedError(
"Only dictionary-specified or OpenMDAO "
"DOEGenerator generators have been implemented."
)
prob.driver = Driver(generator)
else:
prob.driver = Driver()
# handle the options now
if "options" in analysis_options["driver"]:
for option, value_driver_option in analysis_options["driver"][
"options"
].items():
if option == "opt_settings":
for (
key_opt_setting,
value_opt_setting,
) in value_driver_option.items():
prob.driver.opt_settings[key_opt_setting] = (
value_opt_setting
)
else:
prob.driver.options[option] = value_driver_option
# set design variables
if "design_variables" in analysis_options:
for var_name, var_data in analysis_options["design_variables"].items():
prob.model.add_design_var(var_name, **var_data)
# set constraints
if "constraints" in analysis_options:
for constraint_name, constraint_data in analysis_options[
"constraints"
].items():
prob.model.add_constraint(constraint_name, **constraint_data)
# set objective
if "objective" in analysis_options:
prob.model.add_objective(
analysis_options["objective"]["name"],
**analysis_options["objective"]["options"],
)
# Set up the recorder if specified in the input dictionary
if "recorder" in analysis_options:
recorder_filepath = analysis_options["recorder"].get("filepath")
if recorder_filepath:
recorder = om.SqliteRecorder(recorder_filepath)
prob.add_recorder(recorder)
prob.driver.add_recorder(recorder)
prob.model.set_input_defaults(
"x_turbines",
# input_dict["modeling_options"]["windIO_plant"]["wind_farm"]["layouts"]["coordinates"]["x"],
units="m",
)
prob.model.set_input_defaults(
"y_turbines",
# input_dict["modeling_options"]["windIO_plant"]["wind_farm"]["layouts"]["coordinates"]["y"],
units="m",
)
prob.setup()
return prob