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,ContourandSunflowerselect positions from a lattice, rings, inward offsets of the boundary or a Fermat spiral, scaled to fill the boundary.BoundaryGridcombines edge turbines with an inner grid after Stanley and Ning (2019), andPerimeterplaces turbines along the edge only.Delaunayrelaxes an unstructured triangular mesh.PoissonDiscdraws blue noise,Sobola low-discrepancy sequence, andUniformindependent points, the null model the spatial metrics compare against.Stringfollows string guides, andMixturelays 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.