Skip to content
Merged
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
14 changes: 7 additions & 7 deletions ard/layout/gridfarm.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ class GridFarmLayout(templates.LayoutTemplate):
A simplified, uniform four-parameter parallelepiped grid farm layout class.

This is a class to take a parameterized, structured grid farm defined by a
gridded parallelepiped with spacing variables defined to:
1) orient the farm with respect to North,
2) space the rows of turbines along this primary vector,
3) space the columns of turbines along the perpendicular, and
4) skew the positioning along a parallel to the primary (orientation) vector.
gridded parallelepiped with spacing variables defined to:
1) orient the farm with respect to North,
2) space the rows of turbines along this primary vector,
3) space the columns of turbines along the perpendicular, and
4) skew the positioning along a parallel to the primary (orientation) vector.

The layout model is shown in a ASCII image below:

Expand All @@ -27,12 +27,12 @@ class GridFarmLayout(templates.LayoutTemplate):
| ' / / / / / (rotated from
| ' x ----- x ----- x ----- x ----- x north CW by
| ' / / / / / orientation
| NORTH x ----- x ----- x ----- x ----- x angle)
| NORTH x ----- x ----- x ----- x ----- x angle)
| /|
| / |
| / | <- skew angle


Options
-------
modeling_options : dict
Expand Down
27 changes: 24 additions & 3 deletions ard/layout/spacing.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

class TurbineSpacing(om.ExplicitComponent):
"""
A class to return distances between turbines
A class to return distances between each pair of turbines without duplicates.

Options
-------
Expand All @@ -22,6 +22,12 @@ class TurbineSpacing(om.ExplicitComponent):
y_turbines : np.ndarray
a 1D numpy array indicating the y-dimension locations of the turbines,
with length `N_turbines` (mirrored w.r.t. `FarmAeroTemplate`)
turbine_spacing : np.ndarray
a 1D numpy array indicating the distances between turbines,
with length (N_turbines - 1)*N_turbines/2. where, for 3 turbines, turbine_spacing[0]
is the distance between turbines 0 and 1, turbine_spacing[1] is the distance between
turbines 0 and 2, and turbine_spacing[2] is the distance between turbines 1 and 2.
The array is the flattened upper-triangular portion of the distance matrix.
"""

def initialize(self):
Expand All @@ -35,7 +41,6 @@ def setup(self):
self.modeling_options = self.options["modeling_options"]
self.N_turbines = int(self.modeling_options["farm"]["N_turbines"])
self.N_distances = int((self.N_turbines - 1) * self.N_turbines / 2)
# MANAGE ADDITIONAL LATENT VARIABLES HERE!!!!!

# set up inputs and outputs for mooring system
self.add_input(
Expand Down Expand Up @@ -82,7 +87,23 @@ def compute_partials(self, inputs, partials, discrete_inputs=None):
def calculate_turbine_spacing(
x_turbines: np.ndarray,
y_turbines: np.ndarray,
):
) -> np.ndarray:
"""Calculate the spacing between every pair of turbines with no duplicates.

Args:
x_turbines (np.ndarray): a 1D numpy array indicating the x-dimension locations of the turbines,
with length `N_turbines
y_turbines (np.ndarray): a 1D numpy array indicating the y-dimension locations of the turbines,
with length `N_turbines

Returns:
turbine distances (np.ndarray): a 1D numpy array indicating the distances between turbines
with length (N_turbines - 1)*N_turbines/2. The array is the flattened
upper-triangular portion of the distance matrix. For 3 turbines, turbine_spacing[0] is the
distance between turbines 0 and 1, turbine_spacing[1] is the distance between
turbines 0 and 2, and turbine_spacing[2] is the distance between turbines 1 and 2.
"""

N_turbines = len(x_turbines)

# Create index pairs for i < j (upper triangle without diagonal)
Expand Down
51 changes: 29 additions & 22 deletions ard/offshore/mooring_constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@

class MooringConstraint(om.ExplicitComponent):
"""
A class to reduce complex mooring constraints into a simple violation lengthscale
A class to calculate the mooring line spacing distance for use in optimization
constraints. Mooring lines may be defined in 2D or 3D, but the turbine positions
are always assumed to be at sea level (z=0).

Options
-------
Expand Down Expand Up @@ -56,7 +58,6 @@ def setup(self):
)
self.N_anchors = int(self.modeling_options["platform"]["N_anchors"])
self.N_distances = int((self.N_turbines - 1) * self.N_turbines / 2)
# MANAGE ADDITIONAL LATENT VARIABLES HERE!!!!!

# set up inputs and outputs for mooring system
self.add_input(
Expand All @@ -81,14 +82,12 @@ def setup(self):
jnp.zeros((self.N_turbines, self.N_anchors)),
units="km",
) # z location of the mooring anchors in km w.r.t. reference coordinates
# ADD ADDITIONAL (DESIGN VARIABLE) INPUTS HERE!!!!!

self.add_output(
"violation_distance",
"mooring_spacing",
jnp.zeros(self.N_distances),
units="km",
) # consolidated violation length
# ADD ADDITIONAL (DESIGN VARIABLE) OUTPUTS HERE!!!!!

def setup_partials(self):
"""Derivative setup for the OpenMDAO component."""
Expand Down Expand Up @@ -117,7 +116,7 @@ def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None):
else:
raise (ValueError("modeling_options['farm'][']"))

outputs["violation_distance"] = distances
outputs["mooring_spacing"] = distances

def compute_partials(self, inputs, partials, discrete_inputs=None):

Expand All @@ -140,12 +139,12 @@ def compute_partials(self, inputs, partials, discrete_inputs=None):
else:
raise (ValueError("modeling_options['farm'][']"))

partials["violation_distance", "x_turbines"] = jacobian[0]
partials["violation_distance", "y_turbines"] = jacobian[1]
partials["violation_distance", "x_anchors"] = jacobian[2]
partials["violation_distance", "y_anchors"] = jacobian[3]
partials["mooring_spacing", "x_turbines"] = jacobian[0]
partials["mooring_spacing", "y_turbines"] = jacobian[1]
partials["mooring_spacing", "x_anchors"] = jacobian[2]
partials["mooring_spacing", "y_anchors"] = jacobian[3]
if self.N_anchor_dimensions == 3:
partials["violation_distance", "z_anchors"] = jacobian[4]
partials["mooring_spacing", "z_anchors"] = jacobian[4]


def mooring_constraint_xy(
Expand All @@ -154,7 +153,7 @@ def mooring_constraint_xy(
x_anchors: np.ndarray,
y_anchors: np.ndarray,
):
"""Mooring constraint calculation in 2 dimensions
"""Mooring distance calculation in 2 dimensions

Args:
x_turbines (np.ndarray): array of turbine x positions
Expand Down Expand Up @@ -187,8 +186,9 @@ def mooring_constraint_xyz(
y_anchors: np.ndarray,
z_anchors: np.ndarray,
):
"""Mooring constraint calculation in 3 dimensions. Third dimension is only required for the anchors since the
turbine foundations are all assumed to be at sea level.
"""Mooring distance calculation in 3 dimensions. The third dimension
is only required for the anchors since the turbine platforms are
all assumed to be at sea level.

Args:
x_turbines (np.ndarray): array of turbine x positions
Expand Down Expand Up @@ -224,7 +224,8 @@ def calc_mooring_distances(mooring_points: np.ndarray) -> np.ndarray:
"""Calculate the minimum distances between each set of mooring lines

Args:
mooring_points (np.ndarray): array of mooring points of shape (n_turbines, n_anchors+1, n_dimensions) where n_dimensions may be 2 or 3
mooring_points (np.ndarray): array of mooring points of shape
(n_turbines, n_anchors+1, n_dimensions) where n_dimensions may be 2 or 3

Returns:
np.ndarray: 1D array of distances with length (n_turbines - 1)*n_turbines/2
Expand All @@ -251,8 +252,10 @@ def convert_inputs_x_y_to_xy(
x_anchors: np.ndarray,
y_anchors: np.ndarray,
) -> np.ndarray:
"""Convert from inputs of x for turbines, y for turbines, x for anchors, and y for anchors to single array for mooring specification
that is of shape (n_turbines, n_anchors+1, 2). for each set of points, the turbine position is given first followed by the anchor positions
"""Convert from inputs of x for turbines, y for turbines, x for anchors, and y for
anchors to single array for mooring specification that is of shape
(n_turbines, n_anchors+1, 2). for each set of points, the turbine position is given
first followed by the anchor positions.

Args:
x_turbines (np.ndarray): array of turbine x positions
Expand All @@ -261,7 +264,8 @@ def convert_inputs_x_y_to_xy(
y_anchors (np.ndarray): array of anchor y positions

Returns:
np.ndarray: all input information combined into a single array of shape (n_turbines, n_anchors+1, 2)
np.ndarray: all turbine and anchor location information combined into a single
array of shape (n_turbines, n_anchors+1, 2)
"""

# Stack turbine positions and anchor positions directly
Expand All @@ -282,8 +286,10 @@ def convert_inputs_x_y_z_to_xyz(
y_anchors: np.ndarray,
z_anchors: np.ndarray,
) -> np.ndarray:
"""Convert from inputs of x for turbines, y for turbines, z for turbines, x for anchors, y for anchors, and z for anchors to single array for mooring specification
that is of shape (n_turbines, n_anchors+1, 3). for each set of points, the turbine position is given first followed by the anchor positions
"""Convert from inputs of x for turbines, y for turbines, z for turbines, x for anchors,
y for anchors, and z for anchors to single array for mooring specification that is of
shape (n_turbines, n_anchors+1, 3). for each set of points, the turbine position is given
first followed by the anchor positions.

Args:
x_turbines (np.ndarray): array of turbine x positions
Expand Down Expand Up @@ -338,8 +344,9 @@ def distance_point_to_mooring(point: np.ndarray, P_mooring: np.ndarray) -> float
def distance_mooring_to_mooring(
P_mooring_A: np.ndarray, P_mooring_B: np.ndarray
) -> float:
"""Calculate the distance from one mooring to another. Moorings are defined with center point first
followed by anchor points in no specific order.
"""Calculate the distance from one set of mooring lines to another. Moorings
are defined with the center point (platform location) first, followed by the
anchor points in no specific order.

Args:
P_mooring_A (np.ndarray): ndarray of points of mooring A of shape (npoints, nd) (e.g. (4, (x, y, z))).
Expand Down
62 changes: 43 additions & 19 deletions ard/utils/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ def get_nearest_polygons(
tol=1e-6,
):
"""
Determines the nearest polygon for each point using the ray-casting algorithm.
This implementation is based on FLOWFarm.jl (https://github.com/byuflowlab/FLOWFarm.jl)
Determines the nearest polygon for each point using the ray-casting algorithm. This
function may be used to assign turbines to regions in a wind farm layout, but is not
intended for use in a gradient-based optimization context. The function is not
differentiable. This implementation is based on FLOWFarm.jl
(https://github.com/byuflowlab/FLOWFarm.jl)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we have a positive obligation to acknowledge this in more depth? i suppose no as long as there was no copy/paste and we are "paraphrasing" the concept.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. There was copy paste, but then editing to the point of my considering removing the acknowledgment. When I asked Garrett about a similar case where we reused some WISDEM code his take was that reuse is the point of open source.


Args:
boundary_vertices (np.ndarray or list of np.ndarray): Vertices of the boundary in
Expand Down Expand Up @@ -77,14 +80,14 @@ def distance_multi_point_to_multi_polygon_ray_casting(
) -> np.ndarray:
"""
Calculate the distance from each point to the nearest point on a polygon or set of polygons using
the ray-casting algorithm. Negative means the turbine is inside at least one polygon.
This implementation is based on FLOWFarm.jl (https://github.com/byuflowlab/FLOWFarm.jl)
the ray-casting (Jordan curve theorem) algorithm. Negative means the turbine is inside at least
one polygon. This implementation is based on FLOWFarm.jl (https://github.com/byuflowlab/FLOWFarm.jl)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. There was copy paste, but then editing to the point of my considering removing the acknowledgment. When I asked Garrett about a similar case where we reused some WISDEM code his take was that reuse is the point of open source.


Args:
points_x (np.ndarray[list]): points x coordinates.
points_y (np.ndarray[list]): points y coordinates.
boundary_vertices (list[list[np.ndarray]]): Vertices of the boundary in
counterclockwise order.
boundary_vertices (list[list[np.ndarray]]): Vertices of the each boundary in
counterclockwise order. Boundaries should be simple polygons but do not need to have the same number of vertices.
regions (np.array[int]): Predefined region assignments for each point. Defaults to None.
s (float, optional): Smoothing factor for smooth max. Defaults to 700.
tol (float, optional): Tolerance for determining proximity of point to polygon to be considered inside the polygon. Defaults to 1e-6.
Expand Down Expand Up @@ -140,18 +143,26 @@ def distance_point_to_polygon_ray_casting(
return_distance: bool = True,
):
"""
Determine the signed distance from a point to a polygon in a differentiable way.
Determines the signed distance from a point to a polygon using the Jordan curve
theorem (ray-casting) approach as discussed in [1] and [2]. The polygon is
assumed to be simple and defined in counterclockwise order. Complex polygons
(where edges cross one another) are not supported. The function is
differentiable with respect to the point coordinates.

[1] Numerical Recipes: The Art of Scientific Computing by Press, et al. 3rd edition, sec. 21.4.3 (p. 1124)
[2] https://en.wikipedia.org/wiki/Point_in_polygon

Args:
point (jnp.ndarray): Point of interest (2D vector).
vertices (jnp.ndarray): Vertices of the polygon (Nx2 array).
vertices (jnp.ndarray): Vertices of the polygon (Nx2 array) in counterclockwise order.
s (float, optional): Smoothing factor for the smoothmin function. Defaults to 700.
shift (float, optional): Small shift to handle edge cases. Defaults to 1e-10.
return_distance (bool, optional): Whether to return the signed distance or just
inside/outside status. Defaults to True.
inside/outside status. Defaults to True. When False, the function is not
differentiable.

Returns:
float: Signed distance or inside/outside status.
float: Signed distance or inside/outside status. Negative if inside, positive if outside.
"""
# Ensure inputs are JAX arrays with explicit data types
point = jnp.asarray(point, dtype=jnp.float32)
Expand Down Expand Up @@ -181,11 +192,11 @@ def process_edge(edge_start, edge_end, point):
return is_below, distance

# Vectorize the edge processing function
process_edge_vec = jax.vmap(process_edge, in_axes=(0, 0, None))

edge_starts = vertices[:-1]
edge_ends = vertices[1:]
is_below, distances = jax.vmap(process_edge, in_axes=(0, 0, None))(
edge_starts, edge_ends, point
)
is_below, distances = process_edge_vec(edge_starts, edge_ends, point)

# Count the number of intersections
intersection_counter = jnp.sum(is_below)
Expand Down Expand Up @@ -219,7 +230,8 @@ def polygon_normals_calculator(

Args:
boundary_vertices (list of np.ndarray): List of m-by-2 arrays, where each array contains
the boundary vertices of a polygon in counterclockwise order.
the boundary vertices of a polygon in counter-clockwise order. The number of vertices (m)
in each polygon does not need to be the same.
nboundaries (int, optional): The number of boundaries in the set. Defaults to 1.

Returns:
Expand Down Expand Up @@ -320,7 +332,9 @@ def _distance_lineseg_to_lineseg_coplanar(
line_b_end: np.ndarray,
) -> float:
"""Returns the distance between two finite line segments assuming the segments are coplanar.
It is up to the user to check the required condition.
It is up to the user to check the required condition. There may be some error in the case
when the line segments are parallel since multiple points may have equal distances, leading to
some error from the smooth minimum function.

Args:
line_a_start (np.ndarray): start point of line a
Expand Down Expand Up @@ -355,9 +369,14 @@ def distance_lineseg_to_lineseg_nd(
line_b_end: np.ndarray,
tol=1e-12,
) -> float:
"""Find the distance between two line segments in 2d or 3d. This method is primarily based on reference [1].
"""Find the distance between two line segments in 2d or 3d. This method is primarily based on reference [1],
using a parametric approach based on the determinant and cross product to find the closest points on the two line
segments. However, to handle the special case of line segments that are coplanar, we use the smooth minimum of the
distance between the endpoints of the two line segments and the other line segment. In the coplanar case, the
returned distance between the two line segments may have a noticeable error due to possibly having multiple points
with the same distance, which leads to error in the smooth minimum function.

[1] Numerical Recipes: The Art of Scientific Computing by Press, et al. 3rd edition
[1] Numerical Recipes: The Art of Scientific Computing by Press, et al. 3rd edition, sec. 21.4.2 (p. 1121)

Args:
line_a_start (np.ndarray): The start point of line segment "a" as either [x,y,z] or [x,y]
Expand Down Expand Up @@ -532,7 +551,11 @@ def st_lt_1(inputs23i) -> np.ndarray:
def distance_point_to_lineseg_nd(
point: np.ndarray, segment_start: np.ndarray, segment_end: np.ndarray
) -> float:
"""Find the distance from a point to a finite line segment in N-Dimensions
"""Find the distance from a point to a line segment in N-Dimensions. This
implementation can handle any number of dimensions as well as the reduced case
of point to point distance. If the same point is passed for the start and end
of the line segment, then the distance is simply the distance from the point
of interest to the single start/end point.

Args:
point (np.ndarray): point of interest [x,y,...]
Expand Down Expand Up @@ -585,7 +608,8 @@ def get_closest_point_on_line_seg(
segment_end: np.ndarray,
segment_vector: np.ndarray,
) -> np.ndarray:
"""Get the closest point on the line segment to the point of interest in N-Dimensions
"""Get the closest point on a line segment to the point of interest in N-Dimensions
using vector projection.

Args:
point (np.ndarray): point of interest [x,y,...]
Expand Down
Loading