Source code for oowl.strings.primitives

"""Primitive turbine-string guide geometries."""

from __future__ import annotations

from typing import TYPE_CHECKING

import numpy as np
from shapely.affinity import rotate as shp_rotate
from shapely.affinity import translate as shp_translate
from shapely.geometry import LineString

from .._random import as_rng, resolve_seed
from .._validation import (
    _validate_finite,
    _validate_integer,
    _validate_nonnegative,
    _validate_positive,
    _validate_real_array,
)
from .base import StringGuide, _LogicalString

if TYPE_CHECKING:  # pragma: no cover
    from numpy.typing import ArrayLike

__all__ = ["Straight", "Arc", "Organic", "Polyline"]


def _validate_center(center):
    values = _validate_real_array("center", center)
    if values.shape != (2,) or not np.all(np.isfinite(values)):
        raise ValueError("center must contain two finite coordinates")
    return (float(values[0]), float(values[1]))


def _place(line, rotation, center):
    line = shp_rotate(line, rotation, origin=(0, 0), use_radians=False)
    return shp_translate(line, xoff=center[0], yoff=center[1])


[docs] class Straight(StringGuide): """A straight string guide. The line of ``length`` runs along x, turns counterclockwise by ``rotation`` degrees and is centered on ``center``. """ def __init__( self, length: float = 1.0, *, rotation: float = 0.0, center: tuple[float, float] = (0.0, 0.0), ): length = _validate_positive("length", length) rotation = _validate_finite("rotation", rotation) center = _validate_center(center) line = LineString([(-length / 2, 0), (length / 2, 0)]) super().__init__( (_LogicalString("Straight", (_place(line, rotation, center),)),) )
[docs] class Arc(StringGuide): """A circular arc whose chord is horizontal before rotation. ``span`` is the arc's angle in degrees. A positive span bends toward +y and a negative one toward -y. The arc's midpoint sits at ``center``, and ``rotation`` turns it counterclockwise in degrees. """ def __init__( self, radius: float = 1.0, *, span: float = 90.0, rotation: float = 0.0, center: tuple[float, float] = (0.0, 0.0), ): radius = _validate_positive("radius", radius) span = _validate_finite("span", span) if not 0 < abs(span) < 360: raise ValueError("abs(span) must be between 0 and 360 degrees") rotation = _validate_finite("rotation", rotation) center = _validate_center(center) half = np.deg2rad(abs(span)) / 2 theta = np.linspace(-half, half, 129) direction = 1.0 if span > 0 else -1.0 x = radius * np.sin(theta) y = direction * radius * (1 - np.cos(theta)) line = _place(LineString(np.column_stack([x, y])), rotation, center) super().__init__((_LogicalString("Arc", (line,)),))
[docs] class Organic(StringGuide): r"""A smooth random guide constructed from low-frequency harmonics. The lateral offset along the normalized length :math:`s` is .. math:: y(s)=\sum_{k=1}^{n} a_k \frac{\sin(k\pi s)}{k^2}, with standard normal :math:`a_k` and ``n = n_harmonics``. It is scaled so the peak offset is ``roughness * length``. ``rotation`` and ``center`` place it as in :class:`Straight`. Without ``seed``, a :class:`~oowl.layouts.String` layout draws a new shape for every farm. """ def __init__( self, length: float = 1.0, *, roughness: float = 0.25, n_harmonics: int = 4, rotation: float = 0.0, center: tuple[float, float] = (0.0, 0.0), seed: int | np.random.Generator | None = None, ): length = _validate_positive("length", length) roughness = _validate_nonnegative("roughness", roughness) n_harmonics = _validate_integer("n_harmonics", n_harmonics) rotation = _validate_finite("rotation", rotation) center = _validate_center(center) self._pinned_seed = seed is not None self.seed = resolve_seed(seed) rng = as_rng(self.seed) progress = np.linspace(0.0, 1.0, 257) x = length * (progress - 0.5) coefficients = rng.normal(size=int(n_harmonics)) y = sum( coefficient * np.sin(index * np.pi * progress) / index**2 for index, coefficient in enumerate(coefficients, start=1) ) peak = float(np.max(np.abs(y))) if peak > 0: y = roughness * length * y / peak line = _place(LineString(np.column_stack([x, y])), rotation, center) super().__init__((_LogicalString("Organic", (line,)),))
def _polyline_coordinates(values) -> np.ndarray: """Return ``values`` as a copied finite ``(N, 2)`` array of distinct steps.""" try: coordinates = _validate_real_array("coordinates", values).copy() except ValueError as error: raise ValueError("coordinates must have shape (N, 2)") from error if coordinates.ndim == 0: raise TypeError( f"coordinates must be an (N, 2) array, not {type(values).__name__}" ) if coordinates.ndim != 2 or coordinates.shape[1] != 2: raise ValueError("coordinates must have shape (N, 2)") if len(coordinates) < 2: raise ValueError("coordinates must contain at least two points") if not np.all(np.isfinite(coordinates)): raise ValueError("coordinates must contain only finite coordinates") if np.any(np.all(coordinates[1:] == coordinates[:-1], axis=1)): raise ValueError("coordinates must not repeat a point consecutively") with np.errstate(over="ignore"): length = LineString(coordinates).length if not np.isfinite(length): raise ValueError("coordinates must span a finite length") return coordinates
[docs] class Polyline(StringGuide): """One string through given vertices, in the order given. ``coordinates`` is an ``(N, 2)`` array of at least two points, such as a cable route or a measured turbine row. It is kept as given, since a :class:`~oowl.layouts.String` layout sets the scale and position. As in :class:`~oowl.boundaries.Polygon`, input is never repaired, so a point repeated consecutively is refused rather than dropped. Scalar or non-numeric ``coordinates`` raise ``TypeError``. A wrong shape, fewer than two points, non-finite values, a repeated point or a length that overflows raise ``ValueError``. Like the ``exterior`` of :class:`~oowl.boundaries.Polygon`, the repr lists up to four vertices and gives a longer array by its shape. """ def __init__(self, coordinates: ArrayLike): self.coordinates = _polyline_coordinates(coordinates) line = LineString(self.coordinates) super().__init__((_LogicalString("Polyline", (line,)),))