Layouts#

Turbine-placement algorithms and the fitter strategies that size them to a boundary.

oowl.sampling.Choice picks one layout for the whole farm. Mixture instead lays several layouts out in weighted cells of one farm, each in its own cell with crisp seams between them.

PoissonDisc, Sobol and Uniform are the unstructured layouts, and they differ in what they impose on the scatter. PoissonDisc enforces a minimum separation, so it is blue noise and markedly regular. Sobol is low-discrepancy, so it covers more evenly than chance. Uniform imposes nothing, so it is complete spatial randomness and pairs may fall arbitrarily close. It is unrelated to the wind direction model oowl.resources.UniformDirection.

LayoutAlgorithm([n_turbines, density, ...])

Base class for one turbine-placement method applied to one farm.

BoundaryGrid([n_turbines, density, ...])

Automatic boundary-grid layout based on Stanley and Ning (2019).

Perimeter([n_turbines, density, start, seed])

Place an exact turbine count equally along the exterior perimeter.

Delaunay([n_turbines, density, ...])

Generate a boundary-conforming unstructured triangular layout.

Grid([n_turbines, density, spacing_ratio, ...])

Occupy selected intersections of a regular parallelogram grid.

Circular([n_turbines, density, ...])

Occupy selected positions from concentric circular rings.

Contour([n_turbines, density, ...])

Place turbines along nested inward offsets of the farm boundary.

PoissonDisc([n_turbines, density, k, ...])

Place turbines by Poisson-disc sampling, a blue-noise pattern.

Sobol([n_turbines, density, seed])

Place turbines using a scrambled Sobol low-discrepancy sequence.

Uniform([n_turbines, density, seed])

Scatter turbines independently and uniformly across the boundary.

String(strings[, n_turbines, density, ...])

Distribute turbines along one or more string guides.

Sunflower([n_turbines, density, rotation, ...])

Fill a boundary with an exact-count phyllotactic pattern.

Mixture(layouts[, n_turbines, density, ...])

Combine layout methods in weighted regions of one farm.

Fitter()

Base strategy for fitting one layout family to one polygonal farm.

RigidPatternFitter(*[, max_iterations, ...])

Maximally scale and translate an exact finite point pattern.

SelectedCandidateFitter(*[, n_phases, ...])

Fit a scalable candidate field and occupy exactly N positions.

SelectedGridFitter(*[, n_phases, ...])

Select exact, well-distributed intersections from an aligned grid.

SelectedCircularFitter(*[, n_phases, ...])

Select exact positions from boundary-clipped concentric rings.

SelectedContourFitter(*[, n_phases])

Select exact positions from inward offsets of a farm boundary.

Turbine layout algorithms.

A layout places an exact turbine count or fills a boundary to a density, and the pattern families fit their pattern to the boundary with a Fitter. Layouts without a count or density are templates for oowl.sampling.Choice and Mixture.

class oowl.layouts.BoundaryGrid(n_turbines=None, *, density=None, boundary_fraction=0.45, seed=None)[source]#

Automatic boundary-grid layout based on Stanley and Ning (2019).

Up to boundary_fraction of the turbines are equally spaced around the exterior boundary. The remainder occupy a centered, rotated inner grid. Of the perimeter starts and grid rotations tried, the one with the widest closest pair is kept, and when a grid turbine crowds the ring it is refitted inside the boundary inset by the ring’s spacing or the grid’s row spacing, whichever is less, keeping the wider of the two. The paper’s initialization values dy = 4 dx and a 20 degree row offset are fixed. The seed chooses perimeter start and grid rotation. With minimum_spacing the ring and the grid are both filled at that spacing, the ring holding at most boundary_fraction of the turbines.

Parameters:
  • n_turbines (int | None)

  • density (dict[str, float] | None)

  • boundary_fraction (float)

  • seed (int | np.random.Generator | None)

class oowl.layouts.Circular(n_turbines=None, *, density=None, spacing_ratio=1.0, rotation=0.0, include_center=True, fitter=None, seed=None)[source]#

Occupy selected positions from concentric circular rings.

Rings are centered on the boundary centroid, or on an interior point when the centroid lies outside, and clipped to the farm. The fitter finds the largest radial spacing that leaves at least n_turbines candidates, then selects an exact, well-distributed subset, so rings may be incomplete. spacing_ratio is the target spacing along a ring divided by the radial spacing, rotation turns the rings counterclockwise in degrees, and include_center keeps a turbine at the center.

Parameters:
estimate_spacing(boundary)[source]#

