> ## Documentation Index
> Fetch the complete documentation index at: https://proto.evodesign.org/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Multi-Stage DNA Optimization

> Chain rejection sampling and MCMC on a shared construct to refine DNA GC content across two stages

A program may run several optimizers in sequence on the same construct, so the result of one
stage becomes the starting point of the next. This program optimizes a 20 bp DNA insert in two
stages: rejection sampling first makes large moves to enrich GC content into a broad 70-100%
window, then MCMC makes small moves to refine it into a tight 80-90% window.

<a href="https://github.com/evo-design/proto-language/blob/main/examples/notebooks/multi-stage-optimization.ipynb" target="_blank" class="tutorial-notebook-btn">
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
    <path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" />

    <path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" />
  </svg>

  <span>Open as a runnable notebook</span>

  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
    <path d="M7 17L17 7M9 7h8v8" />
  </svg>
</a>

<a href="https://github.com/evo-design/proto-language/blob/main/examples/scripts/toy-multiple-optimizers.py" target="_blank" class="tutorial-notebook-btn">
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
    <path d="M16 18l6-6-6-6" />

    <path d="M8 6l-6 6 6 6" />
  </svg>

  <span>View as a Python script</span>

  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
    <path d="M7 17L17 7M9 7h8v8" />
  </svg>
</a>

## The shared construct

A `Segment` is the stretch of sequence being designed and a `Construct` groups the segments that
make up one molecule. Here a single 20 bp DNA segment (`length=20`, `sequence_type="dna"`,
`label="insert"`) starts empty, leaving all positions open for the optimizers to fill. Both
stages reference these same `insert` and `construct` objects by identity, so the sequences a stage
accepts carry over as the starting point for the next.

```python python icon="python" theme={null}
from proto_language.core import Segment, Construct

insert = Segment(length=20, sequence_type="dna", label="insert")
construct = Construct([insert])
```

## Stage 1: rejection sampling

`RejectionSamplingOptimizer` draws independent proposals and keeps the best by lowest energy;
each proposal batch starts fresh with no state carried between draws. `gen1` is a
`RandomNucleotideGenerator` whose `MaskingStrategy(num_mutations=10)` mutates exactly ten of the
twenty positions per call, and because the segment starts empty its first call fills the initial
random sequence. The single `gc_enrich` constraint (`gc_content_constraint` with `min_gc=70`,
`max_gc=100`) scores 0 when GC content falls inside that broad window and penalizes deviation
below it. The config draws `num_samples=10` proposals total and retains the top `num_results=3`
by lowest energy, which hand off as stage two's starting pool.

```python python icon="python" theme={null}
from proto_tools.transforms.masking import MaskingStrategy
from proto_language.core import Constraint
from proto_language.constraint import gc_content_constraint
from proto_language.generator import RandomNucleotideGenerator, RandomNucleotideGeneratorConfig
from proto_language.optimizer import RejectionSamplingOptimizer, RejectionSamplingOptimizerConfig

gen1 = RandomNucleotideGenerator(
    RandomNucleotideGeneratorConfig(masking_strategy=MaskingStrategy(num_mutations=10))
)
gen1.assign(insert)

stage1 = RejectionSamplingOptimizer(
    constructs=[construct],
    generators=[gen1],
    constraints=[Constraint(inputs=[insert], function=gc_content_constraint,
                            function_config={"min_gc": 70, "max_gc": 100}, label="gc_enrich")],
    config=RejectionSamplingOptimizerConfig(num_samples=10, num_results=3),
)
```

## Stage 2: MCMC refinement

`MCMCOptimizer` runs Metropolis-Hastings with simulated annealing: at each step it mutates the
current sequence, scores the proposals, and accepts improvements outright while accepting worse
proposals with probability `exp(-dE / T)` as the temperature anneals down from `max_temperature`.
Here `gen2` uses `MaskingStrategy(num_mutations=1)`, so each move flips a single base, and the
`gc_refine` constraint (`min_gc=80`, `max_gc=90`) rewards the tighter window. The config runs one
trajectory (`num_results=1`) for `num_steps=10` steps, drawing `proposals_per_result=20` proposals
per step and keeping the best by energy before the accept/reject decision, with
`max_temperature=2.0` as the starting temperature.

```python python icon="python" theme={null}
from proto_language.optimizer import MCMCOptimizer, MCMCOptimizerConfig

gen2 = RandomNucleotideGenerator(
    RandomNucleotideGeneratorConfig(masking_strategy=MaskingStrategy(num_mutations=1))
)
gen2.assign(insert)

stage2 = MCMCOptimizer(
    constructs=[construct],
    generators=[gen2],
    constraints=[Constraint(inputs=[insert], function=gc_content_constraint,
                            function_config={"min_gc": 80, "max_gc": 90}, label="gc_refine")],
    config=MCMCOptimizerConfig(num_results=1, proposals_per_result=20, num_steps=10, max_temperature=2.0),
)
```

## Run both stages

The `Program` runs its optimizers in the order listed, `stage1` then `stage2`. Because both share
the same construct, the three high-GC candidates rejection sampling retains seed the MCMC
trajectory, so the final design reflects both passes: enriched by the first, refined by the
second.

```python python icon="python" theme={null}
from proto_language.core import Program

program = Program(optimizers=[stage1, stage2], num_results=1)
program.run()
```

## Inspect the result

The final design is the construct's joined sequence. Per-segment results live under
`metadata["segments"][<label>]`, and each constraint's diagnostics sit under the `constraints`
entry keyed by the `label` set above. Reading `gc_content` back out from the stage-two
`gc_refine` entry confirms the design lands inside the 80-90% GC window.

```python python icon="python" theme={null}
best = program.constructs[0].joined_sequences[0]
gc_pct = best.metadata["segments"]["insert"]["constraints"]["gc_refine"]["data"]["gc_content"]
print(f"final sequence: {best.sequence}")
print(f"GC content:     {gc_pct:.1f}%")
```

```text theme={null}
final sequence: GGTCCGCCGCGGTGCACCCG
GC content:     85.0%
```

## Next Steps

<CardGroup cols={2}>
  <Card title="Using Optimizers" icon="circle-play" href="/docs/language/guides/using-optimizers">
    Run and chain optimizers.
  </Card>

  <Card title="DNA Sequence Optimization" icon="dna" href="/docs/language/guides/examples/dna-optimization">
    The single-stage version of this program.
  </Card>
</CardGroup>
