Optimizers¶
While evaluating samples using either a cost function or a model and specification is important, the generation of the samples to evaluate is equally important. \(\Psi\)-TaLiRo captures the sample generation behavior as either the optimizer or sampler interfaces, which are expected to select samples for evaluation and accept cost values to inform the selection of further samples. In principle, sample selection can be entirely independent of the sample evaluation process but in practice it is valuable to try and minimize/maximize the cost value, which generally represents some kind of quality property of the sample.
Objective Function¶
The objective function is the interface through which an optimizer can evaluate generated samples
into cost values. This behavior is encapsulated by the ObjFunc
interface, which exposes the eval_sample() and
eval_samples() methods for single and batch sample evaluation
respectively. If the parallelization option is configured in the options, then
the eval_samples() method will evaluate the sample batch in parallel according to the number of
threads selected rather than in sequence.
Optimizer¶
An Optimizer is a sample generator with internal iteration. This means that the optimizer, and
thus the implementer, is responsible for generating samples and calling the objective function to
produce cost values
Base Class¶
You can implement an optimizer by inheriting from the Optimizer
class, which has one required method optimize that accepts
a objective function and a Params value
and returns an arbitrary value representing the result of the optimization attempt. The
Optimizer[C, R] class is parameterized by two type variables: C represents the cost values
the optimizer expects to operate on, and the R type variable represents the type of the
optimization result value.
from dataclasses import dataclass
from random import Random
from staliro.optimizers import ObjFunc, Optimizer
@dataclass()
class OptResult:
average: float
class Opt(Optimizer[float, OptResult]):
def optimize(self, func: ObjFunc[float], params: Optimizer.Params) -> OptResult:
rng = Random(params.seed)
total = 0
for _ in range(params.budget):
sample = [rng.uniform(bound[0], bound[1]) for bound in params.bounds]
cost = func.eval_sample(sample)
total += cost
return OptResult(total / params.budget)
Decorator¶
Optimizers can also be quickly implemented by decorating a function with the
optimizer() decorator.
from staliro.optimizers import ObjFunc, Optimizer, optimizer
@optimizer()
def opt(func: ObjFunc[float], params: Optimizer.Params) -> None:
...
Sampler¶
Warning
This API is not yet available and may be changed before final release.
The opposite of an optimizer is the Sampler which is a sample generator
with external iteration. This means that instead of “pushing” samples using the objective function,
the Sampler accepts requests for samples and is returned cost values.
Base Class¶
The Sampler base class can be inherited from, which has a single
required method called sample, which accepts an optional
cost value and returns a value that can be converted into a sample. The
Sampler[C] class is parameterized by a single type variable C which represents the type of
cost value the sampler expects to receive.
from staliro import SampleLike, optimizers
class Sampler(optimizers.Sampler[float]):
def sample(self, cost: float | None = None) -> SampleLike:
...
Decorator¶
It is also possible to create a Sampler using the sampler()
decorator function. However, instead of decorating a plain python function you must apply this
decorator to a generator function that accepts a
Params value, like so:
from collections.abc import Generator
from random import Random
from staliro import SampleLike, optimizers
@optimizers.sampler()
def sampler(params: optimizers.Optimizer.Params) -> Generator[SampleLike, float, None]:
rng = Random(params.seed)
for _ in range(params.budget):
sample = [rng.uniform(bound[0], bound[1]) for bound in params.bounds]
cost = yield sample
Uniform Random¶
The UniformRandom optimizer uniformly samples the input space,
ignoring the cost value produced by each sample evaluation when selecting the next sample. The
constructor takes an optional min_cost parameter which creates a termination condition that
will stop the optimization run if a cost value is encountered that is \(\leq min\_cost\). The
type of the cost value must be comparable if a min_cost is provided.
from staliro.optimizers import UniformRandom
opt = UniformRandom() # Any cost type
opt = UniformRandom(min_cost=0.0) # float cost type
Simulated Annealing¶
The DualAnnealing optimizer provides an implementation of
general simulated annealing using the implementation of dual annealing provided by the SciPy
library. This optimizer expects float cost values since under the hood the value vectors are
represented using Numpy ndarray values. You can optionally provide a min_cost when
during construction like the UniformRandom optimizer, which will terminate
the optimization attempt if a cost value is found that is \(\leq min\_cost\).
from staliro.optimizers import DualAnnealing
opt = DualAnnealing()
opt = DualAnnealing(min_cost=0.0)