Return a starting radial spacing in meters.

Assumes one square area / n_turbines cell per turbine. The estimate only seeds the fitter’s search and is not the final spacing.

Parameters:

boundary (Boundary)

Return type:

float

class oowl.layouts.Contour(n_turbines=None, *, density=None, spacing_ratio=1.0, include_center=True, fitter=None, seed=None)[source]#

Place turbines along nested inward offsets of the farm boundary.

spacing_ratio is the target distance along each contour divided by the distance between contour layers, and include_center adds a turbine at an interior point. Layer count and turbine allocation follow from the requested count and the available geometry. The innermost contour hugs the deepest point of the farm, so it can pass a few meters from the center or fold back on itself, and a contour turning an acute corner cuts across it. A candidate closer than 0.6 of the smaller of the two spacings to an earlier one, the center first and then the contours from the outside in, is therefore left out.

Parameters:
estimate_spacing(boundary)[source]#

Return a starting contour-layer spacing in meters.

Assumes one square area / n_turbines cell per turbine. The estimate only seeds the fitter’s search and is not the final spacing.

Parameters:

boundary (Boundary)

Return type:

float

class oowl.layouts.Delaunay(n_turbines=None, *, density=None, boundary_count=None, max_iterations=400, tolerance=0.001, relaxation=1.0, seed=None)[source]#

Generate a boundary-conforming unstructured triangular layout.

Boundary nodes start at equal arc-length intervals and slide along their ring. All nodes are connected by a Delaunay triangulation and repeatedly pushed apart by edge forces, adapted from DistMesh (Persson and Strang 2004). boundary_count=None derives the number of boundary nodes from the equilateral spacing of n_turbines triangles over the area, and a node closer than 0.8 of the interval to an earlier one, across a sharp corner or a narrow neck, becomes an interior node instead. Unlike DistMesh, an edge repels its nodes while it is shorter than 0.95 of that spacing, a fixed length rather than one scaled with the edges, and every edge of the triangulation counts, also those leaving the shape. Each iteration moves a node by relaxation times its mean edge force plus up to 0.9 of its last move, a share that grows with the iterations since the forces last turned against the moves and starts again from none then. Iteration stops once no node moves more than tolerance times that spacing, over 600 offshore and other farms within 156 iterations for 99 in 100 and 188 at most, so max_iterations defaults to 400 with room to spare. The nodes are triangulated again once one has moved more than 0.2 of that spacing since the last triangulation. On a multipart boundary the parts share an explicit boundary_count in proportion to their turbine counts.

Parameters:
  • n_turbines (int | None)

  • density (dict[str, float] | None)

  • boundary_count (int | None)

  • max_iterations (int)

  • tolerance (float)

  • relaxation (float)

  • seed (int | np.random.Generator | None)

class oowl.layouts.Fitter[source]#

Base strategy for fitting one layout family to one polygonal farm.

abstractmethod fit(layout, boundary, pattern, rng, hint=None)[source]#

Return fitted positions and JSON-compatible fit metadata.

Pattern layouts pass their (N, 2) point pattern and an optional verified (scale, translation) hint. Candidate layouts build their own candidate field, so they pass pattern=None and no hint.

Parameters:
  • layout (Any)

  • boundary (Boundary)

  • pattern (np.ndarray | None)

  • rng (np.random.Generator)

  • hint (tuple[float, np.ndarray] | None)

Return type:

tuple[np.ndarray, dict]

class oowl.layouts.Grid(n_turbines=None, *, density=None, spacing_ratio=1.0, lattice_angle=90.0, rotation=0.0, fitter=None, seed=None)[source]#

Occupy selected intersections of a regular parallelogram grid.

spacing_ratio is the length of the second grid vector divided by the first. lattice_angle is the angle in degrees between them, and rotation orients the first vector globally. The fitter finds the largest aligned grid that offers at least n_turbines valid intersections, then selects an exact, well-distributed subset.

Parameters:
estimate_spacing(boundary)[source]#

Return a starting length of the first grid vector in meters.

Assumes one parallelogram cell per turbine. The estimate only seeds the fitter’s search and is not the final spacing.

Parameters:

boundary (Boundary)

Return type:

float

class oowl.layouts.LayoutAlgorithm(n_turbines=None, *, density=None, fitter=None, seed=None)[source]#

Base class for one turbine-placement method applied to one farm.

