···599599 }
600600 return known.get(func, f"F4_{func:016b}")
601601602602+ def _build_general_cnf(self, num_2input: int, num_3input: int, num_4input: int,
603603+ use_complements: bool = True, restrict_functions: bool = True) -> Optional[dict]:
604604+ """Build CNF for general synthesis without solving. Returns CNF + metadata for decoding."""
605605+ n_primary = 4
606606+ n_inputs = 8 if use_complements else 4
607607+ n_outputs = 7
608608+ n_gates = num_2input + num_3input + num_4input
609609+ n_nodes = n_inputs + n_gates
610610+611611+ truth_rows = list(range(10))
612612+ n_rows = len(truth_rows)
613613+614614+ clauses = []
615615+ var_counter = [1]
616616+617617+ def new_var():
618618+ v = var_counter[0]
619619+ var_counter[0] += 1
620620+ return v
621621+622622+ # x[i][t] = output of node i on row t
623623+ x = {i: {t: new_var() for t in range(n_rows)} for i in range(n_nodes)}
624624+625625+ # Selection and function variables
626626+ s2, s3, s4 = {}, {}, {}
627627+ f2, f3, f4 = {}, {}, {}
628628+629629+ gate_sizes = [2] * num_2input + [3] * num_3input + [4] * num_4input
630630+631631+ for gate_idx in range(n_gates):
632632+ i = n_inputs + gate_idx
633633+ size = gate_sizes[gate_idx]
634634+635635+ if size == 2:
636636+ s2[i] = {}
637637+ for j in range(i):
638638+ s2[i][j] = {k: new_var() for k in range(j + 1, i)}
639639+ f2[i] = {p: {q: new_var() for q in range(2)} for p in range(2)}
640640+ elif size == 3:
641641+ s3[i] = {}
642642+ for j in range(i):
643643+ s3[i][j] = {}
644644+ for k in range(j + 1, i):
645645+ s3[i][j][k] = {l: new_var() for l in range(k + 1, i)}
646646+ f3[i] = {p: {q: {r: new_var() for r in range(2)} for q in range(2)} for p in range(2)}
647647+ else:
648648+ s4[i] = {}
649649+ for j in range(i):
650650+ s4[i][j] = {}
651651+ for k in range(j + 1, i):
652652+ s4[i][j][k] = {}
653653+ for l in range(k + 1, i):
654654+ s4[i][j][k][l] = {m: new_var() for m in range(l + 1, i)}
655655+ f4[i] = {p: {q: {r: {s: new_var() for s in range(2)} for r in range(2)} for q in range(2)} for p in range(2)}
656656+657657+ g = {h: {i: new_var() for i in range(n_nodes)} for h in range(n_outputs)}
658658+659659+ # Constraint 1: Primary inputs
660660+ for t_idx, t in enumerate(truth_rows):
661661+ for i in range(n_primary):
662662+ bit = (t >> (n_primary - 1 - i)) & 1
663663+ clauses.append([x[i][t_idx] if bit else -x[i][t_idx]])
664664+ if use_complements:
665665+ for i in range(n_primary):
666666+ bit = (t >> (n_primary - 1 - i)) & 1
667667+ clauses.append([x[n_primary + i][t_idx] if not bit else -x[n_primary + i][t_idx]])
668668+669669+ # Constraint 2: Each gate has exactly one input selection
670670+ for gate_idx in range(n_gates):
671671+ i = n_inputs + gate_idx
672672+ size = gate_sizes[gate_idx]
673673+674674+ if size == 2:
675675+ all_sels = [s2[i][j][k] for j in range(i) for k in range(j + 1, i)]
676676+ elif size == 3:
677677+ all_sels = [s3[i][j][k][l] for j in range(i) for k in range(j + 1, i) for l in range(k + 1, i)]
678678+ else:
679679+ all_sels = [s4[i][j][k][l][m] for j in range(i) for k in range(j + 1, i) for l in range(k + 1, i) for m in range(l + 1, i)]
680680+681681+ if not all_sels:
682682+ return None
683683+684684+ clauses.append(all_sels)
685685+ for idx1, sel1 in enumerate(all_sels):
686686+ for sel2 in all_sels[idx1 + 1:]:
687687+ clauses.append([-sel1, -sel2])
688688+689689+ # Constraint 3: Gate function consistency
690690+ for gate_idx in range(n_gates):
691691+ i = n_inputs + gate_idx
692692+ size = gate_sizes[gate_idx]
693693+694694+ if size == 2:
695695+ for j in range(i):
696696+ for k in range(j + 1, i):
697697+ for t_idx in range(n_rows):
698698+ for pv in range(2):
699699+ for qv in range(2):
700700+ for outv in range(2):
701701+ clause = [-s2[i][j][k]]
702702+ clause.append(-x[j][t_idx] if pv else x[j][t_idx])
703703+ clause.append(-x[k][t_idx] if qv else x[k][t_idx])
704704+ clause.append(-f2[i][pv][qv] if outv else f2[i][pv][qv])
705705+ clause.append(x[i][t_idx] if outv else -x[i][t_idx])
706706+ clauses.append(clause)
707707+ elif size == 3:
708708+ for j in range(i):
709709+ for k in range(j + 1, i):
710710+ for l in range(k + 1, i):
711711+ for t_idx in range(n_rows):
712712+ for pv in range(2):
713713+ for qv in range(2):
714714+ for rv in range(2):
715715+ for outv in range(2):
716716+ clause = [-s3[i][j][k][l]]
717717+ clause.append(-x[j][t_idx] if pv else x[j][t_idx])
718718+ clause.append(-x[k][t_idx] if qv else x[k][t_idx])
719719+ clause.append(-x[l][t_idx] if rv else x[l][t_idx])
720720+ clause.append(-f3[i][pv][qv][rv] if outv else f3[i][pv][qv][rv])
721721+ clause.append(x[i][t_idx] if outv else -x[i][t_idx])
722722+ clauses.append(clause)
723723+ else:
724724+ for j in range(i):
725725+ for k in range(j + 1, i):
726726+ for l in range(k + 1, i):
727727+ for m in range(l + 1, i):
728728+ for t_idx in range(n_rows):
729729+ for pv in range(2):
730730+ for qv in range(2):
731731+ for rv in range(2):
732732+ for sv in range(2):
733733+ for outv in range(2):
734734+ clause = [-s4[i][j][k][l][m]]
735735+ clause.append(-x[j][t_idx] if pv else x[j][t_idx])
736736+ clause.append(-x[k][t_idx] if qv else x[k][t_idx])
737737+ clause.append(-x[l][t_idx] if rv else x[l][t_idx])
738738+ clause.append(-x[m][t_idx] if sv else x[m][t_idx])
739739+ clause.append(-f4[i][pv][qv][rv][sv] if outv else f4[i][pv][qv][rv][sv])
740740+ clause.append(x[i][t_idx] if outv else -x[i][t_idx])
741741+ clauses.append(clause)
742742+743743+ # Constraint 3b: Restrict functions
744744+ if restrict_functions:
745745+ allowed_2input = [0b1000, 0b1110, 0b0110, 0b1001, 0b0111, 0b0001]
746746+ allowed_3input = [0b10000000, 0b11111110, 0b01111111, 0b00000001, 0b10010110, 0b01101001]
747747+ allowed_4input = [0x8000, 0xFFFE, 0x7FFF, 0x0001, 0x6996, 0x9669]
748748+749749+ for gate_idx in range(n_gates):
750750+ i = n_inputs + gate_idx
751751+ size = gate_sizes[gate_idx]
752752+753753+ if size == 2:
754754+ or_clause = []
755755+ for func in allowed_2input:
756756+ match_var = new_var()
757757+ or_clause.append(match_var)
758758+ for p in range(2):
759759+ for q in range(2):
760760+ bit_idx = p * 2 + q
761761+ expected = (func >> bit_idx) & 1
762762+ clauses.append([-match_var, f2[i][p][q] if expected else -f2[i][p][q]])
763763+ clauses.append(or_clause)
764764+ elif size == 3:
765765+ or_clause = []
766766+ for func in allowed_3input:
767767+ match_var = new_var()
768768+ or_clause.append(match_var)
769769+ for p in range(2):
770770+ for q in range(2):
771771+ for r in range(2):
772772+ bit_idx = p * 4 + q * 2 + r
773773+ expected = (func >> bit_idx) & 1
774774+ clauses.append([-match_var, f3[i][p][q][r] if expected else -f3[i][p][q][r]])
775775+ clauses.append(or_clause)
776776+ else:
777777+ or_clause = []
778778+ for func in allowed_4input:
779779+ match_var = new_var()
780780+ or_clause.append(match_var)
781781+ for p in range(2):
782782+ for q in range(2):
783783+ for r in range(2):
784784+ for s in range(2):
785785+ bit_idx = p * 8 + q * 4 + r * 2 + s
786786+ expected = (func >> bit_idx) & 1
787787+ clauses.append([-match_var, f4[i][p][q][r][s] if expected else -f4[i][p][q][r][s]])
788788+ clauses.append(or_clause)
789789+790790+ # Constraint 4: Each output assigned to exactly one node
791791+ for h in range(n_outputs):
792792+ clauses.append([g[h][i] for i in range(n_nodes)])
793793+ for i in range(n_nodes):
794794+ for j in range(i + 1, n_nodes):
795795+ clauses.append([-g[h][i], -g[h][j]])
796796+797797+ # Constraint 5: Output correctness
798798+ for h, segment in enumerate(SEGMENT_NAMES):
799799+ for t_idx, t in enumerate(truth_rows):
800800+ expected = 1 if t in SEGMENT_MINTERMS[segment] else 0
801801+ for i in range(n_nodes):
802802+ clauses.append([-g[h][i], x[i][t_idx] if expected else -x[i][t_idx]])
803803+804804+ return {
805805+ 'clauses': clauses,
806806+ 'n_vars': var_counter[0] - 1,
807807+ 'gate_sizes': gate_sizes,
808808+ 'n_inputs': n_inputs,
809809+ 'n_nodes': n_nodes,
810810+ 'use_complements': use_complements,
811811+ 'x': x, 's2': s2, 's3': s3, 's4': s4,
812812+ 'f2': f2, 'f3': f3, 'f4': f4, 'g': g,
813813+ }
814814+815815+ def _decode_general_solution_from_cnf(self, model: set, cnf_data: dict) -> SynthesisResult:
816816+ """Decode a SAT solution using stored CNF metadata."""
817817+ def is_true(var):
818818+ return var in model
819819+820820+ gate_sizes = cnf_data['gate_sizes']
821821+ n_inputs = cnf_data['n_inputs']
822822+ n_nodes = cnf_data['n_nodes']
823823+ use_complements = cnf_data['use_complements']
824824+ s2, s3, s4 = cnf_data['s2'], cnf_data['s3'], cnf_data['s4']
825825+ f2, f3, f4 = cnf_data['f2'], cnf_data['f3'], cnf_data['f4']
826826+ g = cnf_data['g']
827827+828828+ n_gates = len(gate_sizes)
829829+ if use_complements:
830830+ node_names = ['A', 'B', 'C', 'D', "A'", "B'", "C'", "D'"] + [f'g{i}' for i in range(n_gates)]
831831+ else:
832832+ node_names = ['A', 'B', 'C', 'D'] + [f'g{i}' for i in range(n_gates)]
833833+834834+ gates = []
835835+ total_cost = 0
836836+837837+ for gate_idx in range(n_gates):
838838+ i = n_inputs + gate_idx
839839+ size = gate_sizes[gate_idx]
840840+ total_cost += size
841841+842842+ if size == 2:
843843+ for j in range(i):
844844+ for k in range(j + 1, i):
845845+ if is_true(s2[i][j][k]):
846846+ func = sum((1 << (p * 2 + q)) for p in range(2) for q in range(2) if is_true(f2[i][p][q]))
847847+ func_name = self._decode_gate_function(func)
848848+ gates.append(GateInfo(index=gate_idx, input1=j, input2=k, func=func, func_name=func_name))
849849+ node_names[i] = f"({node_names[j]} {func_name} {node_names[k]})"
850850+ break
851851+ elif size == 3:
852852+ for j in range(i):
853853+ for k in range(j + 1, i):
854854+ for l in range(k + 1, i):
855855+ if is_true(s3[i][j][k][l]):
856856+ func = sum((1 << (p * 4 + q * 2 + r)) for p in range(2) for q in range(2) for r in range(2) if is_true(f3[i][p][q][r]))
857857+ func_name = self._decode_3input_function(func)
858858+ gates.append(GateInfo(index=gate_idx, input1=j, input2=(k, l), func=func, func_name=func_name))
859859+ node_names[i] = f"({node_names[j]} {func_name} {node_names[k]} {node_names[l]})"
860860+ break
861861+ else:
862862+ for j in range(i):
863863+ for k in range(j + 1, i):
864864+ for l in range(k + 1, i):
865865+ for m in range(l + 1, i):
866866+ if is_true(s4[i][j][k][l][m]):
867867+ func = sum((1 << (p * 8 + q * 4 + r * 2 + s)) for p in range(2) for q in range(2) for r in range(2) for s in range(2) if is_true(f4[i][p][q][r][s]))
868868+ func_name = self._decode_4input_function(func)
869869+ gates.append(GateInfo(index=gate_idx, input1=j, input2=(k, l, m), func=func, func_name=func_name))
870870+ node_names[i] = f"({node_names[j]} {func_name} {node_names[k]} {node_names[l]} {node_names[m]})"
871871+ break
872872+873873+ output_map = {}
874874+ expressions = {}
875875+ for h, segment in enumerate(SEGMENT_NAMES):
876876+ for i in range(n_nodes):
877877+ if is_true(g[h][i]):
878878+ output_map[segment] = i
879879+ expressions[segment] = node_names[i]
880880+ break
881881+882882+ num_2 = gate_sizes.count(2)
883883+ num_3 = gate_sizes.count(3)
884884+ num_4 = gate_sizes.count(4)
885885+886886+ return SynthesisResult(
887887+ cost=total_cost,
888888+ implicants_by_output={},
889889+ shared_implicants=[],
890890+ method=f"exact_general_{num_2}x2_{num_3}x3_{num_4}x4",
891891+ expressions=expressions,
892892+ cost_breakdown=CostBreakdown(and_inputs=total_cost, or_inputs=0, num_and_gates=n_gates, num_or_gates=0),
893893+ gates=gates,
894894+ output_map=output_map,
895895+ )
896896+602897 def _try_general_synthesis(self, num_2input: int, num_3input: int, num_4input: int,
603898 use_complements: bool = True, restrict_functions: bool = True) -> Optional[SynthesisResult]:
604899 """Try synthesis with a mix of 2, 3, and 4-input gates."""
+349-42
search_with_4input.py
···44"""
5566import multiprocessing as mp
77+from multiprocessing import Manager
78from concurrent.futures import ProcessPoolExecutor, as_completed
89import sys
910import time
1111+import threading
1212+import queue
10131114from bcd_optimization.solver import BCDTo7SegmentSolver
1215from bcd_optimization.truth_tables import SEGMENT_MINTERMS, SEGMENT_NAMES
1616+1717+# ANSI escape codes for terminal control
1818+CLEAR_LINE = "\033[K"
1919+MOVE_UP = "\033[A"
2020+GREEN = "\033[92m"
2121+YELLOW = "\033[93m"
2222+CYAN = "\033[96m"
2323+DIM = "\033[2m"
2424+RESET = "\033[0m"
2525+BOLD = "\033[1m"
132614271515-def try_config(args):
1616- """Try a single (n2, n3, n4) configuration. Run in separate process."""
1717- n2, n3, n4, use_complements, restrict_functions = args
2828+class ProgressDisplay:
2929+ """Async progress display that updates in a separate thread."""
3030+3131+ def __init__(self, stats_queue=None):
3232+ self.lock = threading.Lock()
3333+ self.running = False
3434+ self.thread = None
3535+ self.stats_queue = stats_queue
3636+3737+ # State
3838+ self.completed = 0
3939+ self.total = 0
4040+ self.start_time = 0
4141+ self.last_config = ""
4242+ self.current_cost = 0
4343+ self.message = ""
4444+4545+ # SAT solver stats (aggregated across all running configs)
4646+ self.active_configs = {} # config_str -> stats dict
4747+ self.total_conflicts = 0
4848+ self.total_decisions = 0
4949+ self.total_vars = 0
5050+ self.total_clauses = 0
5151+5252+ def start(self, total, cost):
5353+ """Start the progress display thread."""
5454+ with self.lock:
5555+ self.completed = 0
5656+ self.total = total
5757+ self.start_time = time.time()
5858+ self.last_config = ""
5959+ self.current_cost = cost
6060+ self.message = ""
6161+ self.active_configs = {}
6262+ self.total_conflicts = 0
6363+ self.total_decisions = 0
6464+ self.running = True
6565+6666+ self.thread = threading.Thread(target=self._update_loop, daemon=True)
6767+ self.thread.start()
6868+6969+ def stop(self):
7070+ """Stop the progress display thread."""
7171+ self.running = False
7272+ if self.thread:
7373+ self.thread.join(timeout=0.5)
7474+ # Clear the line
7575+ print(f"\r{CLEAR_LINE}", end="", flush=True)
7676+7777+ def update(self, completed, last_config=""):
7878+ """Update progress state (called from main thread)."""
7979+ with self.lock:
8080+ self.completed = completed
8181+ if last_config:
8282+ self.last_config = last_config
8383+ # Remove completed config from active
8484+ if last_config in self.active_configs:
8585+ del self.active_configs[last_config]
8686+8787+ def set_message(self, msg):
8888+ """Set a temporary message to display."""
8989+ with self.lock:
9090+ self.message = msg
9191+9292+ def _poll_stats(self):
9393+ """Poll stats queue for updates from worker processes."""
9494+ if not self.stats_queue:
9595+ return
9696+9797+ try:
9898+ while True:
9999+ stats = self.stats_queue.get_nowait()
100100+ config = stats.get('config', '')
101101+ with self.lock:
102102+ self.active_configs[config] = stats
103103+ # Aggregate stats
104104+ self.total_conflicts = sum(s.get('conflicts', 0) for s in self.active_configs.values())
105105+ self.total_decisions = sum(s.get('decisions', 0) for s in self.active_configs.values())
106106+ self.total_vars = sum(s.get('vars', 0) for s in self.active_configs.values())
107107+ self.total_clauses = sum(s.get('clauses', 0) for s in self.active_configs.values())
108108+ except:
109109+ pass # Queue empty
110110+111111+ def _update_loop(self):
112112+ """Background thread that updates the display."""
113113+ import shutil
114114+115115+ spinner = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
116116+ spin_idx = 0
117117+ last_conflicts = 0
118118+ conflict_rate = 0
119119+120120+ def fmt_num(n):
121121+ if n >= 1_000_000:
122122+ return f"{n/1_000_000:.1f}M"
123123+ elif n >= 1_000:
124124+ return f"{n/1_000:.1f}K"
125125+ return str(int(n))
126126+127127+ while self.running:
128128+ self._poll_stats()
129129+130130+ # Get terminal width each iteration (in case of resize)
131131+ try:
132132+ term_width = shutil.get_terminal_size().columns
133133+ except:
134134+ term_width = 80
135135+136136+ with self.lock:
137137+ elapsed = time.time() - self.start_time
138138+ speed = self.completed / elapsed if elapsed > 0 else 0
139139+ remaining = self.total - self.completed
140140+ eta = remaining / speed if speed > 0 else 0
141141+142142+ pct = 100 * self.completed / self.total if self.total > 0 else 0
143143+ spin = spinner[spin_idx % len(spinner)]
144144+145145+ # Calculate conflict rate
146146+ conflict_delta = self.total_conflicts - last_conflicts
147147+ conflict_rate = conflict_rate * 0.7 + conflict_delta * 10 * 0.3
148148+ last_conflicts = self.total_conflicts
149149+150150+ # Fixed parts (calculate their length)
151151+ # Format: " ⠹ [BAR] 2/6 (33%) 0.8/s [ACT] 29K"
152152+ prefix = f" {spin} ["
153153+ count_str = f"] {self.completed}/{self.total} ({pct:.0f}%) {speed:.1f}/s"
154154+155155+ if self.total_conflicts > 0:
156156+ activity_level = min(max(conflict_rate / 50000, 0), 1.0)
157157+ conflict_str = f" {fmt_num(self.total_conflicts)}"
158158+ else:
159159+ activity_level = 0
160160+ conflict_str = ""
161161+162162+ # Calculate available space for bars
163163+ fixed_len = len(prefix) + len(count_str) + len(conflict_str) + 4 # +4 for " []" around activity
164164+ available = term_width - fixed_len - 2 # -2 for safety margin
165165+166166+ if available < 10:
167167+ # Too narrow, minimal display
168168+ content = f" {spin} {self.completed}/{self.total} {fmt_num(self.total_conflicts)}"
169169+ else:
170170+ # Split available space: 70% progress bar, 30% activity bar
171171+ if self.total_conflicts > 0:
172172+ progress_width = int(available * 0.65)
173173+ activity_width = available - progress_width
174174+ else:
175175+ progress_width = available
176176+ activity_width = 0
177177+178178+ # Progress bar
179179+ filled = int(progress_width * self.completed / self.total) if self.total > 0 else 0
180180+ bar = "█" * filled + "░" * (progress_width - filled)
181181+182182+ # Activity bar
183183+ if activity_width > 0:
184184+ activity_filled = int(activity_width * activity_level)
185185+ activity_bar = f" [{CYAN}{'▮' * activity_filled}{'▯' * (activity_width - activity_filled)}{RESET}]{conflict_str}"
186186+ else:
187187+ activity_bar = ""
188188+189189+ content = f"{prefix}{bar}{count_str}{activity_bar}"
190190+191191+ line = f"\r{content}{CLEAR_LINE}"
192192+ sys.stdout.write(line)
193193+ sys.stdout.flush()
194194+195195+ spin_idx += 1
196196+ time.sleep(0.1)
197197+198198+199199+def progress_bar(current, total, width=30, label=""):
200200+ """Generate a progress bar string."""
201201+ filled = int(width * current / total) if total > 0 else 0
202202+ bar = "█" * filled + "░" * (width - filled)
203203+ pct = 100 * current / total if total > 0 else 0
204204+ return f"{label}[{bar}] {current}/{total} ({pct:.0f}%)"
205205+206206+207207+def try_config_with_stats(n2, n3, n4, use_complements, restrict_functions, stats_queue):
208208+ """Try a single (n2, n3, n4) configuration with stats reporting."""
18209 cost = n2 * 2 + n3 * 3 + n4 * 4
19210 n_gates = n2 + n3 + n4
211211+ config_str = f"{n2}x2+{n3}x3+{n4}x4"
2021221213 if n_gates < 7:
2222- return None, cost, f"Skipped (only {n_gates} gates)"
214214+ return None, cost, f"Skipped (only {n_gates} gates)", (n2, n3, n4)
215215+216216+ from pysat.formula import CNF
217217+ from pysat.solvers import Solver
2321824219 solver = BCDTo7SegmentSolver()
25220 solver.generate_prime_implicants()
26221222222+ start = time.time()
27223 try:
2828- result = solver._try_general_synthesis(n2, n3, n4, use_complements, restrict_functions)
2929- if result:
3030- return result, cost, "SUCCESS"
3131- else:
3232- return None, cost, "UNSAT"
224224+ # Build the CNF without solving
225225+ cnf = solver._build_general_cnf(n2, n3, n4, use_complements, restrict_functions)
226226+ if cnf is None:
227227+ return None, cost, f"UNSAT (no valid config)", (n2, n3, n4)
228228+229229+ n_vars = cnf['n_vars']
230230+ n_clauses = len(cnf['clauses'])
231231+232232+ # Report initial stats
233233+ if stats_queue:
234234+ try:
235235+ stats_queue.put_nowait({
236236+ 'config': config_str,
237237+ 'phase': 'solving',
238238+ 'vars': n_vars,
239239+ 'clauses': n_clauses,
240240+ 'conflicts': 0,
241241+ 'decisions': 0,
242242+ })
243243+ except:
244244+ pass
245245+246246+ # Solve with periodic stats updates
247247+ with Solver(name='g3', bootstrap_with=CNF(from_clauses=cnf['clauses'])) as sat_solver:
248248+ # Use solve_limited with conflict budget for progress updates
249249+ conflict_budget = 10000
250250+ total_conflicts = 0
251251+ total_decisions = 0
252252+253253+ while True:
254254+ sat_solver.conf_budget(conflict_budget)
255255+ status = sat_solver.solve_limited()
256256+257257+ stats = sat_solver.accum_stats()
258258+ total_conflicts = stats.get('conflicts', 0)
259259+ total_decisions = stats.get('decisions', 0)
260260+261261+ if stats_queue:
262262+ try:
263263+ stats_queue.put_nowait({
264264+ 'config': config_str,
265265+ 'phase': 'solving',
266266+ 'vars': n_vars,
267267+ 'clauses': n_clauses,
268268+ 'conflicts': total_conflicts,
269269+ 'decisions': total_decisions,
270270+ })
271271+ except:
272272+ pass
273273+274274+ if status is not None:
275275+ # Solved (True = SAT, False = UNSAT)
276276+ break
277277+ # status is None means budget exhausted, continue
278278+279279+ elapsed = time.time() - start
280280+281281+ if status:
282282+ model = set(sat_solver.get_model())
283283+ result = solver._decode_general_solution_from_cnf(model, cnf)
284284+ return result, cost, f"SAT ({elapsed:.1f}s, {total_conflicts} conflicts)", (n2, n3, n4)
285285+ else:
286286+ return None, cost, f"UNSAT ({elapsed:.1f}s, {total_conflicts} conflicts)", (n2, n3, n4)
287287+33288 except Exception as e:
3434- return None, cost, f"Error: {e}"
289289+ import traceback
290290+ elapsed = time.time() - start
291291+ return None, cost, f"Error ({elapsed:.1f}s): {e}", (n2, n3, n4)
292292+293293+294294+def try_config(args):
295295+ """Try a single (n2, n3, n4) configuration. Run in separate process."""
296296+ n2, n3, n4, use_complements, restrict_functions, stats_queue = args
297297+ return try_config_with_stats(n2, n3, n4, use_complements, restrict_functions, stats_queue)
352983629937300def verify_result(result):
···753387633977340def main():
7878- print("=" * 60)
7979- print("BCD to 7-Segment Optimal Circuit Search (with 4-input gates)")
8080- print("=" * 60)
341341+ print(f"{BOLD}{'=' * 60}{RESET}")
342342+ print(f"{BOLD}BCD to 7-Segment Optimal Circuit Search (with 4-input gates){RESET}")
343343+ print(f"{BOLD}{'=' * 60}{RESET}")
81344 print()
8282- print("Gates: AND, OR, XOR, XNOR, NAND, NOR (2, 3, and 4 input variants)")
8383- print("Primary input complements (A', B', C', D') are free")
345345+ print(f"Gates: AND, OR, XOR, XNOR, NAND, NOR (2, 3, and 4 input variants)")
346346+ print(f"Primary input complements (A', B', C', D') are free")
84347 print()
8534886349 # Generate configurations sorted by cost
···91354 cost = n2 * 2 + n3 * 3 + n4 * 4
92355 n_gates = n2 + n3 + n4
93356 # Need at least 7 gates for 7 outputs
9494- # Limit to reasonable ranges
95357 if 7 <= n_gates <= 11 and 14 <= cost <= 22:
96358 configs.append((n2, n3, n4, True, True))
9735998360 # Sort by cost, then by number of gates
99361 configs.sort(key=lambda x: (x[0]*2 + x[1]*3 + x[2]*4, x[0]+x[1]+x[2]))
100362101101- print(f"Searching {len(configs)} configurations from {min(c[0]*2+c[1]*3+c[2]*4 for c in configs)} to {max(c[0]*2+c[1]*3+c[2]*4 for c in configs)} inputs")
363363+ # Group configs by cost
364364+ cost_groups = {}
365365+ for cfg in configs:
366366+ cost = cfg[0] * 2 + cfg[1] * 3 + cfg[2] * 4
367367+ if cost not in cost_groups:
368368+ cost_groups[cost] = []
369369+ cost_groups[cost].append(cfg)
370370+371371+ min_cost = min(cost_groups.keys())
372372+ max_cost = max(cost_groups.keys())
373373+374374+ print(f"Searching {len(configs)} configurations from {min_cost} to {max_cost} inputs")
102375 print(f"Using {mp.cpu_count()} CPU cores")
103376 print()
104377105378 best_result = None
106379 best_cost = float('inf')
107380108108- start_time = time.time()
381381+ total_start = time.time()
382382+ configs_tested = 0
383383+ total_configs = len(configs)
109384110110- # Group configs by cost
111111- cost_groups = {}
112112- for cfg in configs:
113113- cost = cfg[0] * 2 + cfg[1] * 3 + cfg[2] * 4
114114- if cost not in cost_groups:
115115- cost_groups[cost] = []
116116- cost_groups[cost].append(cfg)
385385+ # Create shared queue for stats
386386+ manager = Manager()
387387+ stats_queue = manager.Queue()
388388+389389+ progress = ProgressDisplay(stats_queue)
117390118391 with ProcessPoolExecutor(max_workers=mp.cpu_count()) as executor:
119392 for cost in sorted(cost_groups.keys()):
···121394 continue
122395123396 group = cost_groups[cost]
124124- print(f"Trying {cost} inputs ({len(group)} configurations)...", flush=True)
397397+ group_size = len(group)
398398+ group_start = time.time()
399399+ completed_in_group = 0
125400126126- futures = {executor.submit(try_config, cfg): cfg for cfg in group}
401401+ print(f"\n{CYAN}{BOLD}Testing {cost} inputs{RESET} ({group_size} configurations)")
402402+ print("-" * 50)
403403+404404+ # Start async progress display
405405+ progress.start(group_size, cost)
406406+407407+ # Add stats_queue to each config
408408+ configs_with_queue = [(cfg[0], cfg[1], cfg[2], cfg[3], cfg[4], stats_queue) for cfg in group]
409409+ futures = {executor.submit(try_config, cfg): cfg for cfg in configs_with_queue}
410410+ found_solution = False
127411128412 for future in as_completed(futures):
129413 cfg = futures[future]
130130- n2, n3, n4 = cfg[0], cfg[1], cfg[2]
414414+ n2, n3, n4 = cfg[0], cfg[1], cfg[2] # First 3 elements are gate counts
415415+ completed_in_group += 1
416416+ configs_tested += 1
417417+ config_str = f"{n2}x2+{n3}x3+{n4}x4"
131418132419 try:
133133- result, result_cost, status = future.result(timeout=300)
420420+ result, result_cost, status, _ = future.result(timeout=300)
421421+422422+ # Update progress (always, even for UNSAT)
423423+ progress.update(completed_in_group, config_str)
134424135425 if result is not None:
426426+ # Found a potential solution - stop progress to print
136427 valid, msg = verify_result(result)
428428+ progress.stop()
137429 if valid:
138138- print(f" {n2}x2 + {n3}x3 + {n4}x4 = {result_cost}: {status} (verified)")
430430+ print(f"\n {GREEN}✓ {n2}x2 + {n3}x3 + {n4}x4{RESET}: {status} {GREEN}(VERIFIED){RESET}")
139431 if result_cost < best_cost:
140432 best_result = result
141433 best_cost = result_cost
434434+ found_solution = True
142435 for f in futures:
143436 f.cancel()
144437 break
145438 else:
146146- print(f" {n2}x2 + {n3}x3 + {n4}x4 = {result_cost}: INVALID - {msg}")
147147- else:
148148- print(f" {n2}x2 + {n3}x3 + {n4}x4 = {result_cost}: {status}")
439439+ print(f"\n {YELLOW}✗ {n2}x2 + {n3}x3 + {n4}x4{RESET}: INVALID - {msg}")
440440+ # Restart progress
441441+ progress.start(group_size, cost)
442442+ progress.update(completed_in_group, config_str)
443443+ # For UNSAT: just continue, progress bar updates automatically
149444150445 except Exception as e:
151151- print(f" {n2}x2 + {n3}x3 + {n4}x4: Error - {e}")
446446+ progress.stop()
447447+ print(f"\n {n2}x2 + {n3}x3 + {n4}x4: Error - {e}")
448448+ progress.start(group_size, cost)
449449+ progress.update(completed_in_group)
450450+451451+ # Stop progress and print summary
452452+ progress.stop()
453453+ group_elapsed = time.time() - group_start
454454+455455+ if not found_solution:
456456+ print(f" {YELLOW}✗ All {group_size} configurations UNSAT{RESET} ({group_elapsed:.1f}s, {progress.total_conflicts} conflicts)")
152457153458 if best_result is not None and best_cost <= cost:
154154- print(f"\nFound solution at {best_cost} inputs, stopping search.")
459459+ print(f"\n{GREEN}{BOLD}Found solution at {best_cost} inputs!{RESET}")
155460 break
156461157157- elapsed = time.time() - start_time
462462+ total_elapsed = time.time() - total_start
158463159464 print()
160160- print("=" * 60)
161161- print("RESULTS")
162162- print("=" * 60)
163163- print(f"Search time: {elapsed:.1f} seconds")
465465+ print(f"{BOLD}{'=' * 60}{RESET}")
466466+ print(f"{BOLD}RESULTS{RESET}")
467467+ print(f"{BOLD}{'=' * 60}{RESET}")
468468+ print(f"Search time: {total_elapsed:.1f} seconds ({configs_tested} configurations tested)")
469469+ print(f"Average speed: {configs_tested/total_elapsed:.2f} configurations/second")
164470165471 if best_result:
166166- print(f"Best solution: {best_cost} gate inputs")
472472+ print(f"\n{GREEN}{BOLD}Best solution: {best_cost} gate inputs{RESET}")
167473 print()
168474 print("Gates:")
169475···189495 for seg in SEGMENT_NAMES:
190496 print(f" {seg} = {node_names[best_result.output_map[seg]]}")
191497 else:
192192- print("No solution found in the search range.")
498498+ print(f"\n{YELLOW}No solution found in the search range (14-22 inputs).{RESET}")
499499+ print("The minimum is likely 23 inputs (7x2 + 3x3 configuration).")
193500194501195502if __name__ == "__main__":