oowl 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oowl-0.0.1/LICENSE +7 -0
- oowl-0.0.1/PKG-INFO +124 -0
- oowl-0.0.1/README.md +101 -0
- oowl-0.0.1/pyproject.toml +34 -0
- oowl-0.0.1/setup.cfg +4 -0
- oowl-0.0.1/src/oowl/__init__.py +45 -0
- oowl-0.0.1/src/oowl/_random.py +71 -0
- oowl-0.0.1/src/oowl/api.py +243 -0
- oowl-0.0.1/src/oowl/boundaries/__init__.py +36 -0
- oowl-0.0.1/src/oowl/boundaries/base.py +117 -0
- oowl-0.0.1/src/oowl/boundaries/composites.py +102 -0
- oowl-0.0.1/src/oowl/boundaries/hull.py +38 -0
- oowl-0.0.1/src/oowl/boundaries/modifiers.py +141 -0
- oowl-0.0.1/src/oowl/boundaries/primitives.py +150 -0
- oowl-0.0.1/src/oowl/boundaries/radial.py +117 -0
- oowl-0.0.1/src/oowl/layouts/__init__.py +44 -0
- oowl-0.0.1/src/oowl/layouts/base.py +293 -0
- oowl-0.0.1/src/oowl/layouts/candidate.py +43 -0
- oowl-0.0.1/src/oowl/layouts/circular.py +86 -0
- oowl-0.0.1/src/oowl/layouts/composites.py +65 -0
- oowl-0.0.1/src/oowl/layouts/contour.py +162 -0
- oowl-0.0.1/src/oowl/layouts/fitters.py +272 -0
- oowl-0.0.1/src/oowl/layouts/grid.py +88 -0
- oowl-0.0.1/src/oowl/layouts/modifiers.py +78 -0
- oowl-0.0.1/src/oowl/layouts/pattern.py +53 -0
- oowl-0.0.1/src/oowl/layouts/poisson.py +129 -0
- oowl-0.0.1/src/oowl/layouts/string.py +132 -0
- oowl-0.0.1/src/oowl/plotting.py +288 -0
- oowl-0.0.1/src/oowl/site/__init__.py +27 -0
- oowl-0.0.1/src/oowl/site/base.py +193 -0
- oowl-0.0.1/src/oowl/site/directions.py +128 -0
- oowl-0.0.1/src/oowl/site/sampling.py +153 -0
- oowl-0.0.1/src/oowl/strings/__init__.py +20 -0
- oowl-0.0.1/src/oowl/strings/base.py +95 -0
- oowl-0.0.1/src/oowl/strings/composites.py +212 -0
- oowl-0.0.1/src/oowl/strings/modifiers.py +142 -0
- oowl-0.0.1/src/oowl/strings/primitives.py +96 -0
- oowl-0.0.1/src/oowl/windio.py +166 -0
- oowl-0.0.1/src/oowl.egg-info/PKG-INFO +124 -0
- oowl-0.0.1/src/oowl.egg-info/SOURCES.txt +41 -0
- oowl-0.0.1/src/oowl.egg-info/dependency_links.txt +1 -0
- oowl-0.0.1/src/oowl.egg-info/requires.txt +10 -0
- oowl-0.0.1/src/oowl.egg-info/top_level.txt +2 -0
oowl-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright (c) 2026 DTU Wind and Energy Systems
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
oowl-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: oowl
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Onshore and offshore wind layout generation.
|
|
5
|
+
Author: DTU Wind Energy Systems
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://gitlab.windenergy.dtu.dk/fpwra/OOWL
|
|
8
|
+
Project-URL: Documentation, https://oowl-4781ed.pages.windenergy.dtu.dk
|
|
9
|
+
Keywords: wind farm,onshore,offshore,layout,boundary,geometry
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: numpy>=1.24
|
|
14
|
+
Requires-Dist: shapely>=2.0
|
|
15
|
+
Requires-Dist: matplotlib>=3.7
|
|
16
|
+
Requires-Dist: scipy>=1.10
|
|
17
|
+
Requires-Dist: windIO>=2.0
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
20
|
+
Requires-Dist: jupyter; extra == "dev"
|
|
21
|
+
Requires-Dist: ipykernel; extra == "dev"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# OOWL
|
|
25
|
+
|
|
26
|
+
Onshore and Offshore Wind Layouts (OOWL) generates diverse, exact-count
|
|
27
|
+
wind-farm layouts from composable geometry and placement methods.
|
|
28
|
+
|
|
29
|
+
The Python package is currently imported as `oowl`.
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import oowl
|
|
33
|
+
from oowl import boundaries as B
|
|
34
|
+
from oowl import layouts as L
|
|
35
|
+
|
|
36
|
+
boundary = B.Rectangle(9000, 5000).with_channels(
|
|
37
|
+
n_channels=2, width=500, seed=3
|
|
38
|
+
)
|
|
39
|
+
farm = oowl.generate_farm(
|
|
40
|
+
boundary,
|
|
41
|
+
L.GridLayout(n_turbines=92, lattice_angle=60, seed=4),
|
|
42
|
+
)
|
|
43
|
+
farm.plot()
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Model
|
|
47
|
+
|
|
48
|
+
The public API has three independent layers:
|
|
49
|
+
|
|
50
|
+
- `boundaries` define the allowed polygonal farm area. A `MultiPolygon` remains
|
|
51
|
+
one logical farm; `BoundaryCluster` explicitly represents multiple farms.
|
|
52
|
+
- `strings` define optional ordered line guides. Breaks and meanders compose as
|
|
53
|
+
non-mutating geometry modifiers.
|
|
54
|
+
- `layouts` place an exact turbine count. Grids occupy selected intersections
|
|
55
|
+
of an aligned parallelogram grid, circular layouts select from boundary-
|
|
56
|
+
clipped concentric rings, contour layouts follow nested inward copies of the
|
|
57
|
+
farm boundary and retain a central point by default, Poisson layouts provide
|
|
58
|
+
blue-noise patterns, and string
|
|
59
|
+
farms distribute turbines along guides.
|
|
60
|
+
|
|
61
|
+
`oowl.generate_farm(...)` is the single entry point for ordinary boundaries,
|
|
62
|
+
multipart farms, clusters, and standalone string layouts.
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
contours = oowl.generate_farm(
|
|
66
|
+
boundary,
|
|
67
|
+
L.ContourLayout(n_turbines=80, include_center=True),
|
|
68
|
+
)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Strings and clusters
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from oowl import strings as S
|
|
75
|
+
|
|
76
|
+
guide = S.Parallel(S.Straight(), n_strings=5, spacing=0.18)
|
|
77
|
+
guide = guide.with_meander(roughness=0.03, seed=2)
|
|
78
|
+
|
|
79
|
+
string_farm = oowl.generate_farm(
|
|
80
|
+
B.Circle(3000),
|
|
81
|
+
L.StringLayout(60, strings=guide),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
cluster = B.BoundaryCluster.from_components(boundary)
|
|
85
|
+
mixed = oowl.generate_farm(
|
|
86
|
+
cluster,
|
|
87
|
+
L.MixedLayout(
|
|
88
|
+
n_turbines=92,
|
|
89
|
+
layouts=[L.GridLayout(), L.StringLayout(strings=S.Straight())],
|
|
90
|
+
seed=8,
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Units and windIO
|
|
96
|
+
|
|
97
|
+
Realized boundary and turbine coordinates are interpreted as metres. String
|
|
98
|
+
guide lengths and spacing are relative shape parameters when fitted to a
|
|
99
|
+
boundary; standalone `StringLayout.spacing` is dimensional.
|
|
100
|
+
|
|
101
|
+
`Farm.positions` is an ordered `(N, 2)` array that maps directly to windIO's
|
|
102
|
+
`coordinates.x` and `coordinates.y`. Cluster provenance stores contiguous farm
|
|
103
|
+
slices so a future adapter can emit windIO's array of wind-farm objects. The
|
|
104
|
+
package does not currently depend on windIO or model turbines, substations, or
|
|
105
|
+
electrical collection arrays.
|
|
106
|
+
|
|
107
|
+
## Randomness and provenance
|
|
108
|
+
|
|
109
|
+
Explicit seeds reproduce a farm. An unseeded generation call receives fresh
|
|
110
|
+
entropy and records the resolved seed in `Farm.provenance`, so it can be
|
|
111
|
+
replayed later:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
farm = oowl.generate_farm(boundary, L.GridLayout(92))
|
|
115
|
+
replay = oowl.generate_farm(
|
|
116
|
+
boundary,
|
|
117
|
+
L.GridLayout(92),
|
|
118
|
+
seed=farm.provenance["seed"],
|
|
119
|
+
)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Random boundaries and guides resolve their seed when constructed. Reusing one
|
|
123
|
+
such object therefore reuses the same realized geometry, while constructing a
|
|
124
|
+
new unseeded object produces a new geometry.
|
oowl-0.0.1/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# OOWL
|
|
2
|
+
|
|
3
|
+
Onshore and Offshore Wind Layouts (OOWL) generates diverse, exact-count
|
|
4
|
+
wind-farm layouts from composable geometry and placement methods.
|
|
5
|
+
|
|
6
|
+
The Python package is currently imported as `oowl`.
|
|
7
|
+
|
|
8
|
+
```python
|
|
9
|
+
import oowl
|
|
10
|
+
from oowl import boundaries as B
|
|
11
|
+
from oowl import layouts as L
|
|
12
|
+
|
|
13
|
+
boundary = B.Rectangle(9000, 5000).with_channels(
|
|
14
|
+
n_channels=2, width=500, seed=3
|
|
15
|
+
)
|
|
16
|
+
farm = oowl.generate_farm(
|
|
17
|
+
boundary,
|
|
18
|
+
L.GridLayout(n_turbines=92, lattice_angle=60, seed=4),
|
|
19
|
+
)
|
|
20
|
+
farm.plot()
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Model
|
|
24
|
+
|
|
25
|
+
The public API has three independent layers:
|
|
26
|
+
|
|
27
|
+
- `boundaries` define the allowed polygonal farm area. A `MultiPolygon` remains
|
|
28
|
+
one logical farm; `BoundaryCluster` explicitly represents multiple farms.
|
|
29
|
+
- `strings` define optional ordered line guides. Breaks and meanders compose as
|
|
30
|
+
non-mutating geometry modifiers.
|
|
31
|
+
- `layouts` place an exact turbine count. Grids occupy selected intersections
|
|
32
|
+
of an aligned parallelogram grid, circular layouts select from boundary-
|
|
33
|
+
clipped concentric rings, contour layouts follow nested inward copies of the
|
|
34
|
+
farm boundary and retain a central point by default, Poisson layouts provide
|
|
35
|
+
blue-noise patterns, and string
|
|
36
|
+
farms distribute turbines along guides.
|
|
37
|
+
|
|
38
|
+
`oowl.generate_farm(...)` is the single entry point for ordinary boundaries,
|
|
39
|
+
multipart farms, clusters, and standalone string layouts.
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
contours = oowl.generate_farm(
|
|
43
|
+
boundary,
|
|
44
|
+
L.ContourLayout(n_turbines=80, include_center=True),
|
|
45
|
+
)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Strings and clusters
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from oowl import strings as S
|
|
52
|
+
|
|
53
|
+
guide = S.Parallel(S.Straight(), n_strings=5, spacing=0.18)
|
|
54
|
+
guide = guide.with_meander(roughness=0.03, seed=2)
|
|
55
|
+
|
|
56
|
+
string_farm = oowl.generate_farm(
|
|
57
|
+
B.Circle(3000),
|
|
58
|
+
L.StringLayout(60, strings=guide),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
cluster = B.BoundaryCluster.from_components(boundary)
|
|
62
|
+
mixed = oowl.generate_farm(
|
|
63
|
+
cluster,
|
|
64
|
+
L.MixedLayout(
|
|
65
|
+
n_turbines=92,
|
|
66
|
+
layouts=[L.GridLayout(), L.StringLayout(strings=S.Straight())],
|
|
67
|
+
seed=8,
|
|
68
|
+
),
|
|
69
|
+
)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Units and windIO
|
|
73
|
+
|
|
74
|
+
Realized boundary and turbine coordinates are interpreted as metres. String
|
|
75
|
+
guide lengths and spacing are relative shape parameters when fitted to a
|
|
76
|
+
boundary; standalone `StringLayout.spacing` is dimensional.
|
|
77
|
+
|
|
78
|
+
`Farm.positions` is an ordered `(N, 2)` array that maps directly to windIO's
|
|
79
|
+
`coordinates.x` and `coordinates.y`. Cluster provenance stores contiguous farm
|
|
80
|
+
slices so a future adapter can emit windIO's array of wind-farm objects. The
|
|
81
|
+
package does not currently depend on windIO or model turbines, substations, or
|
|
82
|
+
electrical collection arrays.
|
|
83
|
+
|
|
84
|
+
## Randomness and provenance
|
|
85
|
+
|
|
86
|
+
Explicit seeds reproduce a farm. An unseeded generation call receives fresh
|
|
87
|
+
entropy and records the resolved seed in `Farm.provenance`, so it can be
|
|
88
|
+
replayed later:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
farm = oowl.generate_farm(boundary, L.GridLayout(92))
|
|
92
|
+
replay = oowl.generate_farm(
|
|
93
|
+
boundary,
|
|
94
|
+
L.GridLayout(92),
|
|
95
|
+
seed=farm.provenance["seed"],
|
|
96
|
+
)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Random boundaries and guides resolve their seed when constructed. Reusing one
|
|
100
|
+
such object therefore reuses the same realized geometry, while constructing a
|
|
101
|
+
new unseeded object produces a new geometry.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "oowl"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "Onshore and offshore wind layout generation."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "DTU Wind Energy Systems" }]
|
|
14
|
+
keywords = ["wind farm", "onshore", "offshore", "layout", "boundary", "geometry"]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"numpy>=1.24",
|
|
17
|
+
"shapely>=2.0",
|
|
18
|
+
"matplotlib>=3.7",
|
|
19
|
+
"scipy>=1.10",
|
|
20
|
+
"windIO>=2.0",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
dev = ["pytest>=7", "jupyter", "ipykernel"]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Repository = "https://gitlab.windenergy.dtu.dk/fpwra/OOWL"
|
|
28
|
+
Documentation = "https://oowl-4781ed.pages.windenergy.dtu.dk"
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.packages.find]
|
|
31
|
+
where = ["src"]
|
|
32
|
+
|
|
33
|
+
[tool.pytest.ini_options]
|
|
34
|
+
testpaths = ["tests"]
|
oowl-0.0.1/setup.cfg
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""OOWL: onshore and offshore wind layout generation.
|
|
2
|
+
|
|
3
|
+
Three composable layers combined by a single entry point:
|
|
4
|
+
|
|
5
|
+
* :mod:`oowl.boundaries` -- region generators (``Circle``, ``Rectangle``,
|
|
6
|
+
``RadialPolygon``, ...), composable with modifiers (shipping channels,
|
|
7
|
+
exclusion zones).
|
|
8
|
+
* :mod:`oowl.layouts` -- turbine placement algorithms (``GridLayout``),
|
|
9
|
+
sized strictly by turbine count and fitted to a boundary by a pluggable
|
|
10
|
+
:class:`~oowl.layouts.fitters.Fitter`.
|
|
11
|
+
* :mod:`oowl.strings` -- optional ordered guide geometry consumed by
|
|
12
|
+
``StringLayout``.
|
|
13
|
+
|
|
14
|
+
Combine them with :func:`generate_farm`, and persist results with windIO via
|
|
15
|
+
:meth:`Farm.save <oowl.layouts.base.Farm.save>` / :func:`load`.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from . import boundaries, layouts, plotting, site, strings
|
|
21
|
+
from .api import generate_farm
|
|
22
|
+
from .layouts import Farm
|
|
23
|
+
from .plotting import plot_boundary, plot_farm
|
|
24
|
+
from .site import SiteConditions, Weibull, WindRose, generate_site_conditions
|
|
25
|
+
from .windio import load, save
|
|
26
|
+
|
|
27
|
+
__version__ = "0.0.1"
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"boundaries",
|
|
31
|
+
"layouts",
|
|
32
|
+
"strings",
|
|
33
|
+
"plotting",
|
|
34
|
+
"site",
|
|
35
|
+
"generate_farm",
|
|
36
|
+
"generate_site_conditions",
|
|
37
|
+
"Farm",
|
|
38
|
+
"SiteConditions",
|
|
39
|
+
"Weibull",
|
|
40
|
+
"WindRose",
|
|
41
|
+
"plot_boundary",
|
|
42
|
+
"plot_farm",
|
|
43
|
+
"load",
|
|
44
|
+
"save",
|
|
45
|
+
]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Shared random-number utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"as_rng",
|
|
9
|
+
"farthest_point_sample",
|
|
10
|
+
"resolve_seed",
|
|
11
|
+
"spawn_seeds",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_MAX_SEED = np.iinfo(np.uint64).max
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def resolve_seed(seed: int | np.random.Generator | None) -> int:
|
|
19
|
+
"""Resolve ``seed`` to a replayable non-negative integer.
|
|
20
|
+
|
|
21
|
+
``None`` receives fresh operating-system entropy. Passing a generator
|
|
22
|
+
consumes one integer from it, isolating the generated object from later
|
|
23
|
+
changes to that generator's state.
|
|
24
|
+
"""
|
|
25
|
+
if isinstance(seed, np.random.Generator):
|
|
26
|
+
return int(seed.integers(0, _MAX_SEED, dtype=np.uint64))
|
|
27
|
+
if seed is None:
|
|
28
|
+
return int(np.random.SeedSequence().entropy)
|
|
29
|
+
if not isinstance(seed, (int, np.integer)) or int(seed) < 0:
|
|
30
|
+
raise ValueError("seed must be a non-negative integer, Generator, or None")
|
|
31
|
+
return int(seed)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def spawn_seeds(seed: int, n_children: int) -> tuple[int, ...]:
|
|
35
|
+
"""Derive independent, deterministic child seeds from ``seed``."""
|
|
36
|
+
if n_children < 0:
|
|
37
|
+
raise ValueError("n_children must be >= 0")
|
|
38
|
+
children = np.random.SeedSequence(int(seed)).spawn(n_children)
|
|
39
|
+
return tuple(
|
|
40
|
+
int(child.generate_state(1, dtype=np.uint64)[0])
|
|
41
|
+
for child in children
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def as_rng(seed: int | np.random.Generator | None) -> np.random.Generator:
|
|
46
|
+
"""Return a NumPy ``Generator`` from a seed, generator, or ``None``."""
|
|
47
|
+
if isinstance(seed, np.random.Generator):
|
|
48
|
+
return seed
|
|
49
|
+
return np.random.default_rng(seed)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def farthest_point_sample(pts: np.ndarray, n: int, rng) -> np.ndarray:
|
|
53
|
+
"""Select *n* points from *pts* using farthest-point sampling.
|
|
54
|
+
|
|
55
|
+
Iteratively picks the point farthest from all previously selected points,
|
|
56
|
+
producing a spatially uniform subsample that preserves coverage.
|
|
57
|
+
"""
|
|
58
|
+
m = len(pts)
|
|
59
|
+
if m <= n:
|
|
60
|
+
return pts
|
|
61
|
+
|
|
62
|
+
selected = [int(rng.integers(m))]
|
|
63
|
+
dist_sq = np.sum((pts - pts[selected[0]]) ** 2, axis=1)
|
|
64
|
+
|
|
65
|
+
for _ in range(n - 1):
|
|
66
|
+
idx = int(np.argmax(dist_sq))
|
|
67
|
+
selected.append(idx)
|
|
68
|
+
new_dist_sq = np.sum((pts - pts[idx]) ** 2, axis=1)
|
|
69
|
+
dist_sq = np.minimum(dist_sq, new_dist_sq)
|
|
70
|
+
|
|
71
|
+
return pts[selected]
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Single public entry point for ordinary, multipart, and clustered farms."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from copy import copy
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from ._random import as_rng, resolve_seed, spawn_seeds
|
|
11
|
+
from .boundaries.base import Boundary
|
|
12
|
+
from .boundaries.composites import BoundaryCluster
|
|
13
|
+
from .layouts.base import Farm, LayoutAlgorithm, _allocate_counts, _combine_guides
|
|
14
|
+
from .layouts.composites import MixedLayout
|
|
15
|
+
|
|
16
|
+
__all__ = ["generate_farm"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def generate_farm(
|
|
20
|
+
boundary: Boundary | LayoutAlgorithm,
|
|
21
|
+
layout: LayoutAlgorithm | MixedLayout | Sequence[LayoutAlgorithm] | None = None,
|
|
22
|
+
seed=None,
|
|
23
|
+
) -> Farm:
|
|
24
|
+
"""Generate one exact-count layout, optionally fitted to a boundary.
|
|
25
|
+
|
|
26
|
+
A multipart ordinary boundary remains one farm and uses one algorithm.
|
|
27
|
+
``BoundaryCluster`` coordinates multiple logical farms and accepts one
|
|
28
|
+
fixed algorithm, a ``MixedLayout``, or an explicit algorithm per farm.
|
|
29
|
+
"""
|
|
30
|
+
if layout is None:
|
|
31
|
+
if not isinstance(boundary, LayoutAlgorithm):
|
|
32
|
+
raise TypeError("standalone generation requires a LayoutAlgorithm")
|
|
33
|
+
layout = boundary
|
|
34
|
+
boundary = None
|
|
35
|
+
|
|
36
|
+
if isinstance(boundary, BoundaryCluster):
|
|
37
|
+
if isinstance(layout, MixedLayout):
|
|
38
|
+
effective_seed = layout.seed if seed is None else seed
|
|
39
|
+
return _generate_distributed_cluster(
|
|
40
|
+
boundary,
|
|
41
|
+
layout.n_turbines,
|
|
42
|
+
layout.layouts,
|
|
43
|
+
layout.probabilities,
|
|
44
|
+
effective_seed,
|
|
45
|
+
selection_method="mixed",
|
|
46
|
+
)
|
|
47
|
+
if isinstance(layout, LayoutAlgorithm):
|
|
48
|
+
if layout.n_turbines is None:
|
|
49
|
+
raise ValueError("n_turbines is required for cluster generation")
|
|
50
|
+
effective_seed = layout.seed if seed is None else seed
|
|
51
|
+
return _generate_distributed_cluster(
|
|
52
|
+
boundary,
|
|
53
|
+
layout.n_turbines,
|
|
54
|
+
(layout,),
|
|
55
|
+
(1.0,),
|
|
56
|
+
effective_seed,
|
|
57
|
+
selection_method="fixed",
|
|
58
|
+
)
|
|
59
|
+
return _generate_explicit_cluster(boundary, layout, seed)
|
|
60
|
+
|
|
61
|
+
if boundary is None:
|
|
62
|
+
if not isinstance(layout, LayoutAlgorithm):
|
|
63
|
+
raise TypeError("standalone generation requires a LayoutAlgorithm")
|
|
64
|
+
algorithm = copy(layout)
|
|
65
|
+
if seed is not None:
|
|
66
|
+
algorithm.seed = seed
|
|
67
|
+
return algorithm.generate()
|
|
68
|
+
|
|
69
|
+
if isinstance(layout, MixedLayout):
|
|
70
|
+
return _generate_mixed_single(boundary, layout, seed)
|
|
71
|
+
if not isinstance(layout, LayoutAlgorithm):
|
|
72
|
+
raise TypeError("an ordinary Boundary requires one LayoutAlgorithm")
|
|
73
|
+
algorithm = copy(layout)
|
|
74
|
+
if seed is not None:
|
|
75
|
+
algorithm.seed = seed
|
|
76
|
+
return algorithm.generate(boundary)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _with_selection(result, root_seed, index, algorithm, child_seed):
|
|
80
|
+
provenance = dict(result.provenance)
|
|
81
|
+
provenance["seed"] = int(root_seed)
|
|
82
|
+
provenance["selection"] = {
|
|
83
|
+
"method": "mixed",
|
|
84
|
+
"index": int(index),
|
|
85
|
+
"algorithm": type(algorithm).__name__,
|
|
86
|
+
"seed": int(child_seed),
|
|
87
|
+
}
|
|
88
|
+
return Farm(
|
|
89
|
+
result.positions, result.boundary, provenance, guide=result.guide
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _generate_mixed_single(boundary, mixed, seed):
|
|
94
|
+
root_seed = resolve_seed(mixed.seed if seed is None else seed)
|
|
95
|
+
selection_seed, child_seed = spawn_seeds(root_seed, 2)
|
|
96
|
+
rng = as_rng(selection_seed)
|
|
97
|
+
index = int(rng.choice(len(mixed.layouts), p=mixed.probabilities))
|
|
98
|
+
algorithm = copy(mixed.layouts[index])
|
|
99
|
+
algorithm.n_turbines = mixed.n_turbines
|
|
100
|
+
algorithm.seed = child_seed
|
|
101
|
+
result = algorithm.generate(boundary)
|
|
102
|
+
return _with_selection(result, root_seed, index, algorithm, child_seed)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _generate_distributed_cluster(
|
|
106
|
+
boundary,
|
|
107
|
+
n_turbines,
|
|
108
|
+
layouts,
|
|
109
|
+
probabilities,
|
|
110
|
+
seed,
|
|
111
|
+
selection_method,
|
|
112
|
+
):
|
|
113
|
+
root_seed = resolve_seed(seed)
|
|
114
|
+
selection_seed, farm_root = spawn_seeds(root_seed, 2)
|
|
115
|
+
counts = _allocate_counts(
|
|
116
|
+
n_turbines, [farm.area for farm in boundary.boundaries]
|
|
117
|
+
)
|
|
118
|
+
selection_rng = as_rng(selection_seed)
|
|
119
|
+
selected = selection_rng.choice(
|
|
120
|
+
len(layouts), size=len(boundary.boundaries), p=probabilities
|
|
121
|
+
)
|
|
122
|
+
farm_seeds = spawn_seeds(farm_root, len(boundary.boundaries))
|
|
123
|
+
assignments = []
|
|
124
|
+
for count, layout_index, farm_seed in zip(counts, selected, farm_seeds):
|
|
125
|
+
layout_index = int(layout_index)
|
|
126
|
+
template = layouts[layout_index]
|
|
127
|
+
assignments.append((
|
|
128
|
+
template,
|
|
129
|
+
int(count),
|
|
130
|
+
farm_seed,
|
|
131
|
+
{
|
|
132
|
+
"method": selection_method,
|
|
133
|
+
"index": layout_index,
|
|
134
|
+
"algorithm": type(template).__name__,
|
|
135
|
+
},
|
|
136
|
+
))
|
|
137
|
+
return _aggregate_cluster(
|
|
138
|
+
boundary, assignments, root_seed, n_turbines
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _generate_explicit_cluster(boundary, layouts, seed):
|
|
143
|
+
if isinstance(layouts, LayoutAlgorithm) or not isinstance(layouts, Sequence):
|
|
144
|
+
raise TypeError(
|
|
145
|
+
"BoundaryCluster requires a sequence of LayoutAlgorithm objects"
|
|
146
|
+
)
|
|
147
|
+
layouts = tuple(layouts)
|
|
148
|
+
if len(layouts) != len(boundary.boundaries):
|
|
149
|
+
raise ValueError(
|
|
150
|
+
f"BoundaryCluster has {len(boundary.boundaries)} farms but "
|
|
151
|
+
f"received {len(layouts)} layouts"
|
|
152
|
+
)
|
|
153
|
+
for index, algorithm in enumerate(layouts):
|
|
154
|
+
if not isinstance(algorithm, LayoutAlgorithm):
|
|
155
|
+
raise TypeError(
|
|
156
|
+
f"cluster layout {index} must be a LayoutAlgorithm, "
|
|
157
|
+
f"not {type(algorithm).__name__}"
|
|
158
|
+
)
|
|
159
|
+
if algorithm.n_turbines is None:
|
|
160
|
+
raise ValueError(f"cluster layout {index} must define n_turbines")
|
|
161
|
+
|
|
162
|
+
root_seed = resolve_seed(seed) if seed is not None else None
|
|
163
|
+
override_seeds = (
|
|
164
|
+
spawn_seeds(root_seed, len(layouts)) if root_seed is not None else None
|
|
165
|
+
)
|
|
166
|
+
assignments = []
|
|
167
|
+
for index, template in enumerate(layouts):
|
|
168
|
+
override = override_seeds[index] if override_seeds is not None else None
|
|
169
|
+
assignments.append((
|
|
170
|
+
template,
|
|
171
|
+
template.n_turbines,
|
|
172
|
+
override,
|
|
173
|
+
{
|
|
174
|
+
"method": "explicit",
|
|
175
|
+
"index": index,
|
|
176
|
+
"algorithm": type(template).__name__,
|
|
177
|
+
},
|
|
178
|
+
))
|
|
179
|
+
return _aggregate_cluster(
|
|
180
|
+
boundary,
|
|
181
|
+
assignments,
|
|
182
|
+
root_seed,
|
|
183
|
+
sum(algorithm.n_turbines for algorithm in layouts),
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _aggregate_cluster(boundary, assignments, root_seed, requested_n):
|
|
188
|
+
positions = []
|
|
189
|
+
guides = []
|
|
190
|
+
farms = []
|
|
191
|
+
start = 0
|
|
192
|
+
|
|
193
|
+
for index, (farm_boundary, assignment) in enumerate(
|
|
194
|
+
zip(boundary.boundaries, assignments)
|
|
195
|
+
):
|
|
196
|
+
template, count, override_seed, selection = assignment
|
|
197
|
+
count = int(count)
|
|
198
|
+
algorithm = copy(template)
|
|
199
|
+
algorithm.n_turbines = count
|
|
200
|
+
if override_seed is not None:
|
|
201
|
+
algorithm.seed = override_seed
|
|
202
|
+
|
|
203
|
+
child_provenance = None
|
|
204
|
+
child_seed = int(override_seed) if override_seed is not None else None
|
|
205
|
+
if count:
|
|
206
|
+
try:
|
|
207
|
+
result = algorithm.generate(farm_boundary)
|
|
208
|
+
except (TypeError, ValueError) as exc:
|
|
209
|
+
raise type(exc)(
|
|
210
|
+
f"Could not generate cluster farm {index}: {exc}"
|
|
211
|
+
) from exc
|
|
212
|
+
positions.append(result.positions)
|
|
213
|
+
guides.append(result.guide)
|
|
214
|
+
child_provenance = result.provenance
|
|
215
|
+
child_seed = child_provenance.get("seed")
|
|
216
|
+
|
|
217
|
+
stop = start + count
|
|
218
|
+
farms.append({
|
|
219
|
+
"farm_index": index,
|
|
220
|
+
"area": float(farm_boundary.area),
|
|
221
|
+
"n_turbines": count,
|
|
222
|
+
"start": start,
|
|
223
|
+
"stop": stop,
|
|
224
|
+
"seed": child_seed,
|
|
225
|
+
"selection": selection,
|
|
226
|
+
"layout": child_provenance,
|
|
227
|
+
})
|
|
228
|
+
start = stop
|
|
229
|
+
|
|
230
|
+
combined = np.concatenate(positions) if positions else np.empty((0, 2))
|
|
231
|
+
return Farm(
|
|
232
|
+
combined,
|
|
233
|
+
boundary,
|
|
234
|
+
{
|
|
235
|
+
"kind": "cluster",
|
|
236
|
+
"seed": int(root_seed) if root_seed is not None else None,
|
|
237
|
+
"requested_n": int(requested_n),
|
|
238
|
+
"achieved_n": int(len(combined)),
|
|
239
|
+
"farms": farms,
|
|
240
|
+
"modifiers": [],
|
|
241
|
+
},
|
|
242
|
+
guide=_combine_guides(guides),
|
|
243
|
+
)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Boundary geometries for wind farms.
|
|
2
|
+
|
|
3
|
+
Concrete boundaries subclass :class:`Boundary` and return a Shapely
|
|
4
|
+
``Polygon`` / ``MultiPolygon``. Extras (shipping channels, exclusion zones) are
|
|
5
|
+
:class:`BoundaryModifier` objects applied via :class:`CompositeBoundary`, so
|
|
6
|
+
they compose with any boundary. :class:`BoundaryCluster` explicitly groups
|
|
7
|
+
boundaries belonging to separately configured farms.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from .base import Boundary
|
|
13
|
+
from .composites import BoundaryCluster, CompositeBoundary
|
|
14
|
+
from .hull import ConvexHull
|
|
15
|
+
from .modifiers import BoundaryModifier, ExclusionZones, ShippingChannels
|
|
16
|
+
from .primitives import Annulus, Circle, Ellipse, Rectangle, RegularPolygon, Square
|
|
17
|
+
from .radial import RadialPolygon, RandomRadialPolygon, SmoothBlob
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"Boundary",
|
|
21
|
+
"BoundaryCluster",
|
|
22
|
+
"CompositeBoundary",
|
|
23
|
+
"Rectangle",
|
|
24
|
+
"Square",
|
|
25
|
+
"RegularPolygon",
|
|
26
|
+
"Circle",
|
|
27
|
+
"Ellipse",
|
|
28
|
+
"Annulus",
|
|
29
|
+
"RadialPolygon",
|
|
30
|
+
"RandomRadialPolygon",
|
|
31
|
+
"SmoothBlob",
|
|
32
|
+
"ConvexHull",
|
|
33
|
+
"BoundaryModifier",
|
|
34
|
+
"ShippingChannels",
|
|
35
|
+
"ExclusionZones",
|
|
36
|
+
]
|