A boundary fixes the farm’s area, so a layout inside one takes one more quantity to size the farm, n_turbines or a density mapping that names one of these.

  • n_turbines, the exact count of turbines.

  • {"area_per_turbine": square meters}, the boundary’s area per turbine, which rounds to the nearest count of at least one.

  • {"minimum_spacing": meters}, the most turbines that fit with no pair closer than it. Uniform and Sobol hold no minimum, so a random farm is sized by its single closest pair and varies widely between seeds.

  • {"mean_spacing": meters}, the most turbines whose mean nearest-neighbor distance, mean_spacing(), is at least it over the whole farm, the turbines shared over its parts by area.

  • {"maximum_spacing": meters}, the fewest turbines that leave none farther than it from its nearest neighbor, maximum_spacing(), over the whole farm.

A count and a density over-constrain the boundary’s area, so a farm of fixed count is held to a spacing by a sampler’s requirements instead. A spacing that fits fewer than two turbines gives a farm of the one that fits, recorded as metadata["fits_one_turbine"]. A layout with neither is a template for Mixture or a sampler, which then supplies one. seed replays the placement and every unseeded modifier added with modify().

Parameters:
  • n_turbines (int | None)

  • density (dict[str, float] | None)

  • fitter (Fitter | None)

  • seed (int | np.random.Generator | None)

property density: dict[str, float] | None#

The density sizing this layout as {name: value}, or None.

generate(boundary=None)[source]#

Generate this layout, optionally fitted to boundary.

A multipart boundary is filled part by part, each part receiving its share of turbines by area. The parts are the partition cells of a partitioned boundary, in its parts order, else the polygons of its shape. n_turbines counts the turbines left after the modifiers, so the layout places the ones a removal drops on top. A spacing is recorded in the farm’s metadata under its own name. With minimum_spacing a layout that fills directly fills the whole boundary at once, holding its modifiers to the spacing too. Others search over counts, each part on its own, later parts giving way where they meet earlier ones closer than the spacing. mean_spacing searches over counts of the whole farm, shared over its parts by area. Raises TypeError when boundary is not a boundary, and ValueError when no quantity or several size the layout, when the boundary was never given an area, or when it cannot place its turbines.

Parameters:

boundary (Boundary | None)

Return type:

Farm

modify(*modifiers)[source]#

Return a copy with post-generation modifiers appended in order.

Parameters:

modifiers (LayoutModifier)

Return type:

Self

class oowl.layouts.Mixture(layouts, n_turbines=None, *, density=None, weights=None, seed=None)[source]#

Combine layout methods in weighted regions of one farm.

Every template in layouts must omit n_turbines and density. The farm is split into one compact cell per source, a power diagram whose cell areas follow weights exactly, equal by default, and each template is laid out in its own cell. The weights also fix each source’s exact turbine count, and a source allotted no turbines takes no cell.

The cells are laid out in turn. A later cell keeps 0.8 of its own spacing from the turbines already placed and reaches one spacing past its seams, so it fills the strip an earlier cell leaves short of a seam, and an earlier cell leaves 0.2 of its spacing free along the later ones. Turbines either side of a seam stay at least 0.8 of the smaller spacing apart. A cell keeps at least half its area clear, and one its template cannot fill once cleared is laid out whole, as laid_out_whole in its cell record shows, and only its turbines may come closer.

With density={"minimum_spacing": meters} each cell is filled at it, a later one no closer to the turbines already placed than the spacing. A cell too small for two turbines at the spacing takes one, and one left no room takes none.

A template given its own seed keeps it, and the others draw from seeds spawned from the mixture’s. metadata["origin"] gives each turbine’s source, parts each source’s layout and seed and cells the power sites and weights, the cell areas, the clearance each cell kept and whether it was laid out whole.

Parameters:
class oowl.layouts.Perimeter(n_turbines=None, *, density=None, start=None, seed=None)[source]#

Place an exact turbine count equally along the exterior perimeter.

start is a normalized perimeter fraction in [0, 1). When it is None, a reproducible random start is selected from seed. With minimum_spacing, each polygon’s exterior takes the most turbines, equally spaced, whose closest pair keeps the spacing, so chords across corners and across narrow necks count. A turbine closer than the spacing to one on another part is left out.

Parameters:
  • n_turbines (int | None)

  • density (dict[str, float] | None)

  • start (float | None)

  • seed (int | np.random.Generator | None)

class oowl.layouts.PoissonDisc(n_turbines=None, *, density=None, k=30, fitter=None, seed=None)[source]#

Place turbines by Poisson-disc sampling, a blue-noise pattern.

