String layouts#
A guide is ordered geometry without turbine count or spacing. L.String places
turbines along it.
import matplotlib.pyplot as plt
import numpy as np
import oowl
from oowl import boundaries as B
from oowl import layouts as L
from oowl import modifiers as M
from oowl import strings as S
Guide geometry#
Straight, circular-arc and organic guides share one interface. Their
dimensions set only a relative shape, and L.String sets the scale from a
spacing or from the boundary.
guides = {
"Straight": S.Straight(rotation=15),
"Arc": S.Arc(radius=1, span=120, rotation=10),
"Organic": S.Organic(roughness=0.25, n_harmonics=4, seed=3),
}
fig, axes = plt.subplots(1, 3, figsize=(12, 3.5))
for ax, (name, guide) in zip(axes, guides.items()):
guide.plot(ax=ax, title=name)
fig.tight_layout()
Polyline runs one string through given vertices in order, such as a cable
route or a measured turbine row. The layout sets its scale like any guide’s.
route = S.Polyline([[0, 0], [2, 0.4], [3, 1.5], [5, 1.8]])
route_farm = oowl.Sampler(
layout=L.String(route, 12, density={"minimum_spacing": 800})
).sample()[0]
fig, axes = plt.subplots(1, 2, figsize=(9, 3.5))
route.plot(ax=axes[0], title="Polyline")
route_farm.plot(ax=axes[1], title=f"polyline: {route_farm.n_turbines} turbines")
fig.tight_layout()
Composing guides#
Multi combines positioned guides, Parallel repeats one with optional
angular, longitudinal and spacing variation, and Scattered packs independent
organic strings. Modifiers apply in order, so Meander before Parallel
copies one shape and after it varies each copy. See String modifiers
for Breaks and Meander.
multi = S.Multi(
[
S.Straight(length=1.2, center=(0, -0.3)),
S.Arc(radius=0.8, span=100, center=(0, 0.3)),
]
)
parallel = S.Parallel(
S.Straight(),
n_strings=4,
relative_spacing=0.12,
rotation_spread=5,
longitudinal_jitter=0.15,
spacing_jitter=0.2,
seed=6,
).modify(M.Meander(roughness=0.03, seed=4))
scattered = S.Scattered(n_strings=5, roughness=0.2, relative_spacing=0.12, seed=17)
broken = scattered.modify(M.Breaks(n_breaks=3, size=0.2, seed=5))
fig, axes = plt.subplots(1, 4, figsize=(16, 3.5))
multi.plot(ax=axes[0], title="Multi")
parallel.plot(ax=axes[1], title="Varied parallel")
scattered.plot(ax=axes[2], title="Scattered")
broken.plot(ax=axes[3], title="Scattered + breaks")
fig.tight_layout()
Standalone string layouts#
Without a boundary, minimum_spacing sets the closest pair in meters, or
mean_spacing the mean nearest-neighbor distance. M.PositionNoise on a
string farm slides each turbine along its string, so the turbines stay on the
guide.
standalone_layout = L.String(
broken, 60, density={"minimum_spacing": 650}, seed=1
).modify(M.PositionNoise(0.04, seed=1))
standalone = oowl.Sampler(layout=standalone_layout).sample()[0]
standalone.plot(
title=f"standalone string farm: {standalone.n_turbines} turbines",
);
Fitting the pattern into a boundary#
Turbines are allocated by usable path length in along-string order, and
metadata["fit"]["strings"] keeps each string and segment. Only turbines, not
guides, must stay inside the boundary. Strings use multipart boundaries and
clusters like any layout, as Multipart boundaries and clusters shows.
boundary = B.SmoothBlob(area=np.pi * 3000**2, roughness=0.2, seed=7)
fig, axes = plt.subplots(1, 4, figsize=(16, 4))
for ax, (name, guide) in zip(
axes,
[
("single arc", guides["Arc"]),
("multiple", multi),
("varied parallel", parallel),
("broken scattered", broken),
],
):
farm = oowl.Sampler(boundary, L.String(guide, 48)).sample()[0]
farm.plot(ax=ax, title=f"{name}: {farm.n_turbines} turbines")
fig.tight_layout()