Models¶
A model is an optimizer you describe once and then run against any
objective. cmenpy offers two declaration styles that produce the same
contract — a callable exposing:
(best_fitness, best_x) = model.solve(
f, bounds, epochs, pop_size, pop_range=None, debug=False, sense="min"
)
@cm.declarewraps a function: one body drives the whole run, and parameters are bound by keyword.@cm.templatewraps a class: per-instance state, an explicitinitialize/evolvesplit, fields, and numba kernels that inherit compile options.
Both produce a BaseOptimizer with .solve(...), .alias and .tracker.
Where and when to use which¶
@cm.declare (function) |
@cm.template (class) |
|
|---|---|---|
| Run structure | one function body; you iterate the epochs yourself | initialize once, then evolve per epoch |
| Parameters | cm.Argument only, keyword-bound at call time |
cm.Argument, cm.Variable, cm.Constant |
| Per-instance state | closure/local variables only | instance attributes (self.w) |
| Numba kernels | module-level (decorator options only) | methods that receive the model's compile options |
| Compile options | none | parallel / fastmath / cache at instantiation |
| Reuse/composition | copy/paste or plain functions | inheritance and @cm.module Operator Modules |
| Introspection | alias, tracker |
plus model_class, compile_options, prepare() |
| Best for | scripts, quick experiments, one-off algorithms | reusable optimizers, anything with state or kernels |
Choose @cm.declare when the algorithm is a single function, you want
parameters bound by keyword, and you have no per-instance state or compiled
kernels needing model options. It is the shortest path from idea to run.
Choose @cm.template when you want the initialize/evolve split, keep
state across epochs on self, declare constants, compile numba kernels that
should honor parallel/fastmath/cache, share operators through
@cm.module, or introspect/prepare the model.
The same algorithm in both styles:
@cm.declare(x=cm.Argument[int, (0, 3), 2])
def random_search(args, epoch, ctx):
pop = cm.PopulationSwarm(ctx.buff)
pop.compute << ctx.rng.uniform(ctx.bounds.low, ctx.bounds.up,
(pop.size, ctx.bounds.ndim))
for _ in epoch:
pop.compute << ctx.rng.uniform(ctx.bounds.low, ctx.bounds.up,
(pop.size, ctx.bounds.ndim))
@cm.template
class RandomSearch:
x: cm.Argument[int, (0, 3), 2]
def initialize(self, ctx) -> None:
pop = cm.PopulationSwarm(ctx.buff)
pop.compute << ctx.rng.uniform(ctx.bounds.low, ctx.bounds.up,
(pop.size, ctx.bounds.ndim))
def evolve(self, e, ctx) -> None:
pop = cm.PopulationSwarm(ctx.buff)
pop.compute << ctx.rng.uniform(ctx.bounds.low, ctx.bounds.up,
(pop.size, ctx.bounds.ndim))
Lifecycle¶
Function style — one pass:
@cm.declare → FunctionOptimizerModel
model = algo(x=1) # bind fields (+ optional seed)
solve(...) → build context
→ func(args=args, epoch=epoch_it, ctx=Context(...)) # once
→ you iterate `epoch`; ctx.epoch stays 0
→ return (best_fitness, best_x)
Class style — per-epoch hooks:
@cm.template → TemplateOptimizerModel (class wrapped, not yet instantiated)
model = MyAlgorithm(x=1, seed=1) # instantiate: bind fields + options
solve(..., prepare=False) → [prepare(): compile kernels]
→ initialize(ctx) # once
→ for e in epoch_it: evolve(e, ctx=Context(...))
→ return (best_fitness, best_x)
A fresh Context is built for every hook call in the class style, so
ctx.epoch tracks the current epoch. In the function style the single
Context is built before your function runs — use the epoch iterator's
counter, not ctx.epoch, inside @cm.declare.
solve parameters¶
| Parameter | Meaning |
|---|---|
f |
the objective; any callable, optionally wrapped with cm.vectorized_target |
bounds |
a Bounds or a bounds sequence; defines ndim |
epochs |
total epochs (iterated as 0 .. epochs-1) |
pop_size |
initial population size |
pop_range |
(min, max) allowed sizes for dynamic populations |
debug |
record per-epoch history in model.tracker |
sense |
"min" (default) or "max" |
prepare |
(template only) compile the class's kernels before running |
Returns (best_fitness, best_x) — the best row of the buffer, respecting
sense.
Planned for 2026b2
solve will accept two opt-in run-control arguments alongside these:
terminator= for composable stopping criteria
(#33) and history= for
in-memory / on-disk run records
(#35). Today the equivalents
are the fixed epochs cap and debug=True with model.tracker.
The ctx object¶
Every hook receives a read-only Context:
| Attribute | Meaning |
|---|---|
ctx.bounds |
the search space (low, up, ndim) |
ctx.buff |
the KernelBuffer backing the population |
ctx.rng |
seeded random generator |
ctx.sense |
"min" or "max" |
ctx.epoch / ctx.epochs |
current epoch / total (template style) |
ctx.target |
the wrapped objective (budget accounting lives on the run's own wrapper) |
Per-dimension bounds
bounds.low / bounds.up are the first dimension's scalars. For
unequal per-dimension intervals pass the arrays straight to the sampler
(e.g. an opfunu objective's f.lb / f.ub — see
Test functions). Bounds.lows, ups, sample() and
clip() helpers land in 2026b2
(#37).
Where to go next¶
- Function style (
declare) - Class style (
template) - Fields and markers —
Argument,Variable,Constant - Operator modules —
@cm.modulemixins - Build a custom model — full class-style tutorial