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()
../_images/6ffac1ab64244227a139ea1350e7c9239785fa7cb5321984a37d4f96b7c738ab.png

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()
../_images/3189eb51dea4421c94913abe978110e1f438b62b020c937e262424a7e005a932.png

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()
../_images/1dad0b001bc0e5abfac5baee938392d0fb1fc895ed4e8735a7e610068e0e3485.png

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",
);
../_images/d8b416406842ad0c616506330e75ada9b06d7da600c2578f70d3d9aa54c4cf65.png

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()
../_images/4f120f86ff055af1c77c39ec9025adaa4ef2bd0f167c98dc3aa87cb7a04da759.png