Quality-Diversity
bbobax.qd
Quality-Diversity on top of a BBOB problem.
The Quality-Diversity extension is bbobax's own; COCO has no descriptor notion.
A descriptor is orthogonal to a function: any of the 24 can be paired with any descriptor, so a QD problem is composed rather than subclassed --
problem = QDProblem(Rastrigin(num_dims=10), RandomProjection(descriptor_size=2))
Subclassing would need one QD class per function, and the combinatorics get
worse with every descriptor family added. Composition also keeps each half
testable on its own, and lets a descriptor be reused across dimensions: the
descriptor draws its instance data from the problem it is handed, so the same
object serves a whole meta-learning loop over DIMENSIONS.
Descriptor space is [-1, 1]^k by construction, the way COCO's search
space is [-5, 5]^D: every descriptor bbobax ships lands in that box exactly,
for any solution inside the search box. The linear families achieve it by
dividing each projection row by its tight bound over the box; transform_osz
and transform_asy map [-1, 1] onto itself, quantization stays inside by
construction, and a cosine is born there. Solutions outside the search box
can exceed the bounds for the linear families -- the same status BBOB gives
the boundary penalty. The benchmark knowing the bounds does not mean an
algorithm must be told: withholding them is how a bounds-free method (novelty
search and its descendants) is honestly compared against a bounds-given one
(MAP-Elites) on identical problems -- an information regime, exactly as no
algorithm reads params.x_opt.
The families are organized the way BBOB organizes its functions: each isolates one named phenomenon of real descriptor maps, built by composing the same transformations the functions themselves are built from.
RandomProjection-- smooth, linear, Lipschitz: the controlled baseline.IrregularProjection-- osz/asy on the projection: non-uniform sensitivity and asymmetry, BBOB's own irregularities applied to behavior.QuantizedProjection-- level sets on the projection: a small step inxcrosses a level and the descriptor jumps, the contact-switch phenomenon.FourierProjection-- random Fourier features: smooth but arbitrarily sensitive, with the Lipschitz constant as an explicit dial.SubsetProjection-- the descriptor reads a few coordinates while fitness depends on all of them: behavior-space redundancy.AlignedProjection-- rows interpolated toward the instance's own rotation: the one coupled family, dialing how directly behavior trades against fitness structure.
Reading anything from params beyond shapes couples a descriptor to the
landscape; every family above except AlignedProjection deliberately does
not.
One corner of the space has exact ground truth: for Sphere under any
linear descriptor, the best fitness achievable at a descriptor value is a
closed-form projection -- sphere_descriptor_optimum. That is something no
other QD benchmark provides: the per-cell optimum is exactly what QD-score
normalization has always lacked (evaluate it at cell centers and the archive
has an absolute reference), and it exists here because the fidelity work made
x_opt the true argmin.
AlignedProjection
Bases: RandomProjection
A projection interpolated toward the landscape's own axes.
The one coupled family: its rows are drawn between random unit
directions and the first rows of the instance's rotation params.r -- the
axes along which the function's conditioning acts. alignment = 0 is a
random projection; alignment = 1 describes the solution in the
landscape's own rotated coordinates, so moving through behavior space is
moving directly along the function's structure. The dial sets how directly
behavior trades against fitness.
Only meaningful when the problem samples a rotation; with
sample_rotation=False the landscape axes are the coordinate axes.
Requires descriptor_size <= num_dims, since the rotation has only
num_dims rows.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
descriptor_size
|
int
|
Dimensionality of the descriptor space. |
2
|
alignment
|
float
|
In |
1.0
|
Source code in src/bbobax/qd.py
__init__(descriptor_size=2, alignment=1.0)
Initialize the descriptor.
Source code in src/bbobax/qd.py
sample(key, problem, params)
Sample the instance's projection: random rows mixed toward params.r.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
Array
|
JAX random key. |
required |
problem
|
BBOBProblem
|
The problem being described. |
required |
params
|
BBOBParams
|
The problem's instance; its rotation |
required |
Returns:
| Type | Description |
|---|---|
Array
|
A |
Source code in src/bbobax/qd.py
Descriptor
Bases: Protocol
Maps a solution to a point in descriptor space.
The same shape of contract as a problem: sample draws the instance data
the descriptor needs, evaluate scores one solution -- keyed, so a
descriptor may be stochastic, exactly as a problem may be noisy -- and
callers vmap it to cover a batch.
sample receives the problem and its freshly drawn instance. Shapes and
the search box come from the problem; reading anything from params beyond
that couples the descriptor to the landscape, which is a declared design
choice rather than the default.
Where a family validates. A constraint on the descriptor alone is
checked in __init__; a constraint that involves the problem -- and so
cannot be known until sample is handed one -- is checked in sample,
which is therefore allowed to raise. Both SubsetProjection and
AlignedProjection do. This matters to a caller building a meta-learning
loop: a __init__ raise happens once at construction, while a sample
raise happens at trace time inside whatever jit or vmap encloses it.
Construct against the smallest num_dims the loop will cover and the
question never arises.
A protocol rather than a base class, for the reason BBOBProblem spells
out: descriptors share a contract, not an implementation, and satisfying
one should not require importing anything from bbobax.
Source code in src/bbobax/qd.py
evaluate(key, x, params)
Compute the descriptor of a solution.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
Array
|
JAX random key, for descriptors that measure stochastically. |
required |
x
|
Array
|
Input solution, shape |
required |
params
|
Any
|
Instance descriptor parameters. |
required |
Returns:
| Type | Description |
|---|---|
Array
|
The descriptor, shape |
Source code in src/bbobax/qd.py
sample(key, problem, params)
Sample this descriptor's instance data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
Array
|
JAX random key. |
required |
problem
|
BBOBProblem
|
The problem being described; its |
required |
params
|
BBOBParams
|
The problem's freshly drawn instance. Reading it couples the descriptor to the landscape. |
required |
Returns:
| Type | Description |
|---|---|
Any
|
The instance's descriptor parameters. |
Source code in src/bbobax/qd.py
FourierParams
FourierProjection
Bases: _Projection
Random Fourier features of the solution: smooth but arbitrarily sensitive.
Each component is cos(w_j . x + b_j) with Gaussian frequencies scaled by
bandwidth, so the descriptor is smooth everywhere yet its Lipschitz
constant grows linearly with the dial: at bandwidth = 1 the phase moves
through roughly one oscillation across the box, at bandwidth = 10 a
small step in x can carry the descriptor anywhere in its range. This is
the smooth version of "a small change in x is a large change in
behavior". Bounded in [-1, 1] by construction -- everywhere, not only
inside the box.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
descriptor_size
|
int
|
Dimensionality of the descriptor space. |
2
|
bandwidth
|
float
|
The sensitivity; the Lipschitz constant scales linearly with it. |
1.0
|
Source code in src/bbobax/qd.py
__init__(descriptor_size=2, bandwidth=1.0)
evaluate(key, x, params)
Compute the descriptor of a solution.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
Array
|
JAX random key; unused, this descriptor is deterministic. |
required |
x
|
Array
|
Input solution, shape |
required |
params
|
FourierParams
|
The instance's frequencies and phases. |
required |
Returns:
| Type | Description |
|---|---|
Array
|
The descriptor, shape |
Source code in src/bbobax/qd.py
sample(key, problem, params)
Sample the instance's frequencies and phases.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
Array
|
JAX random key. |
required |
problem
|
BBOBProblem
|
The problem being described. |
required |
params
|
BBOBParams
|
The problem's instance; not read. |
required |
Returns:
| Type | Description |
|---|---|
FourierParams
|
The instance's frequencies and phases. |
Source code in src/bbobax/qd.py
IrregularProjection
Bases: RandomProjection
The projection under BBOB's own irregularities: osz, then asy.
Both transformations map [-1, 1] onto itself -- osz fixes the endpoints
and is strictly monotone, asy only shrinks positive components -- so the
bounds survive the composition exactly. What changes is the metric: the
descriptor's sensitivity to x varies over the space and is asymmetric
around the origin, the same distortions the functions themselves apply.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
descriptor_size
|
int
|
Dimensionality of the descriptor space; at least 2, because the asymmetry schedule spans the components exactly as it spans the dimensions in BBOB. |
2
|
beta
|
float
|
The asymmetry, the paper's beta. |
0.2
|
Source code in src/bbobax/qd.py
__init__(descriptor_size=2, beta=0.2)
Initialize the descriptor.
Source code in src/bbobax/qd.py
evaluate(key, x, params)
Compute the descriptor: the projection, made irregular.
QDEval
QDParams
Bases: PyTreeNode
One sampled instance of a Quality-Diversity problem.
Composed, not inherited: a QD problem is a problem paired with a
descriptor, and each half draws its own instance data. The two fields are
named for the two halves of QDProblem, so the parameters read the same
way the object does. descriptor is whatever that descriptor's sample
returns -- a projection matrix for RandomProjection, something else for a
descriptor that needs more.
Source code in src/bbobax/qd.py
QDProblem
A BBOB problem paired with a descriptor.
The pair is the problem: sample draws an instance of each half, and
evaluate returns the fitness and the descriptor together.
Source code in src/bbobax/qd.py
589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 | |
descriptor_range
property
Exact bounds of the descriptor, per component, inside the box.
Ground truth the benchmark always has; whether an algorithm is told is
the experiment's choice, exactly as no algorithm reads x_opt.
descriptor_size
property
The dimension of the descriptor space.
name
property
The underlying function's name.
num_dims
property
The dimension of the search space.
x_range
property
The range of input variables.
__init__(problem, descriptor)
Initialize the Quality-Diversity problem.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
problem
|
BBOBProblem
|
The function being optimized. |
required |
descriptor
|
Descriptor
|
What makes two equally-fit solutions different. |
required |
Source code in src/bbobax/qd.py
evaluate(key, x, params)
Evaluate the fitness and the descriptor of a solution.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
Array
|
JAX random key, consumed by the noise model and by a stochastic descriptor. |
required |
x
|
Array
|
Input solution, shape |
required |
params
|
QDParams
|
Instance parameters. |
required |
Returns:
| Type | Description |
|---|---|
QDEval
|
The evaluation results. |
Source code in src/bbobax/qd.py
sample(key)
Sample an instance of this problem: the function's, and the descriptor's.
The problem's instance is drawn first and handed to the descriptor, so a coupled descriptor can read the landscape it describes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
Array
|
JAX random key. |
required |
Returns:
| Type | Description |
|---|---|
QDParams
|
The instance's parameters. |
Source code in src/bbobax/qd.py
sample_x(key)
Sample a random solution.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
Array
|
JAX random key. |
required |
Returns:
| Type | Description |
|---|---|
Array
|
Random solution within |
QuantizedProjection
Bases: RandomProjection
The projection snapped to level centers: a discontinuous descriptor.
A small step in x that crosses a level boundary jumps the descriptor by
a whole level -- the contact-switch phenomenon of real behavior maps,
built the way StepEllipsoidal builds its plateaus. Values are the
num_levels level centers, strictly inside [-1, 1].
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
descriptor_size
|
int
|
Dimensionality of the descriptor space. |
2
|
num_levels
|
int
|
Levels per component; the discontinuity size is
|
10
|
Source code in src/bbobax/qd.py
__init__(descriptor_size=2, num_levels=10)
Initialize the descriptor.
Source code in src/bbobax/qd.py
evaluate(key, x, params)
Compute the descriptor: the projection, quantized to level centers.
Source code in src/bbobax/qd.py
RandomProjection
Bases: _Projection
A Gaussian random projection of the solution, normalized to the box.
Rows are Gaussian directions, and each row is divided by its tight bound
over the search box (sum_i |P_ji| * max|x|), so the descriptor lands in
[-1, 1] exactly and the bound is achieved at a box corner.
Exact is not typical. The bound is attained only at one aligned box
corner; a random box point projects to a value whose standard deviation is
on the order of 1 / sqrt(num_dims) of it (about a quarter at D = 10,
shrinking with dimension -- measure concentration). A tessellation over
the full [-1, 1]^k therefore spends most of its cells on rarely reached
corners: in the descriptor-family study, MAP-Elites filled 32 of 128 CVT
cells under this family at D = 10 after 512 generations, while
FourierProjection -- whose cosine concentrates mass toward the box faces
-- filled all 128. Bounds are ground truth; reachable mass is a property
of the family.
This descriptor is smooth and Lipschitz: a small change in x gives a
small change in the descriptor, and the reachable set is a linear image of
the box. Real-world QD is frequently neither, so treat it as the baseline
rather than as representative -- the other families each break one of
these properties deliberately.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
descriptor_size
|
int
|
Dimensionality of the descriptor space. |
2
|
Source code in src/bbobax/qd.py
evaluate(key, x, params)
Compute the descriptor of a solution: the projection of x.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
Array
|
JAX random key; unused, this descriptor is deterministic. |
required |
x
|
Array
|
Input solution, shape |
required |
params
|
Array
|
The instance's projection matrix. |
required |
Returns:
| Type | Description |
|---|---|
Array
|
The descriptor, shape |
Source code in src/bbobax/qd.py
sample(key, problem, params)
Sample the instance's projection matrix, normalized to the box.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
Array
|
JAX random key. |
required |
problem
|
BBOBProblem
|
The problem being described. |
required |
params
|
BBOBParams
|
The problem's instance; not read -- this family is deliberately uncoupled from the landscape. |
required |
Returns:
| Type | Description |
|---|---|
Array
|
A |
Source code in src/bbobax/qd.py
SubsetParams
SubsetProjection
Bases: _Projection
A projection that reads only a few coordinates: redundant behavior.
The descriptor depends on subset_size randomly chosen coordinates while
the fitness depends on all of them, so most of the search space moves
without moving the behavior -- the genotype-to-behavior collapse of real
QD, in its simplest form. Moving any coordinate outside the subset leaves
the descriptor exactly unchanged.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
descriptor_size
|
int
|
Dimensionality of the descriptor space. |
2
|
subset_size
|
int | None
|
How many coordinates the descriptor reads; defaults to
|
None
|
Source code in src/bbobax/qd.py
__init__(descriptor_size=2, subset_size=None)
Initialize the descriptor.
Source code in src/bbobax/qd.py
evaluate(key, x, params)
Compute the descriptor: the projection of the subset's coordinates.
sample(key, problem, params)
Sample the instance's coordinate subset and its projection.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
Array
|
JAX random key. |
required |
problem
|
BBOBProblem
|
The problem being described; |
required |
params
|
BBOBParams
|
The problem's instance; not read. |
required |
Returns:
| Type | Description |
|---|---|
SubsetParams
|
The instance's subset and projection matrix. |
Source code in src/bbobax/qd.py
sphere_descriptor_optimum(d, matrix, params)
Compute the exact best fitness at d, for Sphere under a linear map.
Ground truth in closed form: over {x : matrix @ x = d}, the sphere
|x - x_opt|^2 + f_opt is minimized by the minimum-norm correction
x = x_opt + pinv(matrix) @ (d - matrix @ x_opt). Evaluated at a grid's
cell centers, this is the absolute per-cell reference QD benchmarking has
lacked -- targets per cell, not only per problem.
Valid exactly for Sphere composed with a descriptor whose map is linear:
RandomProjection and AlignedProjection (matrix is
params.descriptor), and SubsetProjection after scattering its matrix
to full width::
full = jnp.zeros((k, num_dims)).at[:, sub.subset].set(sub.matrix)
The nonlinear families have no closed form, and the quantized family's preimages are slabs rather than points; both are out of scope by construction, not omission.
The minimum is taken over all of R^D, so the value is a lower bound for
the box-constrained optimum and exact whenever the returned argmin lies
inside the box -- which the caller can check, and which holds for every
cell whose preimage meets the box interior.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
d
|
Array
|
A descriptor value, shape |
required |
matrix
|
Array
|
The descriptor's effective linear map, shape
|
required |
params
|
BBOBParams
|
The sphere's instance. |
required |
Returns:
| Type | Description |
|---|---|
Array
|
The optimal fitness at |
Array
|
|