Sampling#
A Sampler describes the scenarios you want.
sample() returns n of them as a tuple and
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 Describing a population of farms, Sampling with a density and
Drawing, streaming and replaying a population 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 |
|
that value |
a |
|
a uniform draw, inclusive for whole numbers |
a frozen |
|
a draw from it |
a callable |
|
what it returns for the scenario’s |
a |
|
one option, by its |
a class or |
|
an instance built from its drawn arguments |
|
|
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.
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.
|
unit |
meaning |
|---|---|---|
|
m² |
area over count |
|
W/m² |
installed capacity over area, which needs turbines of one rated power |
|
m |
no pair closer than it, |
|
m |
the mean nearest-neighbor distance, |
|
m |
no turbine farther than it from its nearest neighbor, |
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 requirement instead.
given |
derived |
how |
|---|---|---|
|
density |
whatever that count in that area produces |
|
area |
the farm is sized so the spacing is met |
|
|
the layout fills the boundary with the most turbines that hold a minimum or mean, or the fewest that hold a maximum |
|
|
the area from the count |
|
|
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 |
measured on the farm |
|---|---|---|
|
|
|
|
|
|
|
the same names |
|
|
|
|
|
|
|
|
|
|
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.
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 |
|---|---|---|
|
every nearest-neighbor distance in meters, so the low end keeps pairs apart and the high end leaves no turbine alone |
|
|
the turbine count, when an area and a density or a drawn range set it |
|
|
the installed capacity in watts, which needs turbines |
|
|
the share of the boundary inside the convex hull of the turbines, about 0.85 for a full grid |
|
|
how many times as far the turbine positions spread along their long axis as across it |
|
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.
|
a scenario that raises |
|---|---|
|
stops the draw with the error, the default |
|
is drawn again, up to |
|
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 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. Multipart boundaries and clusters 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 Link is a dial between 0 and 1 that several values
read, so they move together, as Describing a population of farms 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"].
|
A reproducible description of wind farm scenarios to draw from. |
|
A weighted choice among values, made separately for each index drawn. |
|
A dial that several sampled values read together. |
|
A value the sampler passes through unchanged rather than drawing from. |
|
A spacing given in rotor diameters, as |
- class oowl.Sampler(boundary=None, layout=None, resource=None, *, configuration=None, n_turbines=None, area=None, density=None, turbines=None, surface=None, domain=None, gap=0.0, n_farms=None, n_sites=None, requirements=None, factory=None, on_error='raise', max_attempts=10, seed=None)[source]#
A reproducible description of wind farm scenarios to draw from.
Building the sampler checks every fixed value, range and component type, and the arguments of every recipe, so most mistakes raise before anything is drawn. What a recipe, class or callable returns is known only when it runs, so it is checked as each scenario is drawn. Any component may be a fixed value or a specification, meaning a
(low, high)range, a frozenscipy.statsdistribution such asstats.loguniform(0.03, 1.0), a callable taking oneGenerator, aChoice, a class, or a(constructor, kwargs)recipe. An argument whose name ends in_range, and the points and vectorscenterandgradient, take their pair as given, and a pair of ranges draws each end or coordinate. The points ofexterior,holes,sitesandcoordinates, given as numbers, are taken as given too. A distribution is drawn once per scenario, so wrap it inLiteralfor an argument that takes the distribution itself.Literalpasses any value through unresolved. A range or choice read from aLinkmoves with the other values on that link.boundaryandlayoutdescribe one farm.configurationis a mapping specification that varies boundary, layout, count, area and density together. It is resolved once per farm, or once per member of a cluster or region.n_turbinesis the turbine count andareathe farm’s area in square meters.densityis one mapping naming the farm’s density,{"area_per_turbine": square meters},{"power_density": watts per square meter}, which needs turbines of one rated power,{"minimum_spacing": meters}, the closest pair,{"mean_spacing": meters}, the mean nearest-neighbor distance, or{"maximum_spacing": meters}, the loneliest turbine’s nearest-neighbor distance. Its value is any specification, and a spacing may also beRotorDiametersof the farm’s largest rotor. Give at most two of count, area and density and the third follows. A boundary given its own area fixes the area asareadoes. A layout’s own count or density stands in for the sampler’s.requirementsonly checks each farm built and sizes nothing. Each key names a farm measure and maps to the fixed(low, high)range it must lie in,Noneleaving an end open."spacing"holds every turbine’s distance to its nearest neighbor in meters, an end also given asRotorDiameters, so its low end keeps pairs apart even in a farm of fixed count and area, and its high end leaves no turbine alone."n_turbines"holds a count an area and a density derive,"capacity"the installed capacity in watts,"coverage"the share of the boundary inside the convex hull of the turbines and"aspect_ratio"how many times as far the turbine positions spread along their long axis as across it. A farm that misses one raises aValueError, whichon_errorhandles, and each records the ranges it met inmetadata["sample"]["requirements"].resource,turbinesandsurfaceattach to every scenario. In a cluster turbines are drawn per farm, one resource is shared by the region and one surface covers the whole scenario.By default a scenario is one
Farm. Withoutboundary,layoutandconfigurationit is the drawnresourceorturbinesalone, the same one a farm scenario of that index carries.n_farmsmakes it aClusterof adjacent farms with clearancegapin meters.domainmakes it a region scattered with sites described byboundary, added at leastgapapart untiln_sitesare placed. A site that finds no room is replaced by another, and 12 in a row that find none end the fill. Each farm records its scenario inmetadata["sample"]["index"]and, in a region, its site inmetadata["group"].factoryis an alternativef(rng) -> Farmthat excludes the declarative fields. It draws every random value fromrng, seeds included, asL.Grid(n, seed=rng), since an unseeded component inside it draws a new seed each call.on_error="resample"draws a scenario that raises aValueErroragain, up tomax_attemptsattempts in all, redrawing its random values and random choices, while balanced and seeded choices keep theirs. A layout given its own seed draws each retry from a seed derived from it and the attempt, so the retries replay, and the farm records the seed it was drawn from.on_error="skip"leaves out a scenario that raises aValueError, sosample(n)andstream(n)hold the scenarios of indicesstarttostart + n - 1that could be built, each recording its index, anditertools.islice(sampler.stream(), n)givesnof them. After 1,000 scenarios in a row are skipped it raises aValueError, as a description nothing meets would otherwise stream forever.seedis the root seed, and a generator orNoneis resolved to an integer once.- Parameters:
- sample(n=1, *, start=0)[source]#
Draw
nscenarios starting at indexstartand return a tuple.Each scenario is a
Farm, or aClusterwhen the sampler hasn_farms, adomainor one boundary per member. A sampler without a boundary and layout draws itsresource, aWindResourceorDimitrov, or else itsturbines, oneTurbineor a tuple of them. Scenarioiis the same however many are drawn, so repeated calls give the same result andstartshards a population.
- class oowl.sampling.Choice(values, probabilities=None, *, selection='random', seed=None)[source]#
A weighted choice among values, made separately for each index drawn.
valuesmay hold plain objects, constructors, recipes(constructor, {kwargs}), or mappings of farm configuration overrides.probabilitiesdefaults to uniform.selection="random"draws each index independently, and"balanced"follows a schedule that keeps every prefix of a population close to the probabilities.seedfixes the selection whatever sampler draws it, andNonefollows the sampler.- Parameters:
- class oowl.sampling.Link(*, follows=None, correlation=None, per='scenario', selection='random')[source]#
A dial that several sampled values read together.
link(spec)returns a linked version of a(low, high)range or aChoice, usable wherever a range or a choice is. Values linked to one dial move together. A reversed range moves the other way.A scenario that
on_error="resample"retries turns its random dials again, as it redraws its other random values, while a balanced dial keeps its place in the population. A farm retried inside a cluster keeps the cluster’s dials, which turn again when the whole cluster is retried.- Parameters:
follows (Link | None) – Another link this dial follows, or
None.correlation (float | None) – Rank correlation with
follows, in [-1, 1]. Requiresfollows. Defaults to 1, so the two dials turn together.per (Literal['scenario', 'farm']) –
"scenario"turns the dial once per scenario, so every farm of a cluster reads the same value."farm"turns it once per farm. A value one cluster shares, its resource, surface, domain and farm or site count, has no farm to read the dial for, so it cannot read a per-farm link, and a per-scenario link cannot follow one.selection (Literal['random', 'balanced']) –
"random"draws the dial independently per scenario."balanced"spreads it evenly over scenarios, so linked choices meet their probabilities and linked ranges are evenly covered.
- class oowl.sampling.Literal(value)[source]#
A value the sampler passes through unchanged rather than drawing from.
- Parameters:
value (Any)
- class oowl.sampling.RotorDiameters(diameters)[source]#
A spacing given in rotor diameters, as
RotorDiameters(7)for 7D.A sampler turns it into meters with each scenario’s turbines, the largest rotor diameter among them, so a farm of a drawn turbine keeps its spacing in diameters.
diametersis any scalar specification, such as a number or a(low, high)range.