Iterate To A Fixed Point

Use fixpoint when a traced step should run until the next state is stable.

Define The State And Step

The step receives the current state plus a stable input, conventionally called theta, and returns the next state with the same pytree structure.

import optree
import autoform as af


@optree.dataclasses.dataclass(namespace=af.PYTREE_NAMESPACE)
class RewriteState:
    draft: str
    status: str


def rewrite_step(state: RewriteState, target: str) -> RewriteState:
    del state
    return RewriteState(draft=target, status="stable")


example_state = RewriteState(draft="rough draft", status="draft")
step_ir = af.trace(rewrite_step)(example_state, "polished draft")

This step is deterministic so the example can run without an LM provider. In a real refinement program, rewrite_step can call lm_call, lm_schema_call, custom tools, or any other traceable autoform code.

Use The Step In A Program

Build the fixed point inside the outer traced program:

def settle(init: RewriteState, target: str) -> RewriteState:
    return af.fixpoint(step_ir, init, target, max_iters=4)


ir = af.trace(settle)(example_state, "polished draft")
result = ir.call(example_state, "polished draft")
print(result)

The first iteration moves from "rough draft" to "polished draft". The next iteration produces the same state, so the default structural equality check stops the loop.

Stop With A Custom Equivalence IR

When stability is semantic, compare only the fields that matter:

def is_stable(prev: RewriteState, new: RewriteState) -> bool:
    del prev
    return af.match(new.status, "stable")


equiv_ir = af.trace(is_stable)(example_state, example_state)


def settle_by_status(init: RewriteState, target: str) -> RewriteState:
    return af.fixpoint(step_ir, init, target, max_iters=4, equiv_ir=equiv_ir)

equiv_ir receives (previous_state, new_state) after each step. Here the loop can stop as soon as the status field says the draft is stable, even if other state fields changed.

Pull Feedback To theta

The fixed-point pullback sends output feedback to the stable input theta, not to the initial state:

def final_draft(init: RewriteState, target: str) -> str:
    final = af.fixpoint(step_ir, init, target, max_iters=4, adj_iters=1)
    return final.draft


draft_ir = af.trace(final_draft)(example_state, "polished draft")
output, (init_feedback, target_feedback) = af.pullback(draft_ir).call(
    (example_state, "polished draft"),
    "make the final draft more concrete",
)

print(output)
print(init_feedback)
print(target_feedback)

This is the main reason to use fixpoint instead of manually unrolling a loop: the backward pass is about the equilibrium. adj_iters controls how much state-to-state feedback is accumulated at the fixed point before feedback is read out for theta.

Adapt The Pattern To LM Refinement

For an LM refinement loop, keep the same shape:

  • state: the current draft, route, score, or optimizer state;

  • theta: the rubric, instruction, task input, or prompt being optimized;

  • step IR: one refinement pass;

  • equivalence IR: a deterministic check, schema judge, or cached semantic comparator.

Use max_iters as the hard budget. If non-convergence matters to downstream code, include a status or counter in the state so the returned value records that the budget was exhausted.