Skip to content

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 replay
  • AnalysisPass: computes data and stores it in PassContext
  • ControlFlowPass: selects a runtime ControlFlowPlan (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

Python
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

Python
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

Python
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:

Python
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.

Python
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).

Python
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.

Python
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.

Python
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:

Python
from luna_model.transformation import register_allowed_import_prefix

register_allowed_import_prefix("my_company_transforms")

Inspect active prefixes:

Python
from luna_model.transformation import allowed_import_prefixes

print(allowed_import_prefixes())

Running custom passes

Python
from luna_model.transformation import PassManager

pm = PassManager([count_binary, scale_objective, choose_branch])
output = pm.run(model)

Backward mapping:

Python
# original_solution = output.record.backward(transformed_solution)

See also