Skip to content

Scoring modes

There are two ways a target is scored, and the difference is the single most important performance lever in cmenpy:

Mode How it scores NFE spent When
Row-by-row (default) np.apply_along_axis — one Python call per individual 1 per individual a plain callable
Vectorized (fast) one call over the whole (pop, ndim) block len(block) wrapped with cm.vectorized_target

Row-by-row

A plain Python callable works as-is:

def sphere(x):                      # x: one solution vector -> one float
    return float(np.sum(x**2))

model.solve(sphere, bounds, epochs, pop_size)     # scored row-by-row

Each epoch, KernelBuffer.apply feeds the active rows through np.apply_along_axis(self.__target, 1, ...): one Python-level call per individual, one NFE each. This is fine for prototypes and cheap objectives; for anything expensive it dominates the runtime.

Vectorized block

A vectorized target receives the entire active block at once and returns one fitness per row:

def sphere_block(x):                # x: (pop, ndim) -> (pop,)
    return np.sum(x**2, axis=1)

model.solve(cm.vectorized_target(sphere_block), bounds, epochs, pop_size)

One call per epoch instead of one per individual — typically an order of magnitude faster for NumPy-shaped objectives. The contract and pitfalls are on Vectorized targets.

How dispatch works

KernelBuffer.apply(x, idx) first normalizes idx into a row selection, then picks the path from that selection and the marker:

Call Path NFE
apply(x, idx=one row) — int, np.intp (what pop.best returns), or a 0-d integer array target.evaluate(x); a vectorized target gets it as a (1, ndim) block and the single score is unwrapped 1
apply(x, idx=block) with target.vectorized — slice, index array, boolean mask target.evaluate_matrix(block) len(block)
apply(x, idx=block), plain target np.apply_along_axis(...) over the rows 1 per row
apply(x, idx) selecting no rows (empty mask, all rows removed) nothing is evaluated 0

With no index, apply writes and scores the active rows only (mask.founds), so removed individuals are never re-scored and never spend budget.

The counter charges one evaluation per individual scored, never per call and never per dimension: a NumPy row index is one individual (not ndim of them), a mask costs the rows it selects, and the whole-block form costs the active rows. The same selection therefore costs the same in both scoring modes.

Equivalence

The two modes produce identical fitness — only the dispatch differs:

x = np.array([[1.0, 2.0], [-3.0, 0.5], [0.0, 0.0]])

rowwise   = [sphere(row) for row in x]
block     = TargetFunction(cm.vectorized_target(sphere_block), BOUNDS)

np.testing.assert_allclose(rowwise, block.evaluate_matrix(x))

Evaluating a single solution

tf.evaluate(x) is the one-individual path: it spends a single evaluation and works with both scoring modes — a vectorized target is handed a (1, ndim) block and its single score is returned as a scalar. tf.evaluate_matrix(x) is the block path and rejects a bare solution vector, so an individual can never be mis-counted as ndim evaluations:

tf.evaluate(x_row)                # 1 NFE, plain or vectorized target
tf.evaluate_matrix(x_block)       # len(x_block) NFE

Correctness checklist for a block objective

  • Shape in, shape out: returns exactly (len(x),) — one value per row.
  • Axis: reductions use axis=1 (per row), never the default whole-array sum.
  • dtype: float in, float out; float(...) a scalar result per row.
  • No in-place writes to x — it is the live buffer's solution block.
  • Equivalence: assert your block version against the row-wise version on a small block before trusting it at scale.