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)