optimizing a gate level bcm to the end of the earth and back

Update cost model: inputs/complements are free

- Single-literal terms (A, B, B', etc.) don't need AND gates - they're
direct wires from inputs to OR gates, cost = 0 AND inputs
- Multi-literal terms still need AND gates with literal count inputs
- Added CostBreakdown showing AND inputs, OR inputs, and totals
- Updated MaxSAT penalty to only count multi-literal terms
- DOT export now shows single-literal terms as direct wires, not AND gates

New cost breakdown for optimized result:
AND gate inputs: 15 (7 gates)
OR gate inputs: 38 (7 gates)
Total: 53 gate inputs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

dunkirk.sh 5443f07c b12c1dcd

verified
+135 -35
+1 -1
bcd_optimization/__init__.py
··· 1 1 """BCD to 7-segment decoder optimization using SAT-based exact synthesis.""" 2 2 3 - from .solver import BCDTo7SegmentSolver, SynthesisResult 3 + from .solver import BCDTo7SegmentSolver, SynthesisResult, CostBreakdown 4 4 from .truth_tables import SEGMENT_TRUTH_TABLES, SEGMENT_NAMES, SEGMENT_MINTERMS 5 5 from .quine_mccluskey import Implicant, quine_mccluskey, quine_mccluskey_multi_output 6 6 from .export import to_verilog, to_c_code, to_equations, to_dot
+41 -24
bcd_optimization/export.py
··· 285 285 lines.append(' }') 286 286 lines.append("") 287 287 288 - # AND gates for shared product terms 289 - lines.append(" // Shared AND gates (product terms)") 290 - lines.append(' subgraph cluster_and {') 291 - lines.append(' label="Product Terms";') 292 - lines.append(' style=dashed;') 293 - lines.append(' color=gray;') 288 + # AND gates for shared product terms (only multi-literal terms need AND gates) 289 + # Single-literal terms are just wires from input to OR 290 + multi_literal_shared = [(i, impl, outputs) for i, (impl, outputs) in enumerate(result.shared_implicants) if impl.num_literals >= 2] 291 + single_literal_shared = [(i, impl, outputs) for i, (impl, outputs) in enumerate(result.shared_implicants) if impl.num_literals < 2] 292 + 293 + if multi_literal_shared: 294 + lines.append(" // Shared AND gates (multi-literal product terms)") 295 + lines.append(' subgraph cluster_and {') 296 + lines.append(' label="Product Terms";') 297 + lines.append(' style=dashed;') 298 + lines.append(' color=gray;') 294 299 295 - for i, (impl, outputs) in enumerate(result.shared_implicants): 296 - term_label = impl.to_expr_str() 297 - lines.append(f' and_{i} [shape=polygon, sides=4, style=filled, fillcolor=lightgreen, label="AND\\n{term_label}"];') 298 - lines.append(' }') 299 - lines.append("") 300 + for i, impl, outputs in multi_literal_shared: 301 + term_label = impl.to_expr_str() 302 + lines.append(f' and_{i} [shape=polygon, sides=4, style=filled, fillcolor=lightgreen, label="AND\\n{term_label}"];') 303 + lines.append(' }') 304 + lines.append("") 300 305 301 - # Connect inputs to AND gates 302 - lines.append(" // Input to AND connections") 303 - for i, (impl, _) in enumerate(result.shared_implicants): 304 - for j, var in enumerate(['A', 'B', 'C', 'D']): 305 - bit = 1 << (3 - j) 306 - if impl.mask & bit: 307 - if (impl.value >> (3 - j)) & 1: 308 - lines.append(f' {var} -> and_{i};') 309 - else: 310 - lines.append(f' n{var} -> and_{i};') 311 - lines.append("") 306 + # Connect inputs to AND gates 307 + lines.append(" // Input to AND connections") 308 + for i, impl, _ in multi_literal_shared: 309 + for j, var in enumerate(['A', 'B', 'C', 'D']): 310 + bit = 1 << (3 - j) 311 + if impl.mask & bit: 312 + if (impl.value >> (3 - j)) & 1: 313 + lines.append(f' {var} -> and_{i};') 314 + else: 315 + lines.append(f' n{var} -> and_{i};') 316 + lines.append("") 312 317 313 318 # OR gates for outputs 314 319 lines.append(" // Output OR gates") ··· 321 326 lines.append(' }') 322 327 lines.append("") 323 328 324 - # Connect AND gates to OR gates 329 + # Connect AND gates to OR gates (multi-literal shared terms) 325 330 lines.append(" // AND to OR connections") 326 - for i, (impl, outputs) in enumerate(result.shared_implicants): 331 + for i, impl, outputs in multi_literal_shared: 327 332 for segment in outputs: 328 333 lines.append(f' and_{i} -> or_{segment};') 329 334 lines.append("") 335 + 336 + # Connect single-literal shared terms directly from inputs to OR gates 337 + if single_literal_shared: 338 + lines.append(" // Single-literal terms (direct wires)") 339 + for i, impl, outputs in single_literal_shared: 340 + for j, var in enumerate(['A', 'B', 'C', 'D']): 341 + bit = 1 << (3 - j) 342 + if impl.mask & bit: 343 + src = var if (impl.value >> (3 - j)) & 1 else f"n{var}" 344 + for segment in outputs: 345 + lines.append(f' {src} -> or_{segment};') 346 + lines.append("") 330 347 331 348 # Handle non-shared terms (direct connections or inline ANDs) 332 349 lines.append(" // Non-shared terms")
+93 -10
bcd_optimization/solver.py
··· 20 20 21 21 22 22 @dataclass 23 + class CostBreakdown: 24 + """Detailed cost breakdown for a synthesis result.""" 25 + 26 + and_inputs: int # Inputs to AND gates (multi-literal product terms only) 27 + or_inputs: int # Inputs to OR gates (one per term per output) 28 + num_and_gates: int # Number of AND gates (multi-literal terms) 29 + num_or_gates: int # Number of OR gates (one per output = 7) 30 + 31 + @property 32 + def total(self) -> int: 33 + """Total gate inputs (AND + OR).""" 34 + return self.and_inputs + self.or_inputs 35 + 36 + 37 + @dataclass 23 38 class SynthesisResult: 24 39 """Result of logic synthesis optimization.""" 25 40 26 - cost: int # Total gate inputs 41 + cost: int # Total gate inputs (for backward compat, = cost_breakdown.and_inputs) 27 42 implicants_by_output: dict[str, list[Implicant]] 28 43 shared_implicants: list[tuple[Implicant, list[str]]] 29 44 method: str 30 45 expressions: dict[str, str] = field(default_factory=dict) 46 + cost_breakdown: CostBreakdown = None 31 47 32 48 33 49 class BCDTo7SegmentSolver: ··· 45 61 self.minterms = {s: set(SEGMENT_MINTERMS[s]) for s in SEGMENT_NAMES} 46 62 self.dc_set = set(DONT_CARES) 47 63 64 + def _compute_cost_breakdown( 65 + self, 66 + selected: list[Implicant], 67 + implicants_by_output: dict[str, list[Implicant]] 68 + ) -> CostBreakdown: 69 + """ 70 + Compute detailed cost breakdown for a set of selected implicants. 71 + 72 + Cost model (assuming input complements are free): 73 + - AND gate inputs: Only for multi-literal terms (2+ literals) 74 + Single literals (A, B', etc.) are direct wires, no AND needed 75 + - OR gate inputs: One per term per output it feeds 76 + - AND gates: One per multi-literal term (shared across outputs) 77 + - OR gates: One per output (7 total) 78 + """ 79 + and_inputs = 0 80 + num_and_gates = 0 81 + 82 + for impl in selected: 83 + if impl.num_literals >= 2: 84 + # Multi-literal term needs an AND gate 85 + and_inputs += impl.num_literals 86 + num_and_gates += 1 87 + # Single-literal terms are just wires (no AND gate cost) 88 + 89 + # OR inputs: count terms feeding each output 90 + or_inputs = sum( 91 + len(implicants_by_output[seg]) 92 + for seg in SEGMENT_NAMES 93 + if seg in implicants_by_output 94 + ) 95 + 96 + return CostBreakdown( 97 + and_inputs=and_inputs, 98 + or_inputs=or_inputs, 99 + num_and_gates=num_and_gates, 100 + num_or_gates=7, 101 + ) 102 + 48 103 def greedy_baseline(self) -> SynthesisResult: 49 104 """ 50 105 Phase 1: Establish baseline using greedy set cover. ··· 73 128 terms = [impl.to_expr_str() for impl in implicants_by_output[segment]] 74 129 expressions[segment] = " + ".join(terms) if terms else "0" 75 130 131 + # Compute detailed cost breakdown 132 + cost_breakdown = self._compute_cost_breakdown(selected, implicants_by_output) 133 + 76 134 return SynthesisResult( 77 - cost=cost, 135 + cost=cost_breakdown.and_inputs, # Primary cost = AND inputs only 78 136 implicants_by_output=implicants_by_output, 79 137 shared_implicants=shared, 80 138 method="greedy", 81 139 expressions=expressions, 140 + cost_breakdown=cost_breakdown, 82 141 ) 83 142 84 143 def generate_prime_implicants(self) -> list[Implicant]: ··· 122 181 f"No implicant covers {segment}:{minterm}" 123 182 ) 124 183 125 - # Soft constraints: penalize each implicant by its literal count 184 + # Soft constraints: penalize each implicant by its AND gate cost 185 + # Single-literal terms (direct wires) have 0 AND cost 186 + # Multi-literal terms cost their literal count (AND gate inputs) 126 187 for i, impl in enumerate(self.prime_implicants): 127 - wcnf.append([-impl_vars[i]], weight=impl.num_literals) 188 + and_cost = impl.num_literals if impl.num_literals >= 2 else 0 189 + if and_cost > 0: 190 + wcnf.append([-impl_vars[i]], weight=and_cost) 128 191 129 192 # Solve 130 193 with RC2(wcnf) as solver: ··· 138 201 if impl_vars[i] in model: 139 202 selected.append(impl) 140 203 141 - # Calculate actual cost 142 - cost = sum(impl.num_literals for impl in selected) 143 - 144 204 # Organize by output 145 205 implicants_by_output = {s: [] for s in SEGMENT_NAMES} 146 206 shared = [] ··· 158 218 terms = [impl.to_expr_str() for impl in implicants_by_output[segment]] 159 219 expressions[segment] = " + ".join(terms) if terms else "0" 160 220 221 + # Compute detailed cost breakdown 222 + cost_breakdown = self._compute_cost_breakdown(selected, implicants_by_output) 223 + 161 224 return SynthesisResult( 162 - cost=cost, 225 + cost=cost_breakdown.and_inputs, # Primary cost = AND inputs only 163 226 implicants_by_output=implicants_by_output, 164 227 shared_implicants=shared, 165 228 method="maxsat", 166 229 expressions=expressions, 230 + cost_breakdown=cost_breakdown, 167 231 ) 168 232 169 233 def exact_synthesis(self, max_gates: int = 15) -> SynthesisResult: ··· 322 386 expressions[segment] = node_names[i] 323 387 break 324 388 389 + # For exact synthesis, all gates are 2-input gates 390 + # This is a different circuit topology than SOP 391 + cost_breakdown = CostBreakdown( 392 + and_inputs=num_gates * 2, # All gates treated as "AND-like" 393 + or_inputs=0, # No separate OR level in multi-level 394 + num_and_gates=num_gates, 395 + num_or_gates=0, 396 + ) 397 + 325 398 return SynthesisResult( 326 399 cost=num_gates * 2, # 2 inputs per 2-input gate 327 400 implicants_by_output={}, 328 401 shared_implicants=[], 329 402 method=f"exact_{num_gates}gates", 330 403 expressions=expressions, 404 + cost_breakdown=cost_breakdown, 331 405 ) 332 406 333 407 def _decode_gate_function(self, func: int) -> str: ··· 405 479 print(f"\n{'=' * 60}") 406 480 print(f"Synthesis Result: {result.method}") 407 481 print(f"{'=' * 60}") 408 - print(f"Total gate inputs: {result.cost}") 482 + 483 + if result.cost_breakdown: 484 + cb = result.cost_breakdown 485 + print(f"Cost breakdown:") 486 + print(f" AND gate inputs: {cb.and_inputs} ({cb.num_and_gates} gates)") 487 + print(f" OR gate inputs: {cb.or_inputs} (7 gates)") 488 + print(f" Total: {cb.total} gate inputs") 489 + else: 490 + print(f"Total gate inputs: {result.cost}") 409 491 410 492 if result.shared_implicants: 411 493 print(f"\nShared terms ({len(result.shared_implicants)}):") 412 494 for impl, outputs in result.shared_implicants: 413 - print(f" {impl.to_expr_str():12} -> {', '.join(outputs)}") 495 + lit_info = f"({impl.num_literals} lit)" if impl.num_literals >= 2 else "(wire)" 496 + print(f" {impl.to_expr_str():12} {lit_info:8} -> {', '.join(outputs)}") 414 497 415 498 print("\nExpressions:") 416 499 for segment in SEGMENT_NAMES: