OrthoRoute/docs/layer_compaction.md

1076 lines
33 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Layer Compaction for PathFinder Routing
**Status:** Research / Proposed Feature
**Last Updated:** November 14, 2025
**Related:** Post-routing optimization, cost reduction
---
## Problem Statement
After routing a complex backplane with PathFinder on 32 layers, analysis may reveal:
```
Layer Utilization Analysis:
F.Cu (0): ████████████████████░░ 85% utilized
In1.Cu (1): ███████████████░░░░░░░ 65% utilized
In2.Cu (2): ████████████████░░░░░░ 70% utilized
In3.Cu (3): ███████████░░░░░░░░░░░ 55% utilized
...
In14.Cu (14): ██████░░░░░░░░░░░░░░░░ 30% utilized
In15.Cu (15): ████░░░░░░░░░░░░░░░░░░ 20% utilized
In16.Cu (16): ███░░░░░░░░░░░░░░░░░░░ 15% utilized
In17.Cu (17): ██░░░░░░░░░░░░░░░░░░░░ 10% utilized
In18.Cu (18): █░░░░░░░░░░░░░░░░░░░░░ 5% utilized
In19-30: ░░░░░░░░░░░░░░░░░░░░░░ <5% utilized
B.Cu (31): ████████████████████░░ 80% utilized
```
**Question:** Can we "pull up" traces from lower layers (17-30) to upper layers (1-16) that have unused capacity, reducing the total layer count from 32 to perhaps 20?
**Why this matters:**
- **Cost:** PCB fab pricing scales with layer count (~$50 more per 2 layers)
- **Stackup:** Simpler stackups are easier to manufacture and have better yields
- **Lead time:** Fewer layers = faster fabrication
- **Example:** Routing with 32 layers but only using 20 could save $300 per board
---
## What Is Layer Compaction?
**Layer Compaction** (also called **Layer Minimization** or **Track Layer Reassignment**) is a post-routing optimization that attempts to reduce the number of physical layers required while maintaining routing completeness and DRC compliance.
### Related Problems in VLSI/PCB CAD
**1. Layer Assignment Problem (VLSI)**
- During global routing, routes are found in 2D without layer assignment
- Layer assignment step maps 2D routes to specific 3D metal layers
- Goal: Minimize layers while respecting routing density constraints
- **NP-hard** in general case but solvable with heuristics
**2. Track Assignment Problem**
- Intermediate step between global and detailed routing
- Assigns routes to specific tracks on each layer
- Can optimize for layer utilization, crosstalk, timing
**3. Via Migration**
- Post-routing optimization that changes via layer spans
- Example: Via from F.Cu→In20.Cu becomes F.Cu→In12.Cu
- Requires rerouting affected trace segments
---
## Why PathFinder Doesn't Automatically Minimize Layers
PathFinder's **negotiated congestion** algorithm optimizes for:
1.**Routability** - All nets successfully routed
2.**Wirelength** - Minimize total trace length
3.**Congestion** - Eliminate overuse/conflicts
4.**Via count** - Reduce layer transitions
But it does **NOT** optimize for:
-**Layer count minimization** - Uses all available layers equally
-**Layer utilization balance** - Doesn't prefer upper layers
-**Unused layer elimination** - Doesn't avoid sparse layers
### Why?
**PathFinder's cost function:**
```
cost(edge) = base_cost + pres_fac * present_usage + hist_fac * history
```
This makes all layers equally attractive (same base cost). Adding:
```
layer_penalty = (layer_depth / max_layers)^2
```
Would bias routing toward upper layers, but PathFinder would use lower layers when upper layers get congested.
**Result:** PathFinder distributes routing load across all layers for maximum routability, not minimum layer count.
---
## Academic Background
### Research on Layer Assignment
From literature review:
**"Layer assignment is a desirable intermediate step between global routing and detailed routing"** (Track Assignment research)
**Approaches:**
1. **ILP-based** - Integer Linear Programming for optimal assignment (slow)
2. **Greedy heuristics** - Fast but suboptimal
3. **Simulated annealing** - Good quality, moderate runtime
4. **Graph-based** - Model as min-cost flow or multicommodity flow
**BoxRouter 2.0 approach:**
- Perform 2D global routing first (planar)
- Layer assignment as second phase
- Machine learning to predict optimal layer ordering
**Key insight:** Separating 2D routing from layer assignment allows explicit layer minimization.
---
## Proposed Approaches
### Approach 1: PathFinder-Based Compaction (RECOMMENDED)
**Key insight:** Use PathFinder itself to reroute nets with layer constraints!
**Workflow:**
```bash
# New mode in main.py:
python main.py compact TestBackplane.ORS --target-layers 20
```
**What it does:**
1. Load ORS file → reconstruct routing solution
2. Initialize PathFinder with board geometry (from ORP file)
3. Load existing routing into PathFinder internal state
4. Set layer constraints (forbid layers > 20)
5. Identify nets using forbidden layers (layers 21-31)
6. Rip up those nets
7. Reroute with PathFinder (respecting layer constraints)
8. Export new ORS file with compacted solution
**Advantages:**
-**Full PathFinder power** - Real negotiated congestion routing
-**Can resolve conflicts** - Negotiation handles overlaps
-**High success rate** - PathFinder will find paths if they exist
-**Quality routing** - Optimal paths within constraints
-**Existing infrastructure** - Reuses ORS import/export
**Implementation:**
```python
def run_compact(ors_file, target_layers, max_iterations=50):
# Load ORS and corresponding ORP
geometry_data, metadata = import_solution_from_ors(ors_file)
orp_file = ors_file.replace('.ORS', '.ORP')
board_data = import_board_from_orp(orp_file)
# Initialize PathFinder
board = create_board_from_data(board_data)
pf = UnifiedPathFinder(config=PathFinderConfig(max_iterations=max_iterations))
pf.initialize_graph(board)
pf.prepare_routing_runtime()
# Load existing solution into PathFinder
load_solution_into_pathfinder(pf, geometry_data)
# Set layer constraints
pf.set_max_layer_constraint(target_layers)
# Find and reroute deep nets
deep_nets = find_nets_using_layers_above(geometry_data, target_layers)
for net_id in deep_nets:
pf.rip_up_net(net_id)
pf.route_multiple_nets(deep_nets)
# Export compacted solution
geom = pf.get_geometry_payload()
output_file = ors_file.replace('.ORS', f'_compact_{target_layers}L.ORS')
export_solution_to_ors(geom, metadata, output_file)
```
See detailed implementation in **Approach 1 Implementation** section below.
---
### Approach 2: ORS-Only Post-Processor (Simpler, Limited)
**Alternative:** Geometry manipulation without rerouting.
### High-Level Strategy
```
Input: Fully routed board with N layers
Output: Same routing using M < N layers (M minimal)
1. Analyze layer utilization
2. Identify migration candidates (traces on underutilized layers)
3. Attempt to migrate traces to upper layers with free capacity
4. Verify DRC compliance
5. Iterate until no more migrations possible
6. Report final layer count
```
### Algorithm Design
#### Phase 1: Layer Utilization Analysis
```python
def analyze_layer_utilization(pathfinder):
"""
Compute routing density per layer.
Returns:
layer_stats = {
0: {'tracks': 250, 'capacity': 1000, 'util': 0.25},
1: {'tracks': 180, 'capacity': 1000, 'util': 0.18},
...
}
"""
layer_stats = {}
for layer_id in range(pathfinder.lattice.Nz):
# Count tracks on this layer
tracks_on_layer = [t for t in geometry.tracks if t['layer'] == layer_id]
# Estimate routing capacity (function of board area and grid pitch)
capacity = estimate_layer_capacity(pathfinder.lattice, layer_id)
layer_stats[layer_id] = {
'tracks': len(tracks_on_layer),
'capacity': capacity,
'utilization': len(tracks_on_layer) / capacity,
'free_capacity': capacity - len(tracks_on_layer)
}
return layer_stats
```
#### Phase 2: Identify Target Layer Count
```python
def find_minimal_layer_count(layer_stats, min_util_threshold=0.05):
"""
Find deepest layer with >= 5% utilization.
Example: If layers 0-19 have >5% util, but 20-31 have <5%,
target is 20 layers.
"""
max_used_layer = 0
for layer_id, stats in sorted(layer_stats.items(), reverse=True):
if stats['utilization'] >= min_util_threshold:
max_used_layer = layer_id
break
# Add safety margin (need some headroom for migration)
target_layer_count = max_used_layer + 3
return target_layer_count
```
#### Phase 3: Migration Strategy
**Greedy approach (fast, good for first pass):**
```python
def compact_to_target_layers(pathfinder, target_layer_count):
"""
Migrate traces from deep layers to upper layers.
Strategy: Start with deepest layers, migrate upward.
"""
migrations_attempted = 0
migrations_succeeded = 0
# Process layers from deepest to shallowest (above target)
for source_layer in range(pathfinder.lattice.Nz - 1, target_layer_count - 1, -1):
# Get all net segments on this layer
segments = find_segments_on_layer(pathfinder, source_layer)
for net_id, segment in segments:
# Find best target layer (respecting H/V constraints)
target_layer = find_migration_target(
segment,
max_layer=target_layer_count - 1,
layer_stats=layer_stats,
direction=get_segment_direction(segment)
)
if target_layer is None:
continue
migrations_attempted += 1
# Attempt migration
success = migrate_segment(
pathfinder,
net_id,
segment,
source_layer,
target_layer
)
if success:
migrations_succeeded += 1
update_layer_stats(layer_stats, segment, source_layer, target_layer)
return migrations_attempted, migrations_succeeded
```
---
## Key Challenges
### 1. Manhattan H/V Layer Constraints
**Problem:** PathFinder uses alternating H/V layers:
- Odd layers (1, 3, 5...): Horizontal routing only
- Even layers (2, 4, 6...): Vertical routing only
**Implication:** Can't migrate a horizontal segment from In17.Cu (odd) to In16.Cu (even).
**Solution:**
```python
def find_migration_target(segment, max_layer, direction):
"""
Find target layer respecting H/V constraints.
If segment is horizontal:
- Can only move to odd layers (1, 3, 5...)
If segment is vertical:
- Can only move to even layers (2, 4, 6...)
"""
candidates = []
for layer in range(1, max_layer):
layer_direction = 'H' if layer % 2 == 1 else 'V'
if layer_direction == direction:
candidates.append(layer)
# Sort by available capacity
return select_best_layer(candidates, layer_stats)
```
### 2. Via Barrel Constraints
**Problem:** Moving a trace from In20.Cu to In10.Cu changes via barrels.
**Example:**
```
Before migration:
Via A: F.Cu (0) → In20.Cu (20) [drills through layers 0-20]
Trace: In20.Cu horizontal segment
Via B: In20.Cu (20) → B.Cu (31) [drills through layers 20-31]
After migration to In10.Cu:
Via A: F.Cu (0) → In10.Cu (10) [drills through layers 0-10] ✓ Shorter!
Trace: In10.Cu horizontal segment
Via B: In10.Cu (10) → B.Cu (31) [drills through layers 10-31] ✓ Shorter!
```
**Benefits:**
- ✅ Shorter via barrels = less capacitance
- ✅ Fewer layers drilled = less manufacturing cost
- ✅ Better signal integrity (less via stub)
**Gotcha - Barrel Conflicts:**
If another net has a via at the same (x, y) position:
```
Net 1: F.Cu → In10.Cu [barrel: 0-10]
Net 2: In8.Cu → In15.Cu [barrel: 8-15]
^^^^^^^ OVERLAP at layers 8-10!
```
This creates a **barrel conflict** - two vias physically overlapping.
**Solution:** Migration must check for barrel conflicts at new layer depth.
### 3. Segment Connectivity
**Problem:** Can't migrate a segment in isolation - must consider net topology.
**Example:**
```
Net "VCC" path:
Pad A (F.Cu) → [via] → In20.Cu → [segment] → [via] → In5.Cu → ... → Pad B
Migrating the In20.Cu segment requires:
1. Changing first via from (F.Cu → In20.Cu) to (F.Cu → In10.Cu)
2. Rerouting segment on In10.Cu
3. Changing second via from (In20.Cu → In5.Cu) to (In10.Cu → In5.Cu)
```
**Complexity:** Must migrate entire connected component, not just one segment.
### 4. Capacity Estimation
**Challenge:** How much "free capacity" does a layer have?
**Naive approach:**
```python
free_capacity = total_routing_tracks - used_tracks
```
**Problems:**
- Doesn't account for routing density (congested areas)
- Assumes uniform distribution (not realistic)
- Ignores DRC spacing requirements
**Better approach:**
```python
# Divide layer into grid cells (e.g. 5mm x 5mm)
for cell in grid_cells:
local_tracks = count_tracks_in_cell(cell, layer)
local_capacity = estimate_cell_capacity(cell, layer)
cell_utilization = local_tracks / local_capacity
# Layer can accept migration only if:
# - Average utilization < 70%
# - No cells > 90% utilized
# - Migration target area < 80% utilized
```
---
## Implementation Strategy
### Option 1: Layer-Biased Routing (During Routing)
**Modify PathFinder cost function:**
```python
def compute_edge_cost(edge, layer):
base = edge.base_cost
present = pres_fac * edge.present_usage
history = hist_fac * edge.history
# NEW: Add layer depth penalty
layer_penalty = (layer / max_layers) ** 2 # Quadratic penalty
return base + present + history + layer_penalty
```
**Effect:** PathFinder naturally prefers upper layers during routing.
**Pros:**
- ✅ Simple to implement
- ✅ Integrated into routing (no post-processing)
- ✅ Respects congestion automatically
**Cons:**
- ❌ May sacrifice routability (might need those deep layers)
- ❌ Can't control target layer count explicitly
- ❌ Might increase wirelength
### Option 2: Post-Routing Layer Compaction (After Routing)
**Two-pass approach:**
**Pass 1: Conservative Migration**
```python
def conservative_layer_compaction(pathfinder, target_layers):
"""
Only migrate segments where there's obvious free capacity.
No rip-up/reroute - just layer reassignment.
"""
for layer in range(target_layers, pathfinder.lattice.Nz):
segments = get_segments_on_layer(pathfinder, layer)
for net_id, seg in segments:
# Find target layer with same direction and free space
target = find_free_layer(seg, max_layer=target_layers - 1)
if target and has_space_for_segment(target, seg):
# Direct reassignment (no rerouting needed)
reassign_segment_layer(pathfinder, net_id, seg, target)
```
**Pass 2: Aggressive Compaction with Rerouting**
```python
def aggressive_layer_compaction(pathfinder, target_layers):
"""
Rip up and reroute nets that still use deep layers.
Use PathFinder with layer constraints.
"""
# Identify nets still using layers >= target_layers
deep_nets = find_nets_using_deep_layers(pathfinder, target_layers)
# Set layer constraints (forbid layers >= target_layers)
pathfinder.set_layer_constraints(max_layer=target_layers - 1)
# Rip up deep nets
for net_id in deep_nets:
pathfinder.rip_up_net(net_id)
# Reroute with layer constraint
pathfinder.route_multiple_nets(deep_nets, max_iterations=20)
# Verify all nets routed within target layers
verify_layer_compliance(pathfinder, target_layers)
```
---
## Algorithm Pseudocode
### Complete Layer Compaction Flow
```python
def compact_layers(pathfinder, board, min_layer_util=0.05):
"""
Post-routing layer compaction to minimize PCB layer count.
Args:
pathfinder: UnifiedPathFinder instance with completed routing
board: Board object
min_layer_util: Minimum utilization to consider layer "used"
Returns:
CompactionResult with before/after metrics
"""
# ===== PHASE 1: ANALYSIS =====
print("[COMPACT] Phase 1: Analyzing layer utilization...")
layer_stats = analyze_layer_utilization(pathfinder)
original_layers = pathfinder.lattice.Nz
# Find minimal layer count needed
target_layers = find_minimal_layer_count(layer_stats, min_layer_util)
if target_layers >= original_layers - 2:
print(f"[COMPACT] Board already optimal: using {original_layers} layers efficiently")
return CompactionResult(success=False, reason="Already optimized")
print(f"[COMPACT] Target: Reduce from {original_layers} to {target_layers} layers")
print(f"[COMPACT] Potential savings: {(original_layers - target_layers) * 2} layers")
# ===== PHASE 2: CONSERVATIVE MIGRATION =====
print("[COMPACT] Phase 2: Conservative migration (no rerouting)...")
mig_attempted, mig_succeeded = 0, 0
for source_layer in range(target_layers, original_layers):
segments = get_all_segments_on_layer(pathfinder, source_layer)
print(f"[COMPACT] Layer {source_layer}: {len(segments)} segments to migrate")
for net_id, segment in segments:
# Find suitable target layer
direction = 'H' if segment['x1'] != segment['x2'] else 'V'
target = find_target_layer(
direction,
max_layer=target_layers - 1,
layer_stats=layer_stats
)
if target is None:
continue
mig_attempted += 1
# Check if migration is safe (no DRC violations)
if can_migrate_safely(pathfinder, net_id, segment, target):
migrate_segment_to_layer(pathfinder, net_id, segment, target)
mig_succeeded += 1
print(f"[COMPACT] Conservative migration: {mig_succeeded}/{mig_attempted} succeeded")
# ===== PHASE 3: AGGRESSIVE REROUTING =====
print("[COMPACT] Phase 3: Aggressive rerouting for remaining deep nets...")
# Find nets still using layers >= target_layers
deep_nets = []
for net_id, path in pathfinder.net_paths.items():
if uses_layers_above(path, target_layers):
deep_nets.append(net_id)
if not deep_nets:
print("[COMPACT] All nets migrated successfully!")
return finalize_compaction(pathfinder, target_layers)
print(f"[COMPACT] {len(deep_nets)} nets still use deep layers - attempting reroute")
# Set layer mask (forbid layers >= target_layers)
original_mask = pathfinder.get_layer_mask()
restricted_mask = create_layer_mask(max_layer=target_layers - 1)
pathfinder.set_layer_mask(restricted_mask)
# Rip up and reroute with layer constraint
for net_id in deep_nets:
pathfinder.rip_up_net(net_id)
reroute_result = pathfinder.route_multiple_nets(
deep_nets,
max_iterations=30,
strict_layer_compliance=True
)
# Restore original mask
pathfinder.set_layer_mask(original_mask)
# ===== PHASE 4: VERIFICATION =====
print("[COMPACT] Phase 4: Verification...")
failed_nets = verify_layer_compliance(pathfinder, target_layers)
if failed_nets:
print(f"[COMPACT] WARNING: {len(failed_nets)} nets could not be migrated")
print(f"[COMPACT] Best achievable: {max_layer_used(pathfinder) + 1} layers")
return CompactionResult(
success=False,
achieved_layers=max_layer_used(pathfinder) + 1,
failed_nets=failed_nets
)
# Success!
actual_layers = max_layer_used(pathfinder) + 1
layers_saved = original_layers - actual_layers
print(f"[COMPACT] ✓ SUCCESS! Reduced from {original_layers} to {actual_layers} layers")
print(f"[COMPACT] Savings: {layers_saved} layers (~${layers_saved * 25} per board)")
return CompactionResult(
success=True,
original_layers=original_layers,
final_layers=actual_layers,
layers_saved=layers_saved,
migrations=mig_succeeded
)
```
---
## Integration with PathFinder
### Where to Hook In
**Option A: Automatic post-routing**
```python
# In unified_pathfinder.py, after emit_geometry():
if config.enable_layer_compaction:
compaction_result = compact_layers(self, board)
if compaction_result.success:
# Re-emit geometry with compacted layers
self.emit_geometry(board)
```
**Option B: Manual trigger (GUI)**
```python
# In main_window.py, add menu item:
def optimize_layer_usage(self):
"""Layer compaction post-processing"""
if not self.router:
return
# Show analysis dialog
layer_stats = analyze_layer_utilization(self.router)
msg = f"Current layers: {self.board_data['layer_count']}\n"
msg += f"Estimated minimal: {find_minimal_layer_count(layer_stats)}\n"
msg += f"\nProceed with layer compaction?"
if QMessageBox.question(self, "Layer Compaction", msg) == QMessageBox.Yes:
result = compact_layers(self.router, target_layers)
# Update display
self.pcb_viewer.update()
```
**Option C: Headless flag**
```bash
python main.py headless board.ORP --compact-layers 20
```
---
## Expected Results
### Test Case: 18-Layer Backplane
**Before compaction:**
```
Layer utilization:
Layers 0-10: 60-80% utilized
Layers 11-15: 20-40% utilized
Layers 16-17: <10% utilized
Total: 18 layers
```
**After compaction:**
```
Layer utilization:
Layers 0-10: 65-85% utilized (slight increase)
Layers 11-15: 35-55% utilized (moderate increase)
Layers 16-17: ELIMINATED
Total: 16 layers (saved 2 layers = ~$50 per board)
```
### Performance Impact
**Expected changes:**
- **Wirelength:** +2-5% (longer paths to avoid congestion)
- **Via count:** +5-10% (more layer transitions)
- **Routing time:** +10-20% (additional rerouting pass)
- **DRC violations:** 0 (must maintain zero violations)
**Trade-off analysis:**
- Small increases in wirelength/vias
- Significant cost savings for production
---
## Implementation Roadmap
### Phase 1: Analysis Tools (1-2 hours)
- [ ] `analyze_layer_utilization()` - Compute per-layer stats
- [ ] `find_minimal_layer_count()` - Determine target
- [ ] `generate_utilization_report()` - Text/CSV output
- [ ] Add to GUI: "Analyze Layer Usage" button
**Deliverable:** Users can see layer utilization without running compaction.
### Phase 2: Simple Migration (3-4 hours)
- [ ] `find_segments_on_layer()` - Extract segments by layer
- [ ] `find_migration_target()` - Find target layer respecting H/V
- [ ] `migrate_segment_to_layer()` - Reassign without rerouting
- [ ] `verify_layer_compliance()` - Check all segments within target
**Deliverable:** Conservative migration for obvious wins.
### Phase 3: PathFinder Integration (4-6 hours)
- [ ] `set_layer_mask()` - Constrain routing to specific layers
- [ ] `rip_up_net()` - Remove net path from graph
- [ ] Modify `_route_single_net()` to respect layer mask
- [ ] Add layer constraint to cost function
**Deliverable:** Can reroute nets with layer restrictions.
### Phase 4: Full Compaction Algorithm (6-8 hours)
- [ ] Implement complete `compact_layers()` function
- [ ] Add conflict resolution (barrel conflicts, DRC)
- [ ] Iterative refinement loop
- [ ] Performance metrics and reporting
**Deliverable:** Complete layer compaction feature.
### Phase 5: UI and Testing (2-3 hours)
- [ ] GUI menu item: "Optimize Layer Count..."
- [ ] Progress dialog during compaction
- [ ] Before/after comparison view
- [ ] Test on multiple board sizes
**Total estimated time: 16-23 hours**
---
## Open Questions
### 1. Blind/Buried Via Support
Current OrthoRoute uses **through-hole vias** only. Layer compaction could benefit from:
- **Blind vias:** F.Cu → In10.Cu (doesn't drill to B.Cu)
- **Buried vias:** In5.Cu → In15.Cu (doesn't reach outer layers)
**Trade-off:** Adds complexity and cost, but enables better layer utilization.
**Recommendation:** Phase 1 implementation assumes through-hole vias only. Add blind/buried support in Phase 2.
### 2. Layer Assignment During Routing
Alternative approach: Instead of post-processing, integrate layer bias into routing:
```python
# In PathFinder cost function:
layer_depth_penalty = lambda z: (z / max_z) ** 2 * depth_weight
# Prefer upper layers during routing
edge_cost += layer_depth_penalty(edge.layer) * 0.5
```
**Pros:** More natural, better integration
**Cons:** Might sacrifice routability
**Recommendation:** Try both approaches, compare results.
### 3. What if Compaction Fails?
If aggressive rerouting can't fit all nets in target layers:
**Options:**
1. **Accept partial compaction** - Reduce 32 → 28 instead of 32 → 20
2. **Relax constraints incrementally** - Try 24 layers, then 26, then 28...
3. **Report failure** - User decides whether to accept current routing
**Recommendation:** Implement progressive fallback (try 20, then 22, then 24...).
### 4. Signal Integrity Impact
Moving high-speed signals to different layers affects:
- **Impedance:** Different layer stackup positions have different impedance
- **Crosstalk:** Moving closer to other signals increases coupling
- **Return paths:** Ground/power plane proximity changes
**Recommendation:**
- Phase 1: Ignore SI (optimize for layer count only)
- Phase 2: Add SI constraints (don't migrate critical nets)
---
## Cost-Benefit Analysis
### PCB Fabrication Cost vs. Layer Count
**Typical pricing (6-layer baseline):**
```
6 layers: $100 per board
8 layers: $125 per board (+25%)
10 layers: $160 per board (+60%)
12 layers: $200 per board (+100%)
14 layers: $250 per board (+150%)
16 layers: $320 per board (+220%)
18 layers: $400 per board (+300%)
20 layers: $490 per board (+390%)
...
32 layers: $1200 per board (+1100%)
```
**Savings example:**
- Compacting 32 → 24 layers: **~$400 per board saved**
- Production run of 100 boards: **$40,000 total savings**
### When Layer Compaction Makes Sense
**Good candidates:**
- ✅ Boards routed with "safety margin" (32 layers available, only 20 needed)
- ✅ Prototype-to-production transition (optimize for cost)
- ✅ Low-speed backplanes (no critical SI requirements)
- ✅ Large production runs (optimization cost amortized)
**Poor candidates:**
- ❌ Already tightly routed (no free capacity)
- ❌ High-speed designs (SI constraints dominate)
- ❌ One-off prototypes (optimization time not worth it)
- ❌ Mixed signal boards (analog/power layers can't be compacted)
---
## Research References
Based on web search and VLSI literature:
1. **PathFinder Algorithm (McMurchie & Ebeling, 1995)**
- Negotiated congestion routing for FPGAs
- Iterative rip-up and reroute
- OrthoRoute's foundation algorithm
2. **Layer Assignment in VLSI**
- "Layer assignment is a desirable intermediate step between global routing and detailed routing"
- Typically done as 2D routing + layer assignment (BoxRouter 2.0)
- Machine learning approaches for optimal layer ordering
3. **Track Assignment Algorithms**
- SPTA 2.0: Scalable parallel ILP-based track assignment
- RDTA: Routability-driven track assignment
- Can optimize for layer utilization
4. **PCB Industry Practice**
- Layer minimization typically done during design phase
- Manual optimization by expert layout engineers
- Post-routing layer reduction not common in commercial tools
---
## Proposed Feature Specification
### User Interface
**Menu Item:** `Route → Optimize Layer Count...`
**Dialog:**
```
┌─────────────────────────────────────────────────┐
│ Layer Compaction Analysis │
├─────────────────────────────────────────────────┤
│ │
│ Current layer count: 32 layers │
│ Estimated minimal: 22 layers │
│ │
│ Layer utilization: │
│ Layers 0-15: 60-85% ████████████░░ │
│ Layers 16-21: 15-40% ███░░░░░░░░░░ │
│ Layers 22-31: < 5% █░░░░░░░░░░░░ │
│ │
│ Estimated impact: │
│ Wirelength increase: ~3% │
│ Via count increase: ~50 vias │
│ Layer savings: 10 layers │
│ Cost savings: ~$250 per board │
│ │
│ Target layer count: [22] (editable) │
│ │
│ [Cancel] [Proceed with Compaction] │
└─────────────────────────────────────────────────┘
```
**Progress Display:**
```
Compacting layers...
Phase 1: Analyzing utilization... ✓
Phase 2: Conservative migration... 150/200 segments migrated
Phase 3: Rerouting deep nets... 15/20 nets rerouted
Phase 4: Verification... ✓
Result: Successfully compacted to 22 layers!
```
### Command-Line Interface
```bash
# Analyze only (no modification)
python main.py headless board.ORP --analyze-layers
# Compact to target
python main.py headless board.ORP --compact-layers 20
# Compact to minimum
python main.py headless board.ORP --compact-layers auto
```
---
## Success Metrics
### How to Measure Success
**Primary metrics:**
-**Layer reduction:** Original layers → Final layers
-**Routability maintained:** 100% nets still routed
-**DRC compliance:** Zero violations
**Secondary metrics:**
- ⚠️ **Wirelength delta:** % increase (acceptable: <5%)
- **Via count delta:** # additional vias (acceptable: <10%)
- **Runtime:** Time spent on compaction (acceptable: <20% of routing time)
**Cost metrics:**
- 💰 **Fabrication cost saved:** $ per board
- 💰 **Break-even point:** # of boards to justify development effort
### Example Results
**Test board: 512-net backplane**
```
Before compaction:
Layers: 18
Tracks: 4088
Vias: 2552
Utilization: Layers 0-12 @ 70%, Layers 13-17 @ 15%
After compaction:
Layers: 14 (saved 4 layers)
Tracks: 4203 (+2.8% wirelength)
Vias: 2687 (+5.3% vias)
Utilization: Layers 0-13 @ 55-80%
Cost savings: $100 per board × 50 boards = $5,000
Runtime: +4.2 minutes (18% of routing time)
```
---
## Future Enhancements
### Advanced Optimizations
**1. Differential Pair Awareness**
- Keep diff pairs on same layer
- Migrate pairs together
- Maintain length matching
**2. Impedance Control**
- Preserve impedance-controlled layers
- Don't migrate high-speed signals to incompatible stackup positions
**3. Power Integrity**
- Don't migrate traces that need proximity to power/ground planes
- Preserve return path quality
**4. Thermal Considerations**
- Hot components might need specific layer routing
- Thermal vias can't be arbitrarily reassigned
**5. Manufacturing Constraints**
- Aspect ratio limits (drill depth / hole diameter)
- Via-in-pad restrictions
- Annular ring requirements
---
## Comparison to Other Approaches
### Layer Assignment During Global Routing (BoxRouter 2.0)
**Approach:** 2D routing first, then assign layers
**Pros:** Can optimize layer count explicitly
**Cons:** PathFinder is already 3D - would require major rewrite
### Via Minimization (Different Problem)
**Goal:** Reduce via count (not layer count)
**Method:** Prefer longer runs on same layer
**Relation:** Complementary to layer compaction
### Layer Swapping (FPGA-specific)
**Context:** FPGAs have limited routing tracks per layer
**Method:** Swap track assignments between layers
**Applicability:** Limited for PCB (different constraints)
---
## Conclusion
**Layer compaction is feasible for PathFinder-routed boards** and could provide significant cost savings for production runs.
**Recommended implementation priority:**
1. **High priority:** Analysis tools (show layer utilization)
2. **Medium priority:** Conservative migration (low-hanging fruit)
3. **Low priority:** Aggressive rerouting (complex, diminishing returns)
**Best use case:** Post-routing optimization for production cost reduction when board has been over-provisioned with layers during design.
**Next steps:**
1. Implement `analyze_layer_utilization()` function
2. Add "Layer Utilization Report" to GUI
3. Prototype conservative migration on test boards
4. Evaluate cost/benefit on real designs
---
**Status:** Ready for implementation
**Estimated development time:** 2-3 weeks for complete feature
**Expected ROI:** High for production boards with >50 unit runs
---
## Appendix: Mathematical Formulation
### Layer Assignment as Optimization Problem
**Variables:**
- `x[s,l]` = 1 if segment `s` assigned to layer `l`, 0 otherwise
**Objective:**
```
Minimize: max_layer_used
Subject to:
- Each segment assigned to exactly one layer
- Layer capacity not exceeded
- H/V direction constraints satisfied
- Via barrel conflicts = 0
- Connectivity maintained (segment endpoints match via layers)
```
**Complexity:** NP-hard (reduction from graph coloring)
**Practical solution:** Greedy heuristics + local search
---
**Last updated:** November 14, 2025
**Author:** Research based on VLSI routing literature and PathFinder algorithm analysis
**Status:** Proposed feature for OrthoRoute v2.0
--- Approach 1: Detailed Implementation ---