# Sampling

A {class}`~oowl.Sampler` describes the scenarios you want.
{meth}`~oowl.Sampler.sample` returns `n` of them as a tuple and
{meth}`~oowl.Sampler.stream` yields them, both from index `start`. Building the
sampler checks every fixed value, range, component type and recipe argument,
and what a recipe, class or callable returns is checked as each scenario is
drawn. Drawing twice gives the same scenarios. This page states the rules,
and {doc}`../sampling/populations`, {doc}`../sampling/spacing` and
{doc}`../sampling/drawing` work through examples.

## Specifications

Any value may be given as one of these forms, and the forms nest.

| form | example | each scenario takes |
| --- | --- | --- |
| a fixed value | `n_turbines=60` | that value |
| a `(low, high)` range | `area=(30e6, 90e6)` | a uniform draw, inclusive for whole numbers |
| a frozen `scipy.stats` distribution | `stats.loguniform(0.03, 1.0)` | a draw from it |
| a callable | `lambda rng: rng.integers(40, 80)` | what it returns for the scenario's `rng` |
| a `Choice` | `SP.Choice([L.Grid, L.PoissonDisc])` | one option, by its `probabilities` or a prefix-stable schedule with `selection="balanced"` |
| a class or `(constructor, kwargs)` recipe | `(B.SmoothBlob, {"roughness": (0.05, 0.5)})` | an instance built from its drawn arguments |
| `Literal(value)` | `SP.Literal(stats.norm())` | the value unchanged |

An argument whose name ends in `_range`, such as `elevation_range`, and the
points and vectors `center` and `gradient` take their pair as given. A pair of
ranges such as `((0, 10), (90, 110))` draws each end or coordinate.
`configuration` overrides several values together, its `"density"` replacing
the sampler's whole, and may be a mapping, a `Choice` of mappings or a
callable, so a `Choice` of configurations can vary which density a scenario
takes. `factory(rng) -> Farm` replaces generation
entirely and draws every random value from `rng`, seeds included.

```python
import oowl
from oowl import boundaries as B, layouts as L, sampling as SP

population = oowl.Sampler(
    boundary=SP.Choice([B.OffshorePolygon, (B.SmoothBlob, {"roughness": (0.05, 0.5)})]),
    layout=SP.Choice([L.Grid, L.PoissonDisc], probabilities=[0.7, 0.3]),
    n_turbines=(40, 120),
    area=(30e6, 90e6),
    seed=7,
)
farms = population.sample(100)
```

Scenario *i* is the same whether 1 or 1000 are drawn, so `sample(n, start=i)`
shards a population. A component given its own `seed` stays the same in every
scenario, and an unseeded one is drawn again from each scenario's seed.

## Sizing

A farm's geometry links three quantities, the turbine count `n_turbines`, the
`area` in square meters and its `density`. The density is one slot, so it is
one mapping whose key names the measure, as in
`density={"minimum_spacing": 800.0}`, and whose value is any specification.

| `density` key | unit | meaning |
| --- | --- | --- |
| `area_per_turbine` | m² | area over count |
| `power_density` | W/m² | installed capacity over area, which needs turbines of one rated power |
| `minimum_spacing` | m | no pair closer than it, `farm.minimum_spacing()` |
| `mean_spacing` | m | the mean nearest-neighbor distance, `farm.mean_spacing()` |
| `maximum_spacing` | m | no turbine farther than it from its nearest neighbor, `farm.maximum_spacing()` |

Give any two and the third follows. All three over-constrain the farm and are
refused, so a spacing a farm of fixed count and area must clear is a
{ref}`requirement <sampling-requirements>` instead.

| given | derived | how |
| --- | --- | --- |
| `n_turbines` + `area` | density | whatever that count in that area produces |
| `n_turbines` + a spacing | area | the farm is sized so the spacing is met |
| `area` + a spacing | `n_turbines` | the layout fills the boundary with the most turbines that hold a minimum or mean, or the fewest that hold a maximum |
| `n_turbines` + `area_per_turbine` or `power_density` | `area` | the area from the count |
| `area` + `area_per_turbine` or `power_density` | `n_turbines` | the count from the area |

A boundary given its own area, as `B.Circle(area=6e7)`, fixes the area as
`area` does, while one built without, as `B.Circle()`, takes whatever size the
other two give. A `Polygon` keeps the area of its coordinates and a `Union`
that of its members. A layout's own count or density stands in for the
sampler's. A spacing may be `SP.RotorDiameters(7)`, which each scenario turns
into meters with the largest rotor among its turbines.

`Uniform` and `Sobol` place turbines at random, so a minimum spacing sizes
them by their single closest pair, and the area or count it gives varies
widely between seeds, where a mean spacing is steady. A spacing is met by scaling the
farm, or, for a boundary with a setback, gap or exclusion zone in meters, by
redrawing it at other sizes until the spacing is met to a millionth, a minimum
never from below and a maximum never from above. A layout that jumps as its
site grows can stop short of that, and `closest_pair` records the spacing
reached. With a minimum spacing, `PoissonDisc`, `Grid`, `Circular`,
`Contour`, `Sunflower`, `Perimeter`, `BoundaryGrid` and `Mixture` fill the
whole boundary in one pass, so a partitioned boundary keeps the spacing across
its cells, and the other layouts fill each cell on its own. Layout modifiers
run once on the filled farm, `Relaxation` and `PositionNoise` held to a
minimum spacing. A spacing that fits fewer than two turbines gives a farm of
the one that fits, recorded as `metadata["fits_one_turbine"]`.

| requested | recorded in `metadata["sample"]` | measured on the farm |
| --- | --- | --- |
| `n_turbines` | `n_turbines` | `farm.n_turbines` |
| `area` | `area`, of the realized boundary | `farm.boundary.area` |
| `area_per_turbine`, `power_density` | the same names | |
| `minimum_spacing` | `minimum_spacing`, and the achieved `closest_pair` | `farm.minimum_spacing()` |
| `mean_spacing` | `mean_spacing` | `farm.mean_spacing()` |
| `maximum_spacing` | `maximum_spacing` | `farm.maximum_spacing()` |

(sampling-requirements)=
## Requirements

`requirements` only checks each farm built and sizes nothing. Each key names a
farm measure, and its value is the fixed `(low, high)` range the measure must
lie in, `None` leaving an end open.

```python
import itertools

sampler = oowl.Sampler(
    B.Circle(area=1.7e8),
    L.PoissonDisc(n_turbines=79),
    requirements={"spacing": (1_200.0, None), "coverage": (0.7, None)},
    on_error="skip",
    seed=1,
)
farms = list(itertools.islice(sampler.stream(), 100))
```

| requirement | the range holds | measured by |
| --- | --- | --- |
| `spacing` | every nearest-neighbor distance in meters, so the low end keeps pairs apart and the high end leaves no turbine alone | `farm.minimum_spacing()`, `farm.maximum_spacing()` |
| `n_turbines` | the turbine count, when an area and a density or a drawn range set it | `farm.n_turbines` |
| `capacity` | the installed capacity in watts, which needs turbines | `farm.capacity()` |
| `coverage` | the share of the boundary inside the convex hull of the turbines, about 0.85 for a full grid | `farm.coverage()` |
| `aspect_ratio` | how many times as far the turbine positions spread along their long axis as across it | `metrics.aspect_ratio(farm)` |

A spacing end may be `SP.RotorDiameters` of a number. Every farm of a cluster
or region is checked, and each records the ranges it met in
`metadata["sample"]["requirements"]`. A farm of one turbine has no spacing to
measure, and turbines in one line fail any aspect ratio. A range that runs
downward or can never vary is refused when the sampler is built.

## Scenarios that fail

A scenario that cannot be built or misses a requirement raises a `ValueError`,
and `on_error` decides what follows. A wrong kind of input raises a
`TypeError`, which no mode retries or skips.

| `on_error` | a scenario that raises |
| --- | --- |
| `"raise"` | stops the draw with the error, the default |
| `"resample"` | is drawn again, up to `max_attempts` attempts in all, the earlier errors in `metadata["sample"]["errors"]` |
| `"skip"` | is left out, while the others keep their indices |

A layout given its own seed draws each retry from a seed derived from it, and
`farm.metadata["layout"]["seed"]` holds the one that built the farm. With
`"skip"`, `sample(n)` holds the scenarios of indices `start` to `start + n - 1`
that could be built, an endless `stream()` yields only those, and 1,000 skipped
in a row raise rather than stream forever.

## Clusters and regions

`n_farms` makes each scenario a {class}`~oowl.Cluster`, even `n_farms=1`, so a
population never mixes farms and clusters, and `domain=` makes it a region of
up to `n_sites` sites. `gap` is the clearance farms keep as they are placed, so
cluster members are packed that far apart and region sites are scattered at
least that far apart. A site that finds no room is replaced by another, and 12
in a row that find none end the fill. Top-level sizing values are refused for
both, so size each member in its boundary or layout recipe or in a
`configuration`. {doc}`../layouts/multipart` shows both, neighboring farms and
sites scattered across a domain.

## Sampling without a farm

A sampler given only a `resource` or `turbines`, as
`oowl.Sampler(resource=(R.WindResource, {"mean_wind_speed": (6.0, 11.0)}))`,
draws that component, the same one a farm of that index would carry.

## Linking values

A {class}`~oowl.sampling.Link` is a dial between 0 and 1 that several values
read, so they move together, as {doc}`../sampling/populations` shows.
`link((first, second))` runs from `first` to `second` as the dial rises, and
`scale="log"`, `("power", p)` or a rising function change how it maps.
`link(SP.Choice(...))` splits the dial into bands sized by the probabilities.
A link can follow another at a rank correlation. The dial turns once per
scenario, or once per farm with `per="farm"`, and `selection="balanced"`
spreads it evenly over the population. A value every farm of a cluster shares,
such as its resource or surface, reads a per-scenario link. Each scenario
records its dials in `metadata["sample"]["links"]`.

```{eval-rst}
.. autosummary::

   oowl.Sampler
   oowl.sampling.Choice
   oowl.sampling.Link
   oowl.sampling.Literal
   oowl.sampling.RotorDiameters
```

```{eval-rst}
.. autoclass:: oowl.Sampler
   :members:

.. autoclass:: oowl.sampling.Choice
   :members:

.. autoclass:: oowl.sampling.Link
   :members:

.. autoclass:: oowl.sampling.Literal
   :members:

.. autoclass:: oowl.sampling.RotorDiameters
   :members:
```
