Concepts#

A scenario is built from six composable layers and drawn from a Sampler.

The six layers#

Boundaries#

oowl.boundaries define the farm area. Shape arguments set proportions and area sets scale, so Rectangle(8, 5, area=40) is an 8:5 rectangle of area 40. Without area a generated shape is a unit-size form, which a sampler sizes from its area, a density, or a count and a spacing, and a farm on a shape without any size raises a ValueError. A shape without a center sits at (0, 0) and may be moved when a cluster is arranged, while Polygon keeps the coordinates it is given.

Modifiers compose through .modify(...) without changing the original. ExclusionZones removes named areas, such as a wreck or a cable corridor. Setback keeps turbines a minimum distance inside every edge. Partition cuts a boundary into cells for multipart farms and clusters. A multipart boundary, such as a Union of separate pieces, is still one farm.

Strings#

oowl.strings are optional line guides, such as Straight, Arc and Parallel, that String places turbines along. Breaks and Meander modify them.

Layouts#

oowl.layouts place an exact turbine count, or fill a boundary to a density, one mapping that names area_per_turbine, minimum_spacing, no pair closer than it, mean_spacing, the mean nearest-neighbor distance, or maximum_spacing, no turbine farther than it from its nearest neighbor, as in L.Grid(density={"minimum_spacing": 800.0}). Uniform and Sobol place turbines at random, so a minimum spacing sizes them by their single closest pair and a mean spacing gives steadier farms.

  • Grid, Circular, Contour and Sunflower select positions from a lattice, rings, inward offsets of the boundary or a Fermat spiral, scaled to fill the boundary.

  • BoundaryGrid combines edge turbines with an inner grid after Stanley and Ning (2019), and Perimeter places turbines along the edge only.

  • Delaunay relaxes an unstructured triangular mesh.

  • PoissonDisc draws blue noise, Sobol a low-discrepancy sequence, and Uniform independent points, the null model the spatial metrics compare against.

  • String follows string guides, and Mixture lays several families out in cells of one farm.

Layout modifiers act after fitting, for example removing row-correlated vacancies from a grid. A layout given n_turbines places the turbines a removal drops on top, so exactly that many remain.

import oowl
from oowl import boundaries as B
from oowl import layouts as L
from oowl import modifiers as M

boundary = B.Rectangle(9, 5, area=45_000_000)

layout = L.Grid(100, seed=8).modify(
    M.TurbineRemoval(70, method="markov", transition_penalty=0.75, seed=9)
)
farm = oowl.Sampler(boundary, layout).sample()[0]  # 100 of 170 grid nodes kept

Resources#

A WindResource is an analytical wind climate. Its direction shares the wind between sectors, UniformDirection, SingleDirection, VonMises or Elliptical, and each sector has Weibull speeds, a surface, stability and turbulence. GWA builds one from the Global Wind Atlas. resource.sample(n) draws inflow cases over 4 to 25 m/s by default, or over another range given by u_min and u_max. Every case includes atmospheric stability, coupled to turbulence, as Stability and turbulence describes.

Turbines#

oowl.turbines provides Idealized, Controlled and Tabulated performance models, assigned to a farm as one type or one per position.

Surfaces#

oowl.surfaces give terrain elevation onshore or positive water depth offshore, from Gaussian or Perlin fields. A farm carries at most one, and a cluster shares one across its farms.

Scenarios#

The description decides what a scenario is. A boundary and a layout give one Farm. n_farms=N, or a list of boundaries, each built or a recipe drawn for its member, gives a Cluster of farms placed together, gap meters apart. domain= gives a region of development sites scattered across it.

In a cluster, turbines are drawn per farm while one resource and surface cover the whole cluster. cluster.plot() draws its farms on one map.

from oowl import resources as R

cluster = oowl.Sampler(
    [B.Circle(area=28_274_333.882), B.OffshorePolygon(area=20_000_000, seed=8)],
    [L.Grid(60), L.PoissonDisc(40)],
    R.WindResource(direction=R.VonMises(n_modes=2, seed=7), n_sectors=12),
    gap=(500, 3000),
    seed=8,
).sample()[0]

Populations#

Fix every value and each scenario is the same. Give a value a (low, high) range, an f(rng) callable, an oowl.sampling.Choice or a (constructor, kwargs) recipe, and the description becomes a population. configuration varies several values together, and a component with its own seed stays fixed across the population.

Count, area and spacing constrain one geometry, so a sampler takes at most two. Ranging count and area independently lets density wander, while count and spacing scale each boundary to fit.

loose = oowl.Sampler(
    B.OffshorePolygon, L.Grid(), n_turbines=(20, 200), area=(5e6, 40e6), seed=3
).sample(8)
tight = oowl.Sampler(
    B.OffshorePolygon,
    L.Grid(),
    n_turbines=(20, 200),
    density={"minimum_spacing": 800.0},
    seed=3,
).sample(8)

print([round(farm.metadata["sample"]["closest_pair"]) for farm in loose])
print([round(farm.metadata["sample"]["closest_pair"]) for farm in tight])

The loose farms come out 322 m to 1037 m apart, and at 322 m a 240 m rotor stands 1.3 diameters from its neighbor. Every tight farm is at 800 m, on a boundary scaled to between 23.7 and 100.5 km². Sampling gives the full rule.

Seeds#

Seeds are derived per scenario by index, so scenario i is the same however many are drawn, and start= shards a population. An unseeded sampler records its root seed on every scenario, which replays it.

record = farm.metadata["sample"]
replay = oowl.Sampler(boundary, layout, seed=record["root"]).sample(
    start=record["index"]
)[0]

A random boundary or guide given a seed keeps its geometry in every scenario, and one without is redrawn from each scenario’s seed.

Units and windIO#

Coordinates are in meters. String guide lengths are relative when fitted to a boundary. Farm.positions is an (N, 2) array matching windIO’s coordinates.x and coordinates.y. oowl.save and oowl.load handle one farm and Cluster.save a cluster, as windIO interchange describes.