While individual optimizers run a single search strategy, a Program chains multiple optimizers into a multi-stage pipeline: broad exploration followed by targeted refinement, cheap filters before expensive scoring, temperature annealing across stages.A Program runs its optimizers sequentially, automatically handling the handoff of results between stages.
When one optimizer finishes and the next begins, the Program performs a carefully orchestrated handoff:After each optimizer completes:Optimizers are responsible for their own ordering. Rejection Sampling keeps result_sequences sorted by energy (best first) throughout its run. Other optimizers preserve their natural ordering.Before the next optimizer runs:
_initialize_sequence_pools() reads from the previous optimizer’s result_sequences
Both pools are filled by cycling through source (preserving diversity when sizes differ)
Stale constraint metadata is cleared so the new stage starts with a clean slate
Not all optimizers use inherited state the same way:
Optimizer
How It Uses Previous Results
Rejection Sampling
Uses as starting proposals, then generates more and keeps overall best
MCMC
Uses as parallel trajectories, generates proposals from each
Cycling
Uses as working proposals for conditioning cycles
BeamSearch
Ignores previous results. Always starts fresh from its prompt parameter
BeamSearch ignores previous optimizer results by design. It always starts fresh from its configured prompt since it is built for autoregressive generation. Place it as the first stage in a pipeline, or use it standalone.
The snippets below are illustrative patterns. They assume the segment, construct, generators, and the named constraint objects (for example gc_constraint, structure_constraint, expression_constraint) have already been defined as shown in the earlier examples and the Constraints guide.
Exploration then Refinement
Rejection Sampling (broad) then MCMC (focused)Use Rejection Sampling to quickly sample thousands of proposals with cheap constraints, then hand the best ones to MCMC for detailed optimization with expensive constraints.Most common multi-stage pattern.
Progressive Constraints
MCMC (basic) then MCMC (+ structure) then MCMC (+ expression)Start with cheap sequence-level constraints, then progressively add expensive constraints. Each stage builds on the previous one’s results.Avoids wasting GPU time scoring bad sequences.
Temperature Annealing
MCMC (hot) then MCMC (warm) then MCMC (cold)Explicit temperature stages: high temperature for broad exploration, medium for narrowing, low for final polishing. More control than single-optimizer annealing.Better for rugged energy landscapes.
Generator Switching
Rejection Sampling + RandomNucleotide then MCMC + ESM2Start with fast random mutations for initial screening, then switch to language-model-guided mutations for biologically informed refinement.Combines fast screening with language-model-guided refinement.
Use run_stage() for fine-grained control: inspect results between stages, conditionally skip stages, or re-run a stage with different parameters.
python
program = Program(optimizers=[opt1, opt2, opt3], num_results=5)# Run first stageprogram.run_stage(0)results = program.get_stage_results(0)# Inspect before continuingbest = results["results"][results["best_result_idx"]]print(f"Stage 1 best energy: {best['energy_score']:.4f}")# Conditionally run next stageif best["energy_score"] < 0.5: program.run_stage(1)else: print("Stage 1 didn't converge, skipping refinement")
A previous stage can also be re-run, which resets the pipeline to that point and invalidates subsequent stages:
python
# Re-run stage 0 (invalidates stages 1 and 2)program.run_stage(0)
program.run()# Final energy scores (from last optimizer)print(program.energy_scores) # [0.05, 0.08, 0.12, ...]# Final sequences (from shared constructs)for construct in program.constructs: for sequence in construct.joined_sequences: print(sequence.sequence)# Structured resultsresults = program.extract_results(program.energy_scores)for result in results["results"]: print(f"Result {result['result_idx']}: energy={result['energy_score']:.4f}") for construct in result["constructs"]: for seg in construct["segments"]: print(f" {seg['label']}: {seg['sequence'][:50]}...")
# Results from stage 0stage_0_results = program.get_stage_results(0)# Export a specific stage's results (writes the 4-table folder for that stage)program.export(path="./stage0_results/", format="csv", stage=0)
Save and restore program state for long-running optimization or checkpointing:
python
# Save statestate = program.serialize_state()# Save to file, database, etc.import jsonwith open("checkpoint.json", "w") as f: json.dump(state, f)# Later: restore state and continuewith open("checkpoint.json") as f: state = json.load(f)program.restore_state(state, stage_index=1)program.run_stage(1) # Resume from stage 1
All optimizers in a Program must share the same Construct objects (by identity, not just value). This is how state persists between stages. The construct is created once and the same object is passed to all optimizers.
python
# Correct: same construct objectconstruct = Construct([segment])opt1 = MCMCOptimizer(constructs=[construct], ...)opt2 = MCMCOptimizer(constructs=[construct], ...) # Same object# Wrong: different construct objects (raises ValueError)opt1 = MCMCOptimizer(constructs=[Construct([segment])], ...)opt2 = MCMCOptimizer(constructs=[Construct([segment])], ...) # Different object!
Each generator and constraint instance can only be used in one optimizer. This prevents shared mutable state bugs. Create new instances for each stage.
python
# Correct: separate generator instances per optimizergen1 = RandomNucleotideGenerator(config)gen2 = RandomNucleotideGenerator(config) # New instance, same config is finegen1.assign(segment)gen2.assign(segment)# Wrong: reusing the same generator instance (raises ValueError)gen = RandomNucleotideGenerator(config)gen.assign(segment)opt1 = MCMCOptimizer(generators=[gen], ...)opt2 = MCMCOptimizer(generators=[gen], ...) # Same instance -- error!