diff --git a/README.md b/README.md index 52e85465..3c717df5 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,7 @@ Moreover, the design of any *one* of these aspects affects all the rest! In brief, we are designing `Ard` to be: principled, modular, extensible, and effective, to allow resource-specific wind farm layout optimization with realistic, well-posed constraints, holistic and complex objectives, and natural incorporation of multiple fidelities and disciplines. ## Documentation - -Ard documentation is available at [https://wisdem.github.io/Ard](https://wisdem.github.io/Ard) +Ard documentation is available at [https://NLRWindSystems.github.io/Ard](https://NLRWindSystems.github.io/Ard) ## Installation instructions @@ -145,7 +144,7 @@ In this example, the wind farm layout is parametrized with two angles, named ori Additionally, we have offshore examples adjacent to the onshore example in the `examples` subdirectory. In the beta pre-release stage, the constituent subcomponents of these problems are known to work and have full testing coverage. -These cases start from a four parameter farm layout, compute land use area, make FLORIS estimates of annual energy production (AEP), compute turbine capital costs, balance-of-station (BOS), and operational costs elements of NREL's turbine systems engineering tool [WISDEM](https://github.com/wisdem/wisdem), and finally give summary estimates of plant finance figures. +These cases start from a four parameter farm layout, compute land use area, make FLORIS estimates of annual energy production (AEP), compute turbine capital costs, balance-of-station (BOS), and operational costs elements of NREL's turbine systems engineering tool [WISDEM](https://github.com/NLRWindSystems/wisdem), and finally give summary estimates of plant finance figures. The components that achieve this can be assembled to either run a single top-down analysis run, or run an optimization. # Contributing to `Ard` diff --git a/ard/collection/optiwindnet_wrap.py b/ard/collection/optiwindnet_wrap.py index fad464a1..fdcc6369 100644 --- a/ard/collection/optiwindnet_wrap.py +++ b/ard/collection/optiwindnet_wrap.py @@ -4,13 +4,23 @@ import numpy as np from optiwindnet.mesh import make_planar_embedding -from optiwindnet.interarraylib import L_from_site +from optiwindnet.interarraylib import L_from_site, calcload from optiwindnet.heuristics import EW_presolver from optiwindnet.MILP import OWNWarmupFailed, solver_factory, ModelOptions from . import templates +def _S_from_terse_links(terse_links, **kwargs): + T = terse_links.shape[0] + S = nx.Graph(T=T, R=abs(terse_links.min()), **kwargs) + S.add_edges_from(tuple(zip(range(T), terse_links))) + calcload(S) + if "capacity" not in kwargs: + S.graph["capacity"] = S.graph["max_load"] + return S + + def _own_L_from_inputs(inputs: dict, discrete_inputs: dict) -> nx.Graph: # get the metadata and data for the OWN warm-starter from the inputs T = len(inputs["x_turbines"]) @@ -122,6 +132,9 @@ class OptiwindnetCollection(templates.CollectionTemplate): ------- total_length_cables : float the total length of cables used in the collection system network + terse_links : np.ndarray + a 1D numpy int array encoding the electrical connections of the collection + system (tree topology), with length `N_turbines` Discrete Outputs ------- @@ -134,9 +147,6 @@ class OptiwindnetCollection(templates.CollectionTemplate): length `N_turbines` max_load_cables : int the maximum cable capacity required by the collection system - terse_links : np.ndarray - a 1D numpy int array encoding the electrical connections of the collection - system (tree topology), with length `N_turbines` """ def initialize(self): @@ -156,6 +166,13 @@ def setup_partials(self): ["x_turbines", "y_turbines", "x_substations", "y_substations"], method="exact", ) + self.declare_partials( + ["terse_links"], + ["x_turbines", "y_turbines", "x_substations", "y_substations"], + method="exact", + val=0.0, + dependent=False, + ) def compute( self, @@ -254,7 +271,6 @@ def compute( # pack and ship self.graph = G discrete_outputs["graph"] = G # TODO: remove for terse links, below! - discrete_outputs["terse_links"] = terse_links discrete_outputs["length_cables"] = length_cables discrete_outputs["load_cables"] = load_cables discrete_outputs["max_load_cables"] = S.graph["max_load"] @@ -262,6 +278,7 @@ def compute( assert ( abs(length_cables.sum() - G.size(weight="length")) < 1e-7 ), f"difference: {length_cables.sum() - G.size(weight='length')}" + outputs["terse_links"] = terse_links outputs["total_length_cables"] = length_cables.sum() def compute_partials(self, inputs, J, discrete_inputs=None): diff --git a/ard/collection/templates.py b/ard/collection/templates.py index b0a905c5..b1bbd83e 100644 --- a/ard/collection/templates.py +++ b/ard/collection/templates.py @@ -108,8 +108,8 @@ def setup(self): # set up outputs for the collection system self.add_output("total_length_cables", 0.0, units="m") + self.add_output("terse_links", np.full((self.N_turbines,), -1)) self.add_discrete_output("length_cables", np.zeros((self.N_turbines,))) - self.add_discrete_output("terse_links", np.full((self.N_turbines,), -1)) self.add_discrete_output("load_cables", np.zeros((self.N_turbines,))) self.add_discrete_output("max_load_cables", 0.0) self.add_discrete_output("graph", None) diff --git a/ard/cost/orbit_wrap.py b/ard/cost/orbit_wrap.py index 2267e082..18349bde 100644 --- a/ard/cost/orbit_wrap.py +++ b/ard/cost/orbit_wrap.py @@ -12,11 +12,13 @@ from ORBIT.core.library import default_library from ORBIT.core.library import initialize_library +from ard.collection.optiwindnet_wrap import _S_from_terse_links +from ard.collection.optiwindnet_wrap import _own_L_from_inputs from ard.cost.wisdem_wrap import ORBIT_setup_latents def generate_orbit_location_from_graph( - graph, # TODO: replace with a terse_links representation + terse_links, X_turbines, Y_turbines, X_substations, @@ -57,6 +59,10 @@ def generate_orbit_location_from_graph( if the recursive setup seems to be stuck in a loop """ + # create graph from terse links + tlm = np.astype(terse_links, np.int_) + graph = _S_from_terse_links(tlm) + # get all edges, sorted by the first node then the second node edges_to_process = [edge for edge in graph.edges] edges_to_process.sort(key=lambda x: (x[0], x[1])) @@ -209,7 +215,9 @@ def initialize(self): super().initialize() self.options.declare("case_title", default="working") - self.options.declare("modeling_options") + self.options.declare( + "modeling_options", types=dict, desc="Ard modeling options" + ) self.options.declare("approximate_branches", default=False) def setup(self): @@ -287,7 +295,7 @@ def setup(self): self.N_substations = self.modeling_options["layout"]["N_substations"] # bring in collection system design - self.add_discrete_input("graph", None) + self.add_input("terse_links", np.full((self.N_turbines,), -1)) # add the detailed turbine and substation locations self.add_input("x_turbines", np.zeros((self.N_turbines,)), units="km") @@ -359,7 +367,7 @@ def compile_orbit_config_file( # generate the csv data needed to locate the farm elements generate_orbit_location_from_graph( - discrete_inputs["graph"], + inputs["terse_links"], inputs["x_turbines"], inputs["y_turbines"], inputs["x_substations"], @@ -430,7 +438,7 @@ def setup(self): "total_capex_kW", "bos_capex", "installation_capex", - "graph", + "terse_links", "x_turbines", "y_turbines", "x_substations", @@ -443,3 +451,17 @@ def setup(self): # connect for key in variable_mapping.keys(): self.connect(key, f"orbit.{key}") + + def setup_partials(self): + + self.declare_partials( + "*", + "*", + method="fd", + step=1.0e-5, + form="central", + step_calc="rel_avg", + ) + self.declare_partials( + "terse_links", "*", method="exact", val=0.0, dependent=False + ) diff --git a/test/ard/unit/collection/test_optiwindnet.py b/test/ard/unit/collection/test_optiwindnet.py index ac9606d2..18217d92 100644 --- a/test/ard/unit/collection/test_optiwindnet.py +++ b/test/ard/unit/collection/test_optiwindnet.py @@ -131,6 +131,7 @@ def test_modeling(self, subtests): # make sure that the outputs in the component match what we planned output_list = [k for k, v in self.collection.list_outputs()] for var_to_check in [ + "terse_links", "total_length_cables", ]: assert var_to_check in output_list @@ -143,7 +144,6 @@ def test_modeling(self, subtests): "length_cables", "load_cables", "max_load_cables", - "terse_links", ]: assert var_to_check in discrete_output_list diff --git a/test/ard/unit/collection/test_templates.py b/test/ard/unit/collection/test_templates.py index 79780bea..66c11e49 100644 --- a/test/ard/unit/collection/test_templates.py +++ b/test/ard/unit/collection/test_templates.py @@ -78,6 +78,7 @@ def test_setup(self): for var_to_check in [ "length_cables", "load_cables", + "terse_links", "total_length_cables", "max_load_cables", ]: @@ -87,9 +88,7 @@ def test_setup(self): discrete_output_list = [ k for k, v in self.coll_temp._discrete_outputs.items() ] - for var_to_check in [ - "terse_links", - ]: + for var_to_check in []: assert var_to_check in discrete_output_list def test_compute(self): diff --git a/test/ard/unit/cost/test_orbit_wrap.py b/test/ard/unit/cost/test_orbit_wrap.py index 2e6e8766..d02bf05b 100644 --- a/test/ard/unit/cost/test_orbit_wrap.py +++ b/test/ard/unit/cost/test_orbit_wrap.py @@ -144,7 +144,7 @@ def test_raise_error(self): "y_substations", ], ) - model.connect("collection.graph", "orbit.graph") + model.connect("collection.terse_links", "orbit.terse_links") model.set_input_defaults( "x_turbines", modeling_options["layout"]["x_turbines"], units="km" @@ -328,7 +328,7 @@ def test_baseline_farm(self, subtests): "y_substations", ], ) - model.connect("collection.graph", "orbit.graph") + model.connect("collection.terse_links", "orbit.terse_links") model.set_input_defaults( "x_turbines", modeling_options["layout"]["x_turbines"], units="km" @@ -525,7 +525,7 @@ def setup_method(self): "y_substations", ], ) - self.model.connect("collection.graph", "orbit.graph") + self.model.connect("collection.terse_links", "orbit.terse_links") self.model.set_input_defaults( "x_turbines", self.modeling_options["layout"]["x_turbines"], units="km"