Operator modules¶
An Operator Module is a Ruby-style mixin: a bundle of kernels, helper
methods and fields that any number of @cm.template models can share by
inheriting from it.
import numpy as np
import cmenpy as cm
@cm.module
class MutationOps:
"""A reusable Gaussian-mutation operator module."""
scale: cm.Constant[float, 0.3] # shared, read-only
@cm.vectorize
def clip(x: cm.Array, lo: cm.Scalar, hi: cm.Scalar):
return min(max(x, lo), hi)
def mutate(self, parents, rng):
noise = rng.standard_normal(parents.shape)
return self.clip(parents + self.scale * noise, -1.0, 1.0)
@cm.template
class HillClimber(MutationOps):
def evolve(self, e, ctx) -> None:
pop = cm.PopulationSwarm(ctx.buff)
trial = cm.PopulationSwarm(ctx.buff.snapshot)
trial.compute << self.mutate(~pop.extract, ctx.rng)
mask = cm.is_best((trial.F, pop.F), ctx.sense)
pop.assign[mask] << trial.access[mask]
HillClimber now has self.scale, self.clip and self.mutate without
duplicating a line.
What @cm.module does¶
- keeps the class identity — it returns the same class;
- materializes
cm.Constantannotations as read-only descriptors immediately, soMutationOps.scaleworks even before any model includes the mixin; - rejects non-classes with
TypeError(a@cm.templatemodel instance is not reusable as a mixin).
Mixin application is ordinary inheritance: a module can hold anything a
base class can — cm.Argument / cm.Variable / cm.Constant fields, all
three kernel decorators, plain helper methods.
MRO rules¶
Collisions resolve through the normal MRO, leftmost base wins:
A subclass redefinition always wins over any base, module or not. Constants follow the same rule, and redeclaring the name in a subclass overrides the inherited value.
Kernels and compile options¶
Kernels declared in a module are inherited exactly like methods: called
through the model instance (self.clip(...)), they compile with the model's
compile options; accessed on the module class they use the decorator's.
model.prepare() / cm.prepare walk the MRO, so inherited kernels are
reported and warmed up with the model's options too.
model = HillClimber(seed=1, fastmath=True)
model.prepare() # {"clip": [...], ...} — includes the module's clip
See Compilation options.
Composition patterns¶
- Keep modules stateless. Let the model own the state (
cm.Variable) and the module own behavior pluscm.Constanthyperparameters. - One concern per module —
MutationOps,SelectionOps,CrossoverOps— composed by listing bases:class GA(SelectionOps, CrossoverOps):. - Temper constants in the model when the fixed value is only a baseline:
When to use a module vs a plain base class¶
@cm.module is not required for inheritance — a plain base class works
almost the same. Use it when you want constants installed (and validated) at
decoration time, so MutationOps.scale is reachable on the mixin itself
before any model includes it. Use a plain base class when the "mixin" is
really a template itself.
Full example¶
example/operator_module.py defines MutationOps once and builds two
models (HillClimber, Annealer) on top of it, runnable side by side.