import numpy
import dolfin
import dolfin_adjoint
from ..optimisation_helpers import MinimumDistanceConstraints
from ..optimisation_helpers import MinimumDistanceConstraintsLargeArrays
from ..turbine_cache import TurbineCache
[docs]
class BaseFarm(object):
"""A base Farm class from which other Farm classes should be derived."""
def __init__(self, domain=None, turbine=None, site_ids=None,
n_time_steps=None):
"""Create an empty Farm."""
# Create a chaching object for the interpolated turbine friction fields
# (as their computation is very expensive)
# n_time_steps is only used with dynamic friction.
self.turbine_cache = TurbineCache()
self._parameters = {"friction": [], "position": []}
self._dynamic_friction_t_0 = []
self.n_time_steps = n_time_steps
if (turbine.controls.dynamic_friction and n_time_steps == None):
raise ValueError("n_time_steps need to be set when dynamic "\
"friction is used. (Use problem_parameters.n_time_steps to"\
" get the number of time steps.).")
self.domain = domain
self._set_turbine_specification(turbine)
# The measure of the farm site
self.site_dx = self.domain.dx(site_ids)
def update(self):
self.turbine_cache.update(self)
@property
def friction_function(self):
self.update()
return self.turbine_cache["turbine_field"]
def _get_turbine_specification(self):
if self._turbine_specification is None:
raise ValueError("The turbine specification has not yet been set.")
return self._turbine_specification
def _set_turbine_specification(self, turbine_specification):
self._turbine_specification = turbine_specification
self.turbine_cache.set_turbine_specification(
self._turbine_specification)
turbine_specification = property(_get_turbine_specification,
_set_turbine_specification,
"The turbine specification.")
@property
def number_of_turbines(self):
"""The number of turbines in the farm.
:returns: The number of turbines in the farm.
:rtype: int
"""
return len(self._parameters["position"])
@property
def control_array_global(self):
"""A serialized representation of the farm based on the controls.
In contrast to the control_array property, this property returns the
global array of all CPUs. In serial, control_array and
control_array_global are equivalent.
:returns: A serialized representation of the farm based on the controls.
:rtype: numpy.ndarray
"""
if self._turbine_specification.smeared:
return dolfin_adjoint.optimization.get_global(self.friction_function)
else:
return self.control_array
@property
def control_array(self):
"""A serialized representation of the farm based on the controls.
:returns: A serialized representation of the farm based on the controls.
:rtype: numpy.ndarray
"""
if self._turbine_specification.smeared:
return self.friction_function.vector().array()
else:
m = []
if (self._turbine_specification.controls.friction or
self._turbine_specification.controls.dynamic_friction):
m += numpy.reshape(
self._parameters["friction"], -1).tolist()
if self._turbine_specification.controls.position:
m += numpy.reshape(
self._parameters["position"], -1).tolist()
return numpy.asarray(m)
@property
def turbine_positions(self):
"""The positions of turbines within the farm.
:returns: The positions of turbines within the farm.
:rtype: :func:`list`
"""
return self._parameters["position"]
@property
def turbine_frictions(self):
"""The friction coefficients of turbines within the farm.
:returns: The friction coefficients of turbines within the farm.
:rtype: :func:`list`
"""
return self._parameters["friction"]
[docs]
def add_turbine(self, coordinates):
"""Add a turbine to the farm at the given coordinates.
Creates a new turbine of the same specification as the prototype turbine
and places it at coordinates.
:param coordinates: The x-y coordinates where the turbine should be placed.
:type coordinates: :func:`list`
"""
if self._turbine_specification is None:
raise ValueError("A turbine specification has not been set.")
turbine = self._turbine_specification
self._parameters["position"].append(coordinates)
if (turbine.controls.dynamic_friction):
self._dynamic_friction_t_0.append(turbine.friction)
self._parameters["friction"] = [self._dynamic_friction_t_0] *\
(self.n_time_steps + 1)
else:
self._parameters["friction"].append(turbine.friction)
dolfin.info("Turbine added at (%.2f, %.2f)." % (coordinates[0],
coordinates[1]))
def _staggered_turbine_layout(self, num_x, num_y, site_x_start, site_x_end,
site_y_start, site_y_end):
"""Adds a staggered, rectangular turbine layout to the farm.
A rectangular turbine layout with turbines evenly spread out in each
direction across the domain.
:param turbine: Defines the type of turbine to add to the farm.
:type turbine: Turbine object.
:param num_x: The number of turbines placed in the x-direction.
:type num_x: int
:param num_y: The number of turbines placed in the y-direction (will be one less in each second row).
:type num_y: int
:param site_x_start: The minimum x-coordinate of the site.
:type site_x_start: float
:param site_x_end: The maximum x-coordinate of the site.
:type site_x_end: float
:param site_y_start: The minimum y-coordinate of the site.
:type site_y_start: float
:param site_y_end: The maximum y-coordinate of the site.
:type site_y_end: float
:raises: ValueError
"""
if self._turbine_specification is None:
raise ValueError("A turbine specification has not been set.")
turbine = self._turbine_specification
# Generate the start and end points in the desired layout.
start_x = site_x_start + turbine.radius
start_y = site_y_start + turbine.radius
end_x = site_x_end - turbine.radius
end_y = site_y_end - turbine.radius
# Check that we can fit enough turbines in each direction.
too_many_x = turbine.diameter*num_x > site_x_end - site_x_start
too_many_y = turbine.diameter*num_y > site_y_end - site_y_start
# Raise exceptions if too many turbines are placed in a certain
# direction.
if too_many_x and too_many_y:
raise ValueError("Too many turbines in the x and y direction")
elif too_many_x:
raise ValueError("Too many turbines in the x direction")
elif too_many_y:
raise ValueError("Too many turbines in the y direction")
# Iterate over the x and y positions and append them to the turbine
# list.
for i, x in enumerate(numpy.linspace(start_x, end_x, num_x)):
if i % 2 == 0:
for y in numpy.linspace(start_y, end_y, num_y):
self.add_turbine((x,y))
else:
ys = numpy.linspace(start_y, end_y, num_y)
for i in range(len(ys)-1):
self.add_turbine((x, ys[i] + 0.5*(ys[i+1]-ys[i])))
dolfin.info("Added %i turbines to the site in an %ix%i rectangular "
"array." % (num_x*num_y, num_x, num_y))
def _regular_turbine_layout(self, num_x, num_y, site_x_start, site_x_end,
site_y_start, site_y_end):
"""Adds a rectangular turbine layout to the farm.
A rectangular turbine layout with turbines evenly spread out in each
direction across the domain.
:param turbine: Defines the type of turbine to add to the farm.
:type turbine: Turbine object.
:param num_x: The number of turbines placed in the x-direction.
:type num_x: int
:param num_y: The number of turbines placed in the y-direction.
:type num_y: int
:param site_x_start: The minimum x-coordinate of the site.
:type site_x_start: float
:param site_x_end: The maximum x-coordinate of the site.
:type site_x_end: float
:param site_y_start: The minimum y-coordinate of the site.
:type site_y_start: float
:param site_y_end: The maximum y-coordinate of the site.
:type site_y_end: float
:raises: ValueError
"""
if self._turbine_specification is None:
raise ValueError("A turbine specification has not been set.")
turbine = self._turbine_specification
# Generate the start and end points in the desired layout.
start_x = site_x_start + turbine.radius
start_y = site_y_start + turbine.radius
end_x = site_x_end - turbine.radius
end_y = site_y_end - turbine.radius
# Check that we can fit enough turbines in each direction.
too_many_x = turbine.diameter*num_x > site_x_end - site_x_start
too_many_y = turbine.diameter*num_y > site_y_end - site_y_start
# Raise exceptions if too many turbines are placed in a certain
# direction.
if too_many_x and too_many_y:
raise ValueError("Too many turbines in the x and y direction")
elif too_many_x:
raise ValueError("Too many turbines in the x direction")
elif too_many_y:
raise ValueError("Too many turbines in the y direction")
# Iterate over the x and y positions and append them to the turbine
# list.
for x in numpy.linspace(start_x, end_x, num_x):
for y in numpy.linspace(start_y, end_y, num_y):
self.add_turbine((x,y))
dolfin.info("Added %i turbines to the site in an %ix%i rectangular "
"array." % (num_x*num_y, num_x, num_y))
def _lhs_turbine_layout(self, number_turbines, site_x_start, site_x_end,
site_y_start, site_y_end):
"""Adds to the farm a turbine layout based on latin hypercube sampling.
:param turbine: Defines the type of turbine to add to the farm.
:type turbine: Turbine object.
:param number_turbines: The number of turbines to be placed.
:type number_turbines: int
:param num_y: The number of turbines placed in the y-direction.
:type num_y: int
:param site_x_start: The minimum x-coordinate of the site.
:type site_x_start: float
:param site_x_end: The maximum x-coordinate of the site.
:type site_x_end: float
:param site_y_start: The minimum y-coordinate of the site.
:type site_y_start: float
:param site_y_end: The maximum y-coordinate of the site.
:type site_y_end: float
:raises: ValueError
"""
if self._turbine_specification is None:
raise ValueError("A turbine specification has not been set.")
turbine = self._turbine_specification
# Generate the start and end points in the desired layout.
start_x = site_x_start + turbine.radius
start_y = site_y_start + turbine.radius
end_x = site_x_end - turbine.radius
end_y = site_y_end - turbine.radius
site_x = end_x - start_x
site_y = end_y - start_y
capacity = int(numpy.floor(site_x/(1.5*turbine.diameter))) \
* int(numpy.floor(site_y/(1.5*turbine.diameter)))
over_capacity = capacity < number_turbines
if over_capacity:
raise ValueError("Too many turbines")
def lhs(number_turbines):
""" Latin hypercube sampling of a unit square
"""
# generate the intervals
cut = numpy.linspace(0, 1, number_turbines+1)
# fill points uniformly
a = cut[:number_turbines]
b = cut[1:number_turbines + 1]
c = numpy.random.rand(number_turbines, 2)
random_points = numpy.zeros_like(c)
for i in [0,1]:
random_points[:, i] = c[:, i]*(b-a) + a
# make the random pairings
points = numpy.zeros_like(random_points)
for j in [0,1]:
order = numpy.random.permutation(list(range(number_turbines)))
points[:, j] = random_points[order, j]
return points
# Fetch the points
points = lhs(number_turbines)
#Scale the points
points[:,0] = (points[:,0] * site_x) + start_x
points[:,1] = (points[:,1] * site_y) + start_y
for i in range(len(points)):
self.add_turbine((points[i,0], points[i,1]))
dolfin.info("Used latin hypercube sampling to add %i turbines to the site"\
% number_turbines)
# NOTE there is no simple way of checking the starting design satisfies
# the inequality constraints (if they have been set) Generally the
# design will 'untangle' itself in the second iteration - i.e. when the
# turbine positions are updated the constraints should be fulfilled.
# However, with densely populated sites, it may be wise to use one of
# the other layout options.
dolfin.info('LHS generated starting layout may not fulfill inequality '\
'constraints if set... use caution')
[docs]
def set_turbine_positions(self, positions):
"""Sets the turbine position and an equal friction parameter.
:param list positions: List of tuples containint x-y coordinates of
turbines to be added.
"""
self.turbine_cache["position"] = positions
self.turbine_cache["friction"] = (
self._turbine_specification.friction*numpy.ones(len(positions)))
self.update()
[docs]
def site_boundary_constraints(self):
"""Raises NotImplementedError if called."""
return NotImplementedError("The Farm base class does not have "
"boundaries.")
[docs]
def minimum_distance_constraints(self, large=False):
"""Returns an instance of MinimumDistanceConstraints.
:param bool large: Use a minimum distance implementation that is
suitable for large farms (i.e. many turbines). Default: False
:returns: An instance of dolfin_adjoint.InequalityConstraint that
enforces a minimum distance between turbines.
:rtype: :py:class:`MinimumDistanceConstraints`
(if large=False) or :py:class:`MinimumDistanceConstraintsLargeArray`
(if large=True)
"""
# Check we have some turbines.
n_turbines = len(self.turbine_positions)
if (n_turbines < 1):
raise ValueError("Turbines must be deployed before minimum "
"distance constraints can be calculated.")
controls = self._turbine_specification.controls
minimum_distance = self._turbine_specification.minimum_distance
positions = self.turbine_positions
if large:
return MinimumDistanceConstraintsLargeArrays(positions, minimum_distance, controls)
else:
return MinimumDistanceConstraints(positions, minimum_distance, controls)