Magma creates U-tube infill channels by pairing adjacent triangle cells. Each pair shares a contiguous vertical range of layers — plastic is injected down one cell and up the other through a window at the bottom.
The assignment problem: for every pair of adjacent cells, decide how many tube segments to place, where (start/end in Z), and which neighbor to pair with, such that:
The solver has two stages. The greedy stage always runs. The CP-SAT refinement
stage is optional (controlled by magma_tube_solver_mode setting).
MagmaTubeSolver::solve()
build_micron_tables() — layer boundaries in integer microns
build_edges() — adjacent cell pairs, shared presence runs
greedy_warm_start() — fast heuristic, populates m_committed
validate("GREEDY") — check constraints, log coverage
if (Refined mode) {
solve_pass(0, 0) — single XY pass per Z level, CP-SAT refinement
over R x R overlapping blocks
validate("CPSAT") — check constraints, log coverage
}
extract_results() — microns → UTubePair layer indices
validate("FINAL") — final constraint check + coverage summary
| Setting | Default | Description |
|---|---|---|
| Tube solver quality | Basic | Basic (greedy only, ~1s) or Refined (greedy + CP-SAT) |
| Solver timeout | 60s | Total time budget for CP-SAT (Refined only, 5–600s) |
| Stagger period | auto | Grid clustering period in mm (0=auto: max_tube_height/3) |
Fast, deterministic heuristic (~100-500ms for 1000+ cells). Produces good initial tube assignments that Stage 2 can refine.
Scoring: For each unconsumed cell×layer, compute a score = sum of achievable tube heights across all unconsumed neighbors. Lower score = fewer/shorter options = more constrained. Push to a min-heap.
Assignment: Pop the most constrained cell×layer. Find its most constrained neighbor (fewest unconsumed layers in their shared run at this Z). Expand the longest valid tube between them (respecting min/max height, run boundaries, and consumed intervals on both cells). Mark consumed on both cells.
Periodic re-scoring: After every max(200, num_edges/3) assignments, rebuild
the heap from scratch with fresh consumed state. This corrects stale priority
ordering — cells that became constrained due to neighbor consumption get
re-prioritized. Typically triggers ~3-10 re-scores per model.
CellConsumed: sorted non-overlapping micron intervals per cell. Binary
search for overlap checks, insert+merge for additions.CellLayerScore: min-heap entry with cell, layer, and score.| Model | Cells | Tubes | Coverage | Time |
|---|---|---|---|---|
| 20mm cube | 71 | 148 | 80.6% | 4ms |
| Stanford bunny | 1056 | 4517 | 76.5% | 503ms |
Constraint-based optimization using Google’s CP-SAT solver. Starts from the greedy solution and improves coverage and stagger. Controlled by the “Refined” solver mode setting.
All interval variables operate in integer microns (µm). This eliminates layer-index lookup tables for variable layer heights — tube height becomes a simple linear bound.
Pre-computation (once per object):
top_um[L] = llround(print_z * 1000) — authoritative
bottom_um[0] = llround(bottom_z * 1000)
bottom_um[L] = top_um[L-1] — exactly contiguous
All micron-space variables use discrete domains derived from a unified layer
boundary list. Since bottom_um[L+1] == top_um[L], each boundary is both the
end of one layer and the start of the next:
boundaries = [bottom_um[eff_start], top_um[eff_start], ..., top_um[eff_end]]
start, end ∈ Domain::FromValues(boundaries)
size ∈ Domain::FromValues({b[j]-b[i] | j>i} ∩ [min_h, max_h])
contrib ∈ {0} ∪ sizes
This dramatically tightens the LP relaxation compared to continuous ranges, improving solver performance by 10-30%.
Variables (per segment slot):
active : BoolVar — is this segment used?
start : IntVar — tube bottom (discrete boundary domain)
end : IntVar — tube top (discrete boundary domain)
size : IntVar — height (discrete feasible-difference domain)
interval : OptionalIntervalVar(start, size, end, active)
Constraints:
Objective:
Maximize:
W_COVERAGE(1M) × Σ{ contrib } — coverage (fill ratio, dominant)
- W_AVG(5) × deficit — discourage avg tube length reduction
Average-length deficit (1 IntVar, 1 constraint):
min_avg = greedy_avg × (100 - stagger_tolerance) / 100 (stagger_tolerance is an internal constant)
deficit = max(0, min_avg × Σ{active} - Σ{contrib})
| Weight | Value | Purpose |
|---|---|---|
| W_COVERAGE | 1,000,000 | 1µm of coverage » all other terms; fill never sacrificed |
| W_AVG | 5 | Worst-case deficit ~500K < 1M; discourages splits at ~20-30K per |
Stagger is handled structurally by domain restriction, not by objective terms.
Tube boundaries can only land on {run endpoints ∪ phase-grid points}:
When a run must split (run height > max_tube_height), internal split points are forced to grid positions. Different edges have different grids → different split positions → automatic stagger with zero constraints or objective terms.
No W_GRID_DIST, W_DODGE, or W_ACTIVATION in the objective. Earlier designs used dodge penalties, but these spread injections across many Z levels. Domain restriction concentrates them on a small set of grid-aligned heights. The average-length deficit discourages needless splitting (same effect as the old W_ACTIVATION but tied to the actual metric we care about).
Committed segments from the greedy stage (stored in m_committed) become
CP-SAT warm start hints:
AddHint(active, start, end)The model is spatially partitioned into XY blocks for bounded computation:
Edge collection uses cell reverse lookup (m_cell_edges) instead of scanning
all edges — O(block cells × edges per cell) instead of O(total edges).
Each run (contiguous cell-pair presence range) gets K segment slots:
K = max(2, K_from_greedy × 13 / 10 + 1)
K_from_greedy = number of greedy tubes on this run
The 30% headroom over the greedy count gives CP-SAT room to split tubes for stagger without running out of slots.
Blocks are solved sequentially. CP-SAT uses all available cores internally for its own search, which dominates the time budget for a single block. Running blocks in parallel would over-subscribe the CPU and hurt total throughput.
Checked between passes via throw_if_canceled() (OrcaSlicer’s standard
pattern). Throws CanceledException which propagates up to the UI. Current
blocks finish their timeout before cancellation takes effect.
Reusable validate_committed() function checks all constraints on committed
segments. Called after each stage (GREEDY, CPSAT, FINAL):
src/libslic3r/Magma/
MagmaGreedyWarmStart.hpp/.cpp — greedy warm start algorithm
MagmaTubeSolver.hpp/.cpp — CP-SAT solver, validation, orchestration
MagmaTubeMap.hpp/.cpp — bridge: reads config, calls solver
Indicative numbers from development testing. Actual times depend on solver timeout, model complexity, and machine.
| Model | Cells | Greedy stage | + CP-SAT refinement |
|---|---|---|---|
| 20mm cube | 71 | 80.6% coverage / ~4ms | ~90% / 1-2 min |
| Stanford bunny | 1056 | 76.5% / ~500ms | 79-80% / 5-10 min |
The greedy stage alone provides good coverage in milliseconds — fast enough for interactive slicing. CP-SAT refinement adds 3-7% coverage at a significant time cost; recommended for final production slicing on complex models.