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.
|
Base class for one turbine-placement method applied to one farm. |
|
Automatic boundary-grid layout based on Stanley and Ning (2019). |
|
Place an exact turbine count equally along the exterior perimeter. |
|
Generate a boundary-conforming unstructured triangular layout. |
|
Occupy selected intersections of a regular parallelogram grid. |
|
Occupy selected positions from concentric circular rings. |
|
Place turbines along nested inward offsets of the farm boundary. |
|
Place turbines by Poisson-disc sampling, a blue-noise pattern. |
|
Place turbines using a scrambled Sobol low-discrepancy sequence. |
|
Scatter turbines independently and uniformly across the boundary. |
|
Distribute turbines along one or more string guides. |
|
Fill a boundary with an exact-count phyllotactic pattern. |
|
Combine layout methods in weighted regions of one farm. |
|
Base strategy for fitting one layout family to one polygonal farm. |
|
Maximally scale and translate an exact finite point pattern. |
|
Fit a scalable candidate field and occupy exactly |
|
Select exact, well-distributed intersections from an aligned grid. |
|
Select exact positions from boundary-clipped concentric rings. |
|
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_fractionof 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 valuesdy = 4 dxand a 20 degree row offset are fixed. The seed chooses perimeter start and grid rotation. Withminimum_spacingthe ring and the grid are both filled at that spacing, the ring holding at mostboundary_fractionof the turbines.
- 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_turbinescandidates, then selects an exact, well-distributed subset, so rings may be incomplete.spacing_ratiois the target spacing along a ring divided by the radial spacing,rotationturns the rings counterclockwise in degrees, andinclude_centerkeeps a turbine at the center.- Parameters:
- 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_ratiois the target distance along each contour divided by the distance between contour layers, andinclude_centeradds 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:
- 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=Nonederives the number of boundary nodes from the equilateral spacing ofn_turbinestriangles 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 byrelaxationtimes 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 thantolerancetimes that spacing, over 600 offshore and other farms within 156 iterations for 99 in 100 and 188 at most, somax_iterationsdefaults 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 explicitboundary_countin proportion to their turbine counts.
- 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)pointpatternand an optional verified(scale, translation)hint. Candidate layouts build their own candidate field, so they passpattern=Noneand no hint.
- 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_ratiois the length of the second grid vector divided by the first.lattice_angleis the angle in degrees between them, androtationorients the first vector globally. The fitter finds the largest aligned grid that offers at leastn_turbinesvalid intersections, then selects an exact, well-distributed subset.- Parameters:
- 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_turbinesor adensitymapping 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.UniformandSobolhold 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
requirementsinstead. A spacing that fits fewer than two turbines gives a farm of the one that fits, recorded asmetadata["fits_one_turbine"]. A layout with neither is a template forMixtureor a sampler, which then supplies one.seedreplays the placement and every unseeded modifier added withmodify().- Parameters:
- property density: dict[str, float] | None#
The density sizing this layout as
{name: value}, orNone.
- 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
partsorder, else the polygons of its shape.n_turbinescounts 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. Withminimum_spacinga 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_spacingsearches over counts of the whole farm, shared over its parts by area. RaisesTypeErrorwhenboundaryis not a boundary, andValueErrorwhen no quantity or several size the layout, when the boundary was never given an area, or when it cannot place its turbines.
- 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
layoutsmust omitn_turbinesanddensity. The farm is split into one compact cell per source, a power diagram whose cell areas followweightsexactly, 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_wholein 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,partseach source’s layout and seed andcellsthe power sites and weights, the cell areas, the clearance each cell kept and whether it was laid out whole.
- class oowl.layouts.Perimeter(n_turbines=None, *, density=None, start=None, seed=None)[source]#
Place an exact turbine count equally along the exterior perimeter.
startis a normalized perimeter fraction in[0, 1). When it isNone, a reproducible random start is selected fromseed. Withminimum_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.
- 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
kcandidates per active point, fills a copy of the boundary scaled into the unit square. Farthest-point sampling keeps exactlyn_turbinespoints, and the pattern is then scaled and translated to fill the boundary. Withminimum_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 asmetadata["fit"]["minimum_distance"]. RaisesValueErrorif too few points fit even then.
- 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_iterationssteps, stopping when the bracket is withintolerancerelative 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.- fit(layout, boundary, pattern, rng, hint=None)[source]#
Return the largest scaled and translated
patterninsideboundary.patternhas shape(N, 2). Positions come back in meters with fit metadata. An optional(scale, translation)hintthat is verified feasible becomes a lower bound on the scale. A missing or emptypatternraisesValueError.
- 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
Npositions.The field is tried at
n_phasesbyn_phasesoffsets. The largest feasible scale is found onsearch_intervalsgeometric steps, down to a bottom on a ladder of quarter octaves so that fits at nearby counts share their steps, then onmax_iterationslinear steps inside the best one. A step at which the layout bounds every field belowNcandidates 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 abovecandidate_cappoints are skipped.max_evaluationscaps the field evaluations, kept ones included, and defaults to the most the search can need. The selected positions are then enlarged bypattern_fitterwhen the layout allows.- Parameters:
- fit(layout, boundary, pattern, rng, hint=None)[source]#
Return exactly
Npositions 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.
- 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.
- 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.
- 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_turbinesaccepted 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. Aminimum_spacingsizes 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 whosemean_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 besiden_turbinesand one is required. The farm carries the realized guide asfarm.guide.seedalso 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:
strings (StringGuide)
n_turbines (int | None)
fitter (RigidPatternFitter)
seed (int | np.random.Generator | 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
partsorder, else the polygons of its shape.n_turbinescounts 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. Withminimum_spacinga 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_spacingsearches over counts of the whole farm, shared over its parts by area. RaisesTypeErrorwhenboundaryis not a boundary, andValueErrorwhen no quantity or several size the layout, when the boundary was never given an area, or when it cannot place its turbines.
- 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
cis radial spacing andgammais 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 largestcleaving at leastn_turbinescandidates, 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 inLayoutAlgorithm.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.
- 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_spacingtherefore 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 behindclark_evans(),ripley_k()andquadrat_p_value(). Unrelated to the wind resourceoowl.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 whosemean_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.