Bridson’s algorithm, with k candidates per active point, fills a copy of the boundary scaled into the unit square. Farthest-point sampling keeps exactly n_turbines points, and the pattern is then scaled and translated to fill the boundary. With minimum_spacing, Bridson’s algorithm fills the boundary itself with that minimum distance, starting in every polygon, and the count is however many points fit.

The minimum separation starts where Bridson’s method is expected to place 5% more points than n_turbines, or three times as many for at most four turbines, from the area and perimeter of the normalized boundary. While too few fit, each retry rescales that estimate by the count the last run placed, shrinking by a factor of 0.5 to 0.98. Few spare points keep the closest pair of the selection wide. The search stops at 1e-3 for a square bounding box and lower for an elongated one, by the square root of the short side over the long side, so the sampling grid keeps the same number of cells. The value used is reported as metadata["fit"]["minimum_distance"]. Raises ValueError if too few points fit even then.

Parameters:
class oowl.layouts.RigidPatternFitter(*, max_iterations=50, tolerance=1e-06)[source]#

Maximally scale and translate an exact finite point pattern.

The scale is searched for at most max_iterations steps, stopping when the bracket is within tolerance relative to the upper bound. A step tries the scale at which the lines of the fitting region’s edges meet, just either side of it, or bisects when that does not halve the bracket.

Parameters:
  • max_iterations (int)

  • tolerance (float)

fit(layout, boundary, pattern, rng, hint=None)[source]#

Return the largest scaled and translated pattern inside boundary.

pattern has shape (N, 2). Positions come back in meters with fit metadata. An optional (scale, translation) hint that is verified feasible becomes a lower bound on the scale. A missing or empty pattern raises ValueError.

Parameters:
Return type:

tuple[np.ndarray, dict]

class oowl.layouts.SelectedCandidateFitter(*, n_phases=6, max_iterations=40, pattern_fitter=None, candidate_cap=100000, max_evaluations=None, search_intervals=128)[source]#

Fit a scalable candidate field and occupy exactly N positions.

The field is tried at n_phases by n_phases offsets. The largest feasible scale is found on search_intervals geometric steps, down to a bottom on a ladder of quarter octaves so that fits at nearby counts share their steps, then on max_iterations linear steps inside the best one. A step at which the layout bounds every field below N candidates is passed over without building its fields. The fields of the last few boundaries are kept, so fitting a nearby count again, as a spacing search does, builds few new ones, and its result is that of a first fit. Fields above candidate_cap points are skipped. max_evaluations caps the field evaluations, kept ones included, and defaults to the most the search can need. The selected positions are then enlarged by pattern_fitter when the layout allows.

Parameters:
fit(layout, boundary, pattern, rng, hint=None)[source]#

Return exactly N positions from a bounded candidate-scale search.

Candidate count is not monotone in scale on concave polygons, so the whole scale interval is scanned for the largest feasible scale before refining it, passing over the scales no field can fill. Candidate allocation and field evaluations are both capped.

Parameters:
  • layout (_CandidateLayout)

  • boundary (Boundary)

  • pattern (np.ndarray | None)

  • rng (np.random.Generator)

  • hint (tuple[float, np.ndarray] | None)

Return type:

tuple[np.ndarray, dict]

class oowl.layouts.SelectedCircularFitter(*, n_phases=6, max_iterations=40, pattern_fitter=None, candidate_cap=100000, max_evaluations=None, search_intervals=128)[source]#

Select exact positions from boundary-clipped concentric rings.

Arguments are as for SelectedCandidateFitter.

Parameters:
class oowl.layouts.SelectedContourFitter(*, n_phases=3, **kwargs)[source]#

Select exact positions from inward offsets of a farm boundary.

Arguments are as for SelectedCandidateFitter, with three phases by default.

Parameters:
  • n_phases (int)

  • kwargs (Any)

class oowl.layouts.SelectedGridFitter(*, n_phases=6, max_iterations=40, pattern_fitter=None, candidate_cap=100000, max_evaluations=None, search_intervals=128)[source]#

Select exact, well-distributed intersections from an aligned grid.

Arguments are as for SelectedCandidateFitter.

Parameters:
class oowl.layouts.Sobol(n_turbines=None, *, density=None, seed=None)[source]#

Place turbines using a scrambled Sobol low-discrepancy sequence.

A Sobol sequence covers a rectangle more evenly than independent random draws. OOWL generates a balanced power-of-two sequence over the boundary’s bounding box, retains points inside the boundary, and returns the first n_turbines accepted points. If the initial sequence is too short, its power is increased and the same scrambled sequence is regenerated.

Unlike PoissonDisc, this method imposes no minimum separation. A minimum_spacing sizes the farm by its closest pair, which one unlucky point sets, so the area or count it gives varies widely between seeds. It is useful when an irregular but spatially uniform layout is wanted without lattice alignment or a packing process.

Parameters:
  • n_turbines (int | None) – Exact number of turbines to generate.

  • density (dict[str, float] | None) – The one density setting the count, {"minimum_spacing": meters} for the most turbines with no pair closer, {"mean_spacing": meters} for the most whose mean_spacing() is at least it, {"maximum_spacing": meters} for the fewest leaving none farther from a neighbor, or {"area_per_turbine": square meters}.

  • seed (int | np.random.Generator | None) – Random seed controlling the deterministic Sobol scramble.

class oowl.layouts.String(strings, n_turbines=None, *, density=None, fitter=None, seed=None)[source]#

Distribute turbines along one or more string guides.

Turbines are shared between strings and their segments by arc length and equally spaced along each segment. Boundary fitting keeps the relative geometry and sets the scale. Without a boundary a guide has no size of its own, so a spacing density, as density={"minimum_spacing": meters}, sets it beside n_turbines and one is required. The farm carries the realized guide as farm.guide. seed also redraws every random part of the guide given no seed of its own, so each sampled farm gets its own guide, and the farm records the guide it used.

Parameters:
generate(boundary=None)[source]#

Generate this layout, optionally fitted to boundary.

A multipart boundary is filled part by part, each part receiving its share of turbines by area. The parts are the partition cells of a partitioned boundary, in its parts order, else the polygons of its shape. n_turbines counts the turbines left after the modifiers, so the layout places the ones a removal drops on top. A spacing is recorded in the farm’s metadata under its own name. With minimum_spacing a layout that fills directly fills the whole boundary at once, holding its modifiers to the spacing too. Others search over counts, each part on its own, later parts giving way where they meet earlier ones closer than the spacing. mean_spacing searches over counts of the whole farm, shared over its parts by area. Raises TypeError when boundary is not a boundary, and ValueError when no quantity or several size the layout, when the boundary was never given an area, or when it cannot place its turbines.

Parameters:

boundary (Boundary | None)

Return type:

Farm

class oowl.layouts.Sunflower(n_turbines=None, *, density=None, rotation=0.0, fitter=None, seed=None)[source]#

Fill a boundary with an exact-count phyllotactic pattern.

Candidates follow the Fermat spiral

\[r_i=c\sqrt{i},\qquad \theta_i=i\gamma+\theta_0,\]

where c is radial spacing and gamma is the golden angle. The spiral is centered on an interior point, which always holds a turbine, and clipped by the boundary. The fitter finds the largest c leaving at least n_turbines candidates, then keeps an exact, well-distributed subset.

Parameters:
  • n_turbines (int | None) – Exact number of turbines to generate.

  • density (dict[str, float] | None) – The one density setting the count instead of n_turbines, such as {"minimum_spacing": meters}, as in LayoutAlgorithm.

  • rotation (float) – Rotation of the spiral in degrees.

  • fitter (oowl.layouts.fitters.SelectedCandidateFitter) – Candidate-field fitting strategy. Defaults to one deterministic phase.

  • seed (int | np.random.Generator | None) – Random seed used only to resolve equivalent exact-count subsets.

estimate_spacing(boundary)[source]#

Return a starting radial spacing c in meters for the fitter.

Assumes one area / n_turbines disc per turbine.

Parameters:

boundary (Boundary)

Return type:

float

class oowl.layouts.Uniform(n_turbines=None, *, density=None, seed=None)[source]#

Scatter turbines independently and uniformly across the boundary.

Every position is an independent draw with no lattice and no minimum separation, so pairs may fall arbitrarily close together. A minimum_spacing therefore sizes the farm by its closest pair, which one unlucky draw sets, so the area or count it gives varies widely between seeds. This is complete spatial randomness, the null model behind clark_evans(), ripley_k() and quadrat_p_value(). Unrelated to the wind resource oowl.resources.UniformDirection.

Parameters:
  • n_turbines (int | None) – Exact number of turbines to generate.

  • density (dict[str, float] | None) – The one density setting the count, {"minimum_spacing": meters} for the most turbines with no pair closer, {"mean_spacing": meters} for the most whose mean_spacing() is at least it, {"maximum_spacing": meters} for the fewest leaving none farther from a neighbor, or {"area_per_turbine": square meters}.

  • seed (int | np.random.Generator | None) – Random seed controlling the draws.