Creating Custom Passes
You can create custom passes either by subclassing pass base classes or via decorators.
Pass categories
TransformationPass: modifies the model and returns an artifact for backward replayAnalysisPass: computes data and stores it inPassContextControlFlowPass: selects a runtimeControlFlowPlan(branch/sub-pipeline)CompositePass: does both transform + analysis result emission
Via Subclassing
If you prefer explicit classes over decorators, subclass the pass base classes directly.
Subclassing AnalysisPass
from luna_model.transformation import AnalysisPass
class VariableCountAnalysis(AnalysisPass[int]):
PROVIDES = "custom::variable-count"
def name(self) -> str:
return "variable-count-analysis"
def run(self, model, ctx) -> int:
return model.num_variables()
Subclassing TransformationPass
from luna_model.solution import Solution
from luna_model import Sense
from luna_model.transformation import TransformationPass
from luna_model.transformation.artifact import NothingArtifact
class FlipSenseTransform(TransformationPass[NothingArtifact]):
def name(self) -> str:
return "flip-sense-transform"
def forward(self, model, ctx):
model.sense = Sense.MIN if model.sense == Sense.MAX else Sense.MAX
return model, NothingArtifact()
@classmethod
def backward(cls, artifact: NothingArtifact, solution: Solution) -> Solution:
return solution
Subclassing ControlFlowPass
from luna_model.transformation import ControlFlowPass, ControlFlowPlan, Pipeline
from luna_model.transformation.passes import BinarySpinPass
from luna_model import Vtype
class SizeBasedBranch(ControlFlowPass):
def name(self) -> str:
return "size-based-branch"
def run(self, model, ctx) -> ControlFlowPlan:
if model.num_variables() > 100:
return ControlFlowPlan("then", Pipeline([BinarySpinPass(vtype=Vtype.SPIN)], name="then"))
return ControlFlowPlan("else", Pipeline([], name="else"))
Use subclassed passes like any built-in pass:
from luna_model.transformation import PassManager
pm = PassManager([VariableCountAnalysis(), FlipSenseTransform(), SizeBasedBranch()])
output = pm.run(model)
Decorator-based custom analysis
Use @analyze for concise analysis passes.
from luna_model.transformation import analyze
@analyze(name="count-binary")
def count_binary(model, ctx) -> int:
return sum(1 for v in model.variables() if v.vtype.name == "BINARY")
The result is available to later passes via PassContext.require_analysis(...).
Decorator-based custom transformation
Use @transform and return (model, artifact).
from dataclasses import dataclass
from luna_model.solution import Solution
from luna_model.transformation import transform
from luna_model.transformation.artifact import TransformationPassArtifact
@dataclass(frozen=True)
class ScaleArtifact(TransformationPassArtifact):
factor: float
def serialize(self) -> bytes:
return str(self.factor).encode()
@classmethod
def deserialize(cls, data: bytes) -> "ScaleArtifact":
return cls(float(data.decode()))
def backward_scale(artifact: ScaleArtifact, solution: Solution) -> Solution:
# map solution values back if your transform changed variable/value mapping
return solution
@transform(name="scale-objective", backward=backward_scale)
def scale_objective(model, ctx):
factor = 2.0
model.objective = model.objective * factor
return model, ScaleArtifact(factor=factor)
Decorator-based custom control flow
Use @control_flow and return a ControlFlowPlan.
from luna_model.transformation import ControlFlowPlan, Pipeline, control_flow
from luna_model.transformation.passes import BinarySpinPass
from luna_model import Vtype
@control_flow(name="size-based-branch")
def choose_branch(model, ctx):
if model.num_variables() > 100:
return ControlFlowPlan("then", Pipeline([BinarySpinPass(vtype=Vtype.SPIN)], name="then"))
return ControlFlowPlan("else", Pipeline([], name="else"))
Decorator-based custom composite pass
Use @composite when one pass should emit both artifact and analysis result.
from luna_model.transformation import composite
from luna_model.transformation.artifact import NothingArtifact
@composite(name="touch-and-count", provides="decorated_composite::touch-and-count")
def touch_and_count(model, ctx):
# transform
transformed = model
# analysis result
count = transformed.num_variables()
return transformed, NothingArtifact(), count
Registering plugin import prefixes
Decorator-based transform/composite artifact replay resolves Python symbols dynamically. For security, only allowlisted module prefixes can be resolved.
By default, luna_model. is allowed.
If your custom pass lives in another package, register its prefix:
from luna_model.transformation import register_allowed_import_prefix
register_allowed_import_prefix("my_company_transforms")
Inspect active prefixes:
from luna_model.transformation import allowed_import_prefixes
print(allowed_import_prefixes())
Running custom passes
from luna_model.transformation import PassManager
pm = PassManager([count_binary, scale_objective, choose_branch])
output = pm.run(model)
Backward mapping: