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.