From 153bea619d7ed1aea6e570e56c425a519eba45a9 Mon Sep 17 00:00:00 2001 From: Cory Frontin Date: Tue, 3 Feb 2026 12:31:46 -0700 Subject: [PATCH 1/5] switched to terse_links for collection --- ard/collection/optiwindnet_wrap.py | 15 +++++++++++---- ard/collection/templates.py | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/ard/collection/optiwindnet_wrap.py b/ard/collection/optiwindnet_wrap.py index fad464a1..0994e48b 100644 --- a/ard/collection/optiwindnet_wrap.py +++ b/ard/collection/optiwindnet_wrap.py @@ -122,6 +122,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 +137,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 +156,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 +261,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 +268,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) From d7a17f066f34b3340d77dc16282655fe5a8854f5 Mon Sep 17 00:00:00 2001 From: Cory Frontin Date: Tue, 3 Feb 2026 12:44:40 -0700 Subject: [PATCH 2/5] flip to terse links, use to connect w/ orbit wrap --- ard/collection/optiwindnet_wrap.py | 12 +++++++- ard/cost/orbit_wrap.py | 45 ++++++++++++++++++++++++++---- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/ard/collection/optiwindnet_wrap.py b/ard/collection/optiwindnet_wrap.py index 0994e48b..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"]) diff --git a/ard/cost/orbit_wrap.py b/ard/cost/orbit_wrap.py index 2267e082..ab193928 100644 --- a/ard/cost/orbit_wrap.py +++ b/ard/cost/orbit_wrap.py @@ -12,11 +12,12 @@ 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 +58,22 @@ 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_) + L = _own_L_from_inputs( + { + "x_turbines": X_turbines, + "y_turbines": Y_turbines, + "x_substations": X_substations, + "y_substations": Y_substations, + }, + { + "x_border": None, + "y_border": None, + }, + ) + 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 +226,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 +306,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 +378,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 +449,7 @@ def setup(self): "total_capex_kW", "bos_capex", "installation_capex", - "graph", + "terse_links", "x_turbines", "y_turbines", "x_substations", @@ -443,3 +462,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 + ) From e85e630164ec48dfc4c17190a96a5200050226b8 Mon Sep 17 00:00:00 2001 From: Cory Frontin Date: Tue, 3 Feb 2026 12:47:35 -0700 Subject: [PATCH 3/5] black reformat --- ard/cost/orbit_wrap.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ard/cost/orbit_wrap.py b/ard/cost/orbit_wrap.py index ab193928..424a9c61 100644 --- a/ard/cost/orbit_wrap.py +++ b/ard/cost/orbit_wrap.py @@ -16,6 +16,7 @@ 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( terse_links, X_turbines, From 6d3f22a3e91deb66b1c8ad4ebced60bf526ef724 Mon Sep 17 00:00:00 2001 From: Cory Frontin Date: Tue, 3 Feb 2026 13:32:39 -0700 Subject: [PATCH 4/5] propogate terse_links over graph into tests --- test/ard/unit/collection/test_optiwindnet.py | 2 +- test/ard/unit/collection/test_templates.py | 5 ++--- test/ard/unit/cost/test_orbit_wrap.py | 6 +++--- 3 files changed, 6 insertions(+), 7 deletions(-) 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" From 33ba07a972ac023488025e256228f93c774b96a4 Mon Sep 17 00:00:00 2001 From: Cory Frontin Date: Tue, 3 Feb 2026 17:58:55 -0700 Subject: [PATCH 5/5] remove unused code --- ard/cost/orbit_wrap.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/ard/cost/orbit_wrap.py b/ard/cost/orbit_wrap.py index 424a9c61..18349bde 100644 --- a/ard/cost/orbit_wrap.py +++ b/ard/cost/orbit_wrap.py @@ -61,18 +61,6 @@ def generate_orbit_location_from_graph( # create graph from terse links tlm = np.astype(terse_links, np.int_) - L = _own_L_from_inputs( - { - "x_turbines": X_turbines, - "y_turbines": Y_turbines, - "x_substations": X_substations, - "y_substations": Y_substations, - }, - { - "x_border": None, - "y_border": None, - }, - ) graph = _S_from_terse_links(tlm) # get all edges, sorted by the first node then the second node