Quickstart#

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

Draw a farm#

Describe the scenario as a Sampler from a boundary, a layout and optionally a wind resource, then draw from it. The layout places exactly the requested number of turbines.

boundary = B.Rectangle(9, 5, area=45_000_000).modify(
    M.Partition.Channels(n_farms=3, gap=500, seed=3)
)
resource = R.WindResource(direction=R.VonMises(n_modes=2, seed=7), n_sectors=12)

spec = oowl.Sampler(
    boundary,
    L.Grid(n_turbines=92, lattice_angle=60, seed=4),
    resource,
)

farm = spec.sample()[0]
farm.plot()
print(farm.n_turbines)  # 92
print(farm.positions.shape)  # (92, 2)

sample() returns a tuple of scenarios, so one farm is the first element. A Farm holds positions in meters, its boundary, and metadata recording how it was drawn. Drawing again gives the same farm.

Shape arguments set proportions and area sets scale. Without area, a generated boundary is a unit-size form, which the sampler sizes from its area, a density, or n_turbines with a spacing. A farm on a shape without any size raises a ValueError.

Try another method#

Only the layout changes.

contours = oowl.Sampler(
    boundary,
    L.Contour(n_turbines=80, include_center=True, seed=5),
).sample()[0]

sunflower = oowl.Sampler(
    B.Circle(area=28_274_333.882),
    L.Sunflower(n_turbines=80, rotation=15, seed=6),
).sample()[0]

Measure a farm#

print(farm.summary())
print(farm.summary(["clark_evans", "aspect_ratio"]))

Draw a population#

Give a value a (low, high) range, or pass a class instead of an instance, and the description becomes a population.

population = oowl.Sampler(
    boundary=B.OffshorePolygon,
    layout=L.Grid,
    n_turbines=(40, 120),
    density={"minimum_spacing": 800.0},
    seed=7,
)

farms = population.sample(100)
print(round(farms[0].metadata["sample"]["closest_pair"], 1))  # 800.0

for index, drawn in enumerate(farms):
    drawn.save(f"dataset/farm_{index:04d}.yaml")

density names how tightly turbines are packed, here the minimum turbine distance in meters, and it may also be a mean_spacing, a maximum_spacing, an area_per_turbine or a power_density. A sampler takes any two of count, area and density. A count and a spacing scale each boundary without an area of its own to fit, which keeps every farm buildable. A boundary given an area keeps it, so a count and a spacing there over-constrain the farm. To hold a farm of fixed count and area to a spacing it must clear, check it with requirements={"spacing": (minimum, None)}. See Concepts.

Seeds are derived per scenario, so population.sample(10, start=90) gives farms 90 to 99 without drawing the others, and stream() yields scenarios one at a time.

Reproduce a farm#

An unseeded sampler records its root seed on every farm.

farm = oowl.Sampler(boundary, L.Grid(92)).sample()[0]
record = farm.metadata["sample"]

replay = oowl.Sampler(boundary, L.Grid(92), seed=record["root"]).sample(
    start=record["index"]
)[0]

The same seed gives the same farm with the same code, so the record also keeps versions, those of OOWL, NumPy, SciPy and Shapely that drew it. With pip, install those versions. In a checkout of the release, pixi install --frozen --environment dev installs exactly the packages pixi.lock pins.

Next, read Concepts or start the guides with Boundary shapes.