Upload solver_assistant_v3.py
Browse files- solver_assistant_v3.py +1788 -0
solver_assistant_v3.py
ADDED
|
@@ -0,0 +1,1788 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
solver_assistant_v3.py
|
| 4 |
+
|
| 5 |
+
Natural language -> structured instance -> exact solver -> verified answer.
|
| 6 |
+
JSON schema interface for LLM front-ends.
|
| 7 |
+
Bench mode: instances with analytically-known optima.
|
| 8 |
+
Cross-verifier: B&B vs independent exhaustive DFS (MAX-CUT).
|
| 9 |
+
Stress test harness.
|
| 10 |
+
|
| 11 |
+
No downloads. No network. Pure standard library.
|
| 12 |
+
|
| 13 |
+
Usage:
|
| 14 |
+
python3 solver_assistant_v3.py # interactive REPL
|
| 15 |
+
python3 solver_assistant_v3.py "your problem" # one-shot
|
| 16 |
+
python3 solver_assistant_v3.py --demo # built-in demos
|
| 17 |
+
python3 solver_assistant_v3.py --stress # stress test
|
| 18 |
+
python3 solver_assistant_v3.py --stress --trials 100
|
| 19 |
+
python3 solver_assistant_v3.py --bench # analytic-optimum bench
|
| 20 |
+
python3 solver_assistant_v3.py --cross # B&B vs DFS (MAX-CUT)
|
| 21 |
+
python3 solver_assistant_v3.py --cross --sizes 14,16,18,20,22
|
| 22 |
+
python3 solver_assistant_v3.py --json '{"type":"maxcut","edges":[["A","B",3]]}'
|
| 23 |
+
python3 solver_assistant_v3.py --help
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
import itertools
|
| 27 |
+
import json
|
| 28 |
+
import math
|
| 29 |
+
import random
|
| 30 |
+
import re
|
| 31 |
+
import sys
|
| 32 |
+
import time
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
# =============================================================================
|
| 36 |
+
# SOLVERS
|
| 37 |
+
# =============================================================================
|
| 38 |
+
|
| 39 |
+
def cut_value(W, side):
|
| 40 |
+
n = len(W)
|
| 41 |
+
return sum(W[i][j]
|
| 42 |
+
for i in range(n) for j in range(i + 1, n)
|
| 43 |
+
if side[i] != side[j])
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def greedy_cut(W, seed=0):
|
| 47 |
+
n = len(W)
|
| 48 |
+
rng = random.Random(seed)
|
| 49 |
+
order = list(range(n))
|
| 50 |
+
rng.shuffle(order)
|
| 51 |
+
side = [0] * n
|
| 52 |
+
assigned = [False] * n
|
| 53 |
+
for v in order:
|
| 54 |
+
to0 = sum(W[v][u] for u in range(n) if assigned[u] and side[u] == 0)
|
| 55 |
+
to1 = sum(W[v][u] for u in range(n) if assigned[u] and side[u] == 1)
|
| 56 |
+
side[v] = 0 if to1 >= to0 else 1
|
| 57 |
+
assigned[v] = True
|
| 58 |
+
return cut_value(W, side), side
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def local_search_maxcut(W, side, passes=10):
|
| 62 |
+
n = len(W)
|
| 63 |
+
side = side[:]
|
| 64 |
+
cur = cut_value(W, side)
|
| 65 |
+
for _ in range(passes):
|
| 66 |
+
improved = False
|
| 67 |
+
for v in range(n):
|
| 68 |
+
Wv = W[v]
|
| 69 |
+
sv = side[v]
|
| 70 |
+
gain = 0
|
| 71 |
+
for u in range(n):
|
| 72 |
+
if u == v:
|
| 73 |
+
continue
|
| 74 |
+
if side[u] == sv:
|
| 75 |
+
gain += Wv[u]
|
| 76 |
+
else:
|
| 77 |
+
gain -= Wv[u]
|
| 78 |
+
if gain > 0:
|
| 79 |
+
side[v] ^= 1
|
| 80 |
+
cur += gain
|
| 81 |
+
improved = True
|
| 82 |
+
if not improved:
|
| 83 |
+
break
|
| 84 |
+
return cur, side
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def warm_start_maxcut(W, restarts=64, seed=0):
|
| 88 |
+
best_val, best_side = -1, None
|
| 89 |
+
for r in range(restarts):
|
| 90 |
+
v, s = greedy_cut(W, seed=seed * 1000 + r)
|
| 91 |
+
v, s = local_search_maxcut(W, s, passes=5)
|
| 92 |
+
if v > best_val:
|
| 93 |
+
best_val, best_side = v, s
|
| 94 |
+
return best_val, best_side
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def brute_force_maxcut(W):
|
| 98 |
+
n = len(W)
|
| 99 |
+
best = -1
|
| 100 |
+
best_side = None
|
| 101 |
+
for mask in range(1 << n):
|
| 102 |
+
side = [(mask >> i) & 1 for i in range(n)]
|
| 103 |
+
v = cut_value(W, side)
|
| 104 |
+
if v > best:
|
| 105 |
+
best = v
|
| 106 |
+
best_side = side
|
| 107 |
+
return best, best_side
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def maxcut_bb(W, time_limit, warm, warm_side):
|
| 111 |
+
n = len(W)
|
| 112 |
+
side = [-1] * n
|
| 113 |
+
order = sorted(range(n), key=lambda v: -sum(W[v]))
|
| 114 |
+
st = {"best": float(warm), "best_side": list(warm_side),
|
| 115 |
+
"nodes": 0, "aborted": False}
|
| 116 |
+
t0 = time.perf_counter()
|
| 117 |
+
|
| 118 |
+
def ub():
|
| 119 |
+
fc = 0.0
|
| 120 |
+
for i in range(n):
|
| 121 |
+
si = side[i]
|
| 122 |
+
if si < 0:
|
| 123 |
+
continue
|
| 124 |
+
Wi = W[i]
|
| 125 |
+
for j in range(i + 1, n):
|
| 126 |
+
sj = side[j]
|
| 127 |
+
if sj >= 0 and si != sj:
|
| 128 |
+
fc += Wi[j]
|
| 129 |
+
free = [v for v in range(n) if side[v] < 0]
|
| 130 |
+
extra = 0.0
|
| 131 |
+
for v in free:
|
| 132 |
+
Wv = W[v]
|
| 133 |
+
wa = wb = 0.0
|
| 134 |
+
for u in range(n):
|
| 135 |
+
w = Wv[u]
|
| 136 |
+
if not w:
|
| 137 |
+
continue
|
| 138 |
+
su = side[u]
|
| 139 |
+
if su == 0:
|
| 140 |
+
wa += w
|
| 141 |
+
elif su == 1:
|
| 142 |
+
wb += w
|
| 143 |
+
extra += wa if wa > wb else wb
|
| 144 |
+
for a in range(len(free)):
|
| 145 |
+
Wv = W[free[a]]
|
| 146 |
+
for b in range(a + 1, len(free)):
|
| 147 |
+
extra += Wv[free[b]]
|
| 148 |
+
return fc + extra
|
| 149 |
+
|
| 150 |
+
def rec():
|
| 151 |
+
if st["aborted"]:
|
| 152 |
+
return
|
| 153 |
+
st["nodes"] += 1
|
| 154 |
+
if (st["nodes"] & 0x3FF) == 0 and (time.perf_counter() - t0) > time_limit:
|
| 155 |
+
st["aborted"] = True
|
| 156 |
+
return
|
| 157 |
+
if ub() <= st["best"] + 1e-9:
|
| 158 |
+
return
|
| 159 |
+
v = -1
|
| 160 |
+
for x in order:
|
| 161 |
+
if side[x] < 0:
|
| 162 |
+
v = x
|
| 163 |
+
break
|
| 164 |
+
if v < 0:
|
| 165 |
+
val = cut_value(W, side)
|
| 166 |
+
if val > st["best"]:
|
| 167 |
+
st["best"] = val
|
| 168 |
+
st["best_side"] = side[:]
|
| 169 |
+
return
|
| 170 |
+
for s in (0, 1):
|
| 171 |
+
side[v] = s
|
| 172 |
+
rec()
|
| 173 |
+
side[v] = -1
|
| 174 |
+
if st["aborted"]:
|
| 175 |
+
return
|
| 176 |
+
|
| 177 |
+
rec()
|
| 178 |
+
st["seconds"] = time.perf_counter() - t0
|
| 179 |
+
return st
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def maxcut_dfs_exact(W, time_limit=60.0):
|
| 183 |
+
"""
|
| 184 |
+
Independent exact MAX-CUT by exhaustive DFS.
|
| 185 |
+
|
| 186 |
+
Fix vertex 0 to side 0 (S/T symmetry halves the search). DFS over
|
| 187 |
+
remaining vertices, maintaining the cut value incrementally.
|
| 188 |
+
|
| 189 |
+
Shares no code with maxcut_bb: different algorithm, no upper bound,
|
| 190 |
+
no warm start, no pruning. Genuinely independent cross-check.
|
| 191 |
+
Returns (best_value, best_mask, aborted_flag).
|
| 192 |
+
"""
|
| 193 |
+
n = len(W)
|
| 194 |
+
if n == 0:
|
| 195 |
+
return 0, 0, False
|
| 196 |
+
|
| 197 |
+
neighbors = []
|
| 198 |
+
total_to_earlier = [0] * n
|
| 199 |
+
for v in range(n):
|
| 200 |
+
ns = [(u, W[v][u]) for u in range(v) if W[v][u]]
|
| 201 |
+
neighbors.append(ns)
|
| 202 |
+
total_to_earlier[v] = sum(w for _, w in ns)
|
| 203 |
+
|
| 204 |
+
best = [-1]
|
| 205 |
+
best_mask = [0]
|
| 206 |
+
aborted = [False]
|
| 207 |
+
t0 = time.perf_counter()
|
| 208 |
+
|
| 209 |
+
def rec(v, mask, cut):
|
| 210 |
+
if aborted[0]:
|
| 211 |
+
return
|
| 212 |
+
if (v & 0xFF) == 0 and (time.perf_counter() - t0) > time_limit:
|
| 213 |
+
aborted[0] = True
|
| 214 |
+
return
|
| 215 |
+
if v == n:
|
| 216 |
+
if cut > best[0]:
|
| 217 |
+
best[0] = cut
|
| 218 |
+
best_mask[0] = mask
|
| 219 |
+
return
|
| 220 |
+
to_1 = 0
|
| 221 |
+
for u, w in neighbors[v]:
|
| 222 |
+
if mask & (1 << u):
|
| 223 |
+
to_1 += w
|
| 224 |
+
to_0 = total_to_earlier[v] - to_1
|
| 225 |
+
rec(v + 1, mask, cut + to_1)
|
| 226 |
+
rec(v + 1, mask | (1 << v), cut + to_0)
|
| 227 |
+
|
| 228 |
+
rec(1, 0, 0)
|
| 229 |
+
return best[0], best_mask[0], aborted[0]
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def solve_maxcut(W, names, time_limit=30.0):
|
| 233 |
+
n = len(W)
|
| 234 |
+
t0 = time.perf_counter()
|
| 235 |
+
if n <= 20:
|
| 236 |
+
val, side = brute_force_maxcut(W)
|
| 237 |
+
method = "brute force"
|
| 238 |
+
status = "OPTIMAL (proven)"
|
| 239 |
+
else:
|
| 240 |
+
warm, warm_side = warm_start_maxcut(W, restarts=64, seed=0)
|
| 241 |
+
res = maxcut_bb(W, time_limit, warm, warm_side)
|
| 242 |
+
val, side = res["best"], res["best_side"]
|
| 243 |
+
method = f"branch and bound ({res['nodes']:,} nodes)"
|
| 244 |
+
status = ("TIMEOUT (best found is lower bound)" if res["aborted"]
|
| 245 |
+
else "OPTIMAL (proven)")
|
| 246 |
+
t = time.perf_counter() - t0
|
| 247 |
+
check = cut_value(W, side)
|
| 248 |
+
partition = {
|
| 249 |
+
"S": [names[i] for i in range(n) if side[i] == 0],
|
| 250 |
+
"T": [names[i] for i in range(n) if side[i] == 1],
|
| 251 |
+
}
|
| 252 |
+
return {
|
| 253 |
+
"value": val,
|
| 254 |
+
"partition": partition,
|
| 255 |
+
"verify": check == val,
|
| 256 |
+
"method": method,
|
| 257 |
+
"status": status,
|
| 258 |
+
"seconds": t,
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
# --- SAT ---------------------------------------------------------------
|
| 263 |
+
|
| 264 |
+
def verify_sat(clauses, assignment):
|
| 265 |
+
for c in clauses:
|
| 266 |
+
satisfied = False
|
| 267 |
+
for lit in c:
|
| 268 |
+
v = abs(lit)
|
| 269 |
+
if v in assignment and assignment[v] == (lit > 0):
|
| 270 |
+
satisfied = True
|
| 271 |
+
break
|
| 272 |
+
if not satisfied:
|
| 273 |
+
return False
|
| 274 |
+
return True
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
def dpll(clauses, n, assignment, deadline, nodes):
|
| 278 |
+
nodes[0] += 1
|
| 279 |
+
if (nodes[0] & 0xFF) == 0 and time.perf_counter() > deadline:
|
| 280 |
+
raise TimeoutError
|
| 281 |
+
|
| 282 |
+
while True:
|
| 283 |
+
unit = None
|
| 284 |
+
for c in clauses:
|
| 285 |
+
satisfied = False
|
| 286 |
+
unassigned = []
|
| 287 |
+
for lit in c:
|
| 288 |
+
v = abs(lit)
|
| 289 |
+
if v in assignment:
|
| 290 |
+
if assignment[v] == (lit > 0):
|
| 291 |
+
satisfied = True
|
| 292 |
+
break
|
| 293 |
+
else:
|
| 294 |
+
unassigned.append(lit)
|
| 295 |
+
if satisfied:
|
| 296 |
+
continue
|
| 297 |
+
if not unassigned:
|
| 298 |
+
return None
|
| 299 |
+
if len(unassigned) == 1:
|
| 300 |
+
unit = unassigned[0]
|
| 301 |
+
break
|
| 302 |
+
if unit is None:
|
| 303 |
+
break
|
| 304 |
+
assignment[abs(unit)] = (unit > 0)
|
| 305 |
+
|
| 306 |
+
pos, neg = set(), set()
|
| 307 |
+
for c in clauses:
|
| 308 |
+
for lit in c:
|
| 309 |
+
v = abs(lit)
|
| 310 |
+
if v in assignment:
|
| 311 |
+
continue
|
| 312 |
+
(pos if lit > 0 else neg).add(v)
|
| 313 |
+
pure_pos = pos - neg
|
| 314 |
+
pure_neg = neg - pos
|
| 315 |
+
if pure_pos or pure_neg:
|
| 316 |
+
new_assign = dict(assignment)
|
| 317 |
+
for v in pure_pos:
|
| 318 |
+
new_assign[v] = True
|
| 319 |
+
for v in pure_neg:
|
| 320 |
+
new_assign[v] = False
|
| 321 |
+
return dpll(clauses, n, new_assign, deadline, nodes)
|
| 322 |
+
|
| 323 |
+
all_sat = True
|
| 324 |
+
for c in clauses:
|
| 325 |
+
satisfied = False
|
| 326 |
+
for lit in c:
|
| 327 |
+
v = abs(lit)
|
| 328 |
+
if v in assignment and assignment[v] == (lit > 0):
|
| 329 |
+
satisfied = True
|
| 330 |
+
break
|
| 331 |
+
if not satisfied:
|
| 332 |
+
all_sat = False
|
| 333 |
+
break
|
| 334 |
+
if all_sat:
|
| 335 |
+
for v in range(1, n + 1):
|
| 336 |
+
if v not in assignment:
|
| 337 |
+
assignment[v] = True
|
| 338 |
+
return assignment
|
| 339 |
+
|
| 340 |
+
counts = {}
|
| 341 |
+
for c in clauses:
|
| 342 |
+
for lit in c:
|
| 343 |
+
v = abs(lit)
|
| 344 |
+
if v not in assignment:
|
| 345 |
+
counts[v] = counts.get(v, 0) + 1
|
| 346 |
+
if not counts:
|
| 347 |
+
return None
|
| 348 |
+
var = max(counts, key=counts.get)
|
| 349 |
+
|
| 350 |
+
for val in (True, False):
|
| 351 |
+
new_assign = dict(assignment)
|
| 352 |
+
new_assign[var] = val
|
| 353 |
+
result = dpll(clauses, n, new_assign, deadline, nodes)
|
| 354 |
+
if result is not None:
|
| 355 |
+
return result
|
| 356 |
+
return None
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
def solve_sat(clauses, n_vars, time_limit=30.0):
|
| 360 |
+
deadline = time.perf_counter() + time_limit
|
| 361 |
+
nodes = [0]
|
| 362 |
+
t0 = time.perf_counter()
|
| 363 |
+
try:
|
| 364 |
+
result = dpll(clauses, n_vars, {}, deadline, nodes)
|
| 365 |
+
t = time.perf_counter() - t0
|
| 366 |
+
if result is None:
|
| 367 |
+
return {
|
| 368 |
+
"satisfiable": False,
|
| 369 |
+
"model": None,
|
| 370 |
+
"verify": True,
|
| 371 |
+
"method": f"DPLL ({nodes[0]:,} nodes)",
|
| 372 |
+
"status": "UNSAT (proven)",
|
| 373 |
+
"seconds": t,
|
| 374 |
+
}
|
| 375 |
+
ok = verify_sat(clauses, result)
|
| 376 |
+
return {
|
| 377 |
+
"satisfiable": True,
|
| 378 |
+
"model": {v: result[v] for v in range(1, n_vars + 1)},
|
| 379 |
+
"verify": ok,
|
| 380 |
+
"method": f"DPLL ({nodes[0]:,} nodes)",
|
| 381 |
+
"status": "SAT (model verified)",
|
| 382 |
+
"seconds": t,
|
| 383 |
+
}
|
| 384 |
+
except TimeoutError:
|
| 385 |
+
t = time.perf_counter() - t0
|
| 386 |
+
return {
|
| 387 |
+
"satisfiable": None,
|
| 388 |
+
"model": None,
|
| 389 |
+
"verify": False,
|
| 390 |
+
"method": f"DPLL ({nodes[0]:,} nodes)",
|
| 391 |
+
"status": "TIMEOUT",
|
| 392 |
+
"seconds": t,
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
|
| 396 |
+
# --- SUBSET-SUM --------------------------------------------------------
|
| 397 |
+
|
| 398 |
+
def subset_sum_mim(arr, target):
|
| 399 |
+
n = len(arr)
|
| 400 |
+
half = n // 2
|
| 401 |
+
left, right = arr[:half], arr[half:]
|
| 402 |
+
|
| 403 |
+
def all_sums(a):
|
| 404 |
+
out = []
|
| 405 |
+
for mask in range(1 << len(a)):
|
| 406 |
+
s = 0
|
| 407 |
+
for i in range(len(a)):
|
| 408 |
+
if mask & (1 << i):
|
| 409 |
+
s += a[i]
|
| 410 |
+
out.append((s, mask))
|
| 411 |
+
return out
|
| 412 |
+
|
| 413 |
+
L = all_sums(left)
|
| 414 |
+
R = all_sums(right)
|
| 415 |
+
R.sort()
|
| 416 |
+
|
| 417 |
+
for s, m in L:
|
| 418 |
+
need = target - s
|
| 419 |
+
lo, hi = 0, len(R)
|
| 420 |
+
while lo < hi:
|
| 421 |
+
mid = (lo + hi) // 2
|
| 422 |
+
if R[mid][0] < need:
|
| 423 |
+
lo = mid + 1
|
| 424 |
+
else:
|
| 425 |
+
hi = mid
|
| 426 |
+
if lo < len(R) and R[lo][0] == need:
|
| 427 |
+
return m | (R[lo][1] << half)
|
| 428 |
+
return None
|
| 429 |
+
|
| 430 |
+
|
| 431 |
+
def brute_force_subset_sum(arr, target):
|
| 432 |
+
n = len(arr)
|
| 433 |
+
for mask in range(1 << n):
|
| 434 |
+
s = sum(arr[i] for i in range(n) if mask & (1 << i))
|
| 435 |
+
if s == target:
|
| 436 |
+
return mask
|
| 437 |
+
return None
|
| 438 |
+
|
| 439 |
+
|
| 440 |
+
def solve_subset_sum(arr, target):
|
| 441 |
+
t0 = time.perf_counter()
|
| 442 |
+
mask = subset_sum_mim(arr, target)
|
| 443 |
+
t = time.perf_counter() - t0
|
| 444 |
+
if mask is None:
|
| 445 |
+
return {
|
| 446 |
+
"found": False,
|
| 447 |
+
"subset": None,
|
| 448 |
+
"verify": True,
|
| 449 |
+
"method": "meet-in-the-middle",
|
| 450 |
+
"status": "NO EXACT SUBSET",
|
| 451 |
+
"seconds": t,
|
| 452 |
+
}
|
| 453 |
+
chosen = [arr[i] for i in range(len(arr)) if mask & (1 << i)]
|
| 454 |
+
return {
|
| 455 |
+
"found": True,
|
| 456 |
+
"subset": chosen,
|
| 457 |
+
"verify": sum(chosen) == target,
|
| 458 |
+
"method": "meet-in-the-middle",
|
| 459 |
+
"status": "FOUND (verified)",
|
| 460 |
+
"seconds": t,
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
|
| 464 |
+
# --- TSP ---------------------------------------------------------------
|
| 465 |
+
|
| 466 |
+
def held_karp(D):
|
| 467 |
+
n = len(D)
|
| 468 |
+
INF = float("inf")
|
| 469 |
+
size = 1 << n
|
| 470 |
+
dp = [[INF] * n for _ in range(size)]
|
| 471 |
+
parent = [[-1] * n for _ in range(size)]
|
| 472 |
+
dp[1][0] = 0.0
|
| 473 |
+
|
| 474 |
+
for mask in range(size):
|
| 475 |
+
row = dp[mask]
|
| 476 |
+
for i in range(n):
|
| 477 |
+
if not (mask & (1 << i)):
|
| 478 |
+
continue
|
| 479 |
+
ci = row[i]
|
| 480 |
+
if ci == INF:
|
| 481 |
+
continue
|
| 482 |
+
Di = D[i]
|
| 483 |
+
for j in range(n):
|
| 484 |
+
if mask & (1 << j):
|
| 485 |
+
continue
|
| 486 |
+
nm = mask | (1 << j)
|
| 487 |
+
nc = ci + Di[j]
|
| 488 |
+
if nc < dp[nm][j]:
|
| 489 |
+
dp[nm][j] = nc
|
| 490 |
+
parent[nm][j] = i
|
| 491 |
+
|
| 492 |
+
full = size - 1
|
| 493 |
+
best = INF
|
| 494 |
+
end = -1
|
| 495 |
+
for i in range(n):
|
| 496 |
+
c = dp[full][i] + D[i][0]
|
| 497 |
+
if c < best:
|
| 498 |
+
best = c
|
| 499 |
+
end = i
|
| 500 |
+
|
| 501 |
+
path = []
|
| 502 |
+
mask = full
|
| 503 |
+
i = end
|
| 504 |
+
while i != -1:
|
| 505 |
+
path.append(i)
|
| 506 |
+
p = parent[mask][i]
|
| 507 |
+
mask ^= (1 << i)
|
| 508 |
+
i = p
|
| 509 |
+
path.reverse()
|
| 510 |
+
return best, path
|
| 511 |
+
|
| 512 |
+
|
| 513 |
+
def brute_force_tsp(points):
|
| 514 |
+
n = len(points)
|
| 515 |
+
best_cost = float("inf")
|
| 516 |
+
best_tour = None
|
| 517 |
+
for perm in itertools.permutations(range(1, n)):
|
| 518 |
+
tour = (0,) + perm
|
| 519 |
+
cost = sum(math.dist(points[tour[i]], points[tour[(i + 1) % n]])
|
| 520 |
+
for i in range(n))
|
| 521 |
+
if cost < best_cost:
|
| 522 |
+
best_cost = cost
|
| 523 |
+
best_tour = list(tour)
|
| 524 |
+
return best_cost, best_tour
|
| 525 |
+
|
| 526 |
+
|
| 527 |
+
def solve_tsp(points, time_limit=60.0):
|
| 528 |
+
n = len(points)
|
| 529 |
+
if n > 18:
|
| 530 |
+
return {
|
| 531 |
+
"tour": None, "length": None, "verify": False,
|
| 532 |
+
"method": "Held-Karp refused",
|
| 533 |
+
"status": f"TOO LARGE (n={n} > 18)", "seconds": 0.0,
|
| 534 |
+
}
|
| 535 |
+
D = [[math.dist(points[i], points[j]) for j in range(n)] for i in range(n)]
|
| 536 |
+
t0 = time.perf_counter()
|
| 537 |
+
cost, tour = held_karp(D)
|
| 538 |
+
t = time.perf_counter() - t0
|
| 539 |
+
s = sum(D[tour[k]][tour[(k + 1) % n]] for k in range(n))
|
| 540 |
+
return {
|
| 541 |
+
"tour": tour, "length": cost,
|
| 542 |
+
"verify": abs(s - cost) < 1e-6,
|
| 543 |
+
"method": "Held-Karp DP",
|
| 544 |
+
"status": "OPTIMAL (proven)",
|
| 545 |
+
"seconds": t,
|
| 546 |
+
}
|
| 547 |
+
|
| 548 |
+
|
| 549 |
+
# =============================================================================
|
| 550 |
+
# TEXT PARSERS
|
| 551 |
+
# =============================================================================
|
| 552 |
+
|
| 553 |
+
class ParseError(Exception):
|
| 554 |
+
pass
|
| 555 |
+
|
| 556 |
+
|
| 557 |
+
EDGE_RE = re.compile(
|
| 558 |
+
r"([A-Za-z][A-Za-z0-9_]*|\d+)\s*[-=:]\s*([A-Za-z][A-Za-z0-9_]*|\d+)"
|
| 559 |
+
r"\s*(?:[:=]\s*(-?\d+))?"
|
| 560 |
+
)
|
| 561 |
+
|
| 562 |
+
|
| 563 |
+
def parse_maxcut(text):
|
| 564 |
+
edges = []
|
| 565 |
+
for m in EDGE_RE.finditer(text):
|
| 566 |
+
u, v, w = m.group(1), m.group(2), m.group(3)
|
| 567 |
+
edges.append((u, v, int(w) if w is not None else 1))
|
| 568 |
+
if not edges:
|
| 569 |
+
raise ParseError("No edges found. Try: max cut on A-B:3, B-C:5")
|
| 570 |
+
names = []
|
| 571 |
+
for u, v, _ in edges:
|
| 572 |
+
if u not in names:
|
| 573 |
+
names.append(u)
|
| 574 |
+
if v not in names:
|
| 575 |
+
names.append(v)
|
| 576 |
+
index = {n: i for i, n in enumerate(names)}
|
| 577 |
+
n = len(names)
|
| 578 |
+
W = [[0] * n for _ in range(n)]
|
| 579 |
+
for u, v, w in edges:
|
| 580 |
+
i, j = index[u], index[v]
|
| 581 |
+
W[i][j] = w
|
| 582 |
+
W[j][i] = w
|
| 583 |
+
return W, names
|
| 584 |
+
|
| 585 |
+
|
| 586 |
+
CLAUSE_RE = re.compile(r"\(([^()]+)\)")
|
| 587 |
+
|
| 588 |
+
|
| 589 |
+
def parse_sat(text):
|
| 590 |
+
raw_clauses = CLAUSE_RE.findall(text)
|
| 591 |
+
if not raw_clauses:
|
| 592 |
+
m = re.search(
|
| 593 |
+
r"(?:sat|3[\s-]?sat|formula|clauses?)\s*[:=]?\s*(.+)$",
|
| 594 |
+
text, re.I | re.S,
|
| 595 |
+
)
|
| 596 |
+
rest = m.group(1).strip() if m else text
|
| 597 |
+
raw_clauses = re.split(r"\s+and\s+", rest, flags=re.I)
|
| 598 |
+
|
| 599 |
+
raw_clauses = [c for c in (r.strip() for r in raw_clauses) if c]
|
| 600 |
+
if not raw_clauses:
|
| 601 |
+
raise ParseError(
|
| 602 |
+
"No clauses found. Try: sat (x1 or -x2 or x3) and (x2 or x3)"
|
| 603 |
+
)
|
| 604 |
+
|
| 605 |
+
parsed = []
|
| 606 |
+
var_names = set()
|
| 607 |
+
for raw in raw_clauses:
|
| 608 |
+
raw = raw.strip().strip("()").strip()
|
| 609 |
+
if not raw:
|
| 610 |
+
continue
|
| 611 |
+
parts = re.split(r"\s+(?:or|\|)\s+|\s*,\s*", raw, flags=re.I)
|
| 612 |
+
clause = []
|
| 613 |
+
for p in parts:
|
| 614 |
+
p = p.strip()
|
| 615 |
+
if not p:
|
| 616 |
+
continue
|
| 617 |
+
neg = False
|
| 618 |
+
while p and p[0] in "-~!":
|
| 619 |
+
neg = not neg
|
| 620 |
+
p = p[1:].strip()
|
| 621 |
+
if not p:
|
| 622 |
+
raise ParseError("Empty literal in clause")
|
| 623 |
+
if re.match(r"^([A-Za-z_]*)(\d+)$", p) or \
|
| 624 |
+
re.match(r"^[A-Za-z_]\w*$", p):
|
| 625 |
+
var_names.add(p)
|
| 626 |
+
clause.append((p, neg))
|
| 627 |
+
else:
|
| 628 |
+
raise ParseError(f"Cannot parse literal: {p!r}")
|
| 629 |
+
if clause:
|
| 630 |
+
parsed.append(clause)
|
| 631 |
+
|
| 632 |
+
if not parsed:
|
| 633 |
+
raise ParseError("Could not parse any clauses.")
|
| 634 |
+
|
| 635 |
+
name_to_int = {name: i + 1 for i, name in enumerate(sorted(var_names))}
|
| 636 |
+
int_clauses = [
|
| 637 |
+
tuple((name_to_int[n] if not neg else -name_to_int[n])
|
| 638 |
+
for n, neg in c)
|
| 639 |
+
for c in parsed
|
| 640 |
+
]
|
| 641 |
+
return int_clauses, len(name_to_int), name_to_int
|
| 642 |
+
|
| 643 |
+
|
| 644 |
+
LIST_RE = re.compile(r"\[([^\]]+)\]")
|
| 645 |
+
|
| 646 |
+
|
| 647 |
+
def parse_subset_sum(text):
|
| 648 |
+
m = LIST_RE.search(text)
|
| 649 |
+
if m:
|
| 650 |
+
nums = [int(x) for x in re.split(r"[,\s]+", m.group(1)) if x.strip()]
|
| 651 |
+
else:
|
| 652 |
+
nums = [int(x) for x in re.findall(r"-?\d+", text)]
|
| 653 |
+
if not nums:
|
| 654 |
+
raise ParseError("No numbers found. Try: subset sum [3, 7, 1] target 4")
|
| 655 |
+
target = None
|
| 656 |
+
for pat in [r"(?:target|sum to|summing to|equal(?:s| to)?|=)\s*(-?\d+)",
|
| 657 |
+
r"(-?\d+)\s*$"]:
|
| 658 |
+
tm = re.search(pat, text, re.I)
|
| 659 |
+
if tm:
|
| 660 |
+
target = int(tm.group(1))
|
| 661 |
+
break
|
| 662 |
+
defaulted = False
|
| 663 |
+
if target is None:
|
| 664 |
+
target = sum(nums) // 2
|
| 665 |
+
defaulted = True
|
| 666 |
+
return nums, target, defaulted
|
| 667 |
+
|
| 668 |
+
|
| 669 |
+
PAIR_RE = re.compile(
|
| 670 |
+
r"\(\s*(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\s*,\s*"
|
| 671 |
+
r"(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\s*\)"
|
| 672 |
+
)
|
| 673 |
+
|
| 674 |
+
|
| 675 |
+
def parse_tsp(text):
|
| 676 |
+
pairs = PAIR_RE.findall(text)
|
| 677 |
+
if not pairs:
|
| 678 |
+
raise ParseError(
|
| 679 |
+
"No coordinates found. Try: tsp (0,0) (1,2) (3,1) (2,4)"
|
| 680 |
+
)
|
| 681 |
+
points = [(float(a), float(b)) for a, b in pairs]
|
| 682 |
+
if len(points) < 3:
|
| 683 |
+
raise ParseError("TSP needs at least 3 points.")
|
| 684 |
+
return points
|
| 685 |
+
|
| 686 |
+
|
| 687 |
+
# =============================================================================
|
| 688 |
+
# JSON SCHEMA INTERFACE (LLM adapter point)
|
| 689 |
+
# =============================================================================
|
| 690 |
+
|
| 691 |
+
def _require(d, key, typ, ctx):
|
| 692 |
+
if key not in d:
|
| 693 |
+
raise ValueError(f"{ctx}: missing field {key!r}")
|
| 694 |
+
v = d[key]
|
| 695 |
+
if not isinstance(v, typ):
|
| 696 |
+
raise ValueError(
|
| 697 |
+
f"{ctx}: field {key!r} must be {typ.__name__}, "
|
| 698 |
+
f"got {type(v).__name__}"
|
| 699 |
+
)
|
| 700 |
+
return v
|
| 701 |
+
|
| 702 |
+
|
| 703 |
+
def _validate_maxcut_schema(d):
|
| 704 |
+
edges = _require(d, "edges", list, "maxcut")
|
| 705 |
+
if not edges:
|
| 706 |
+
raise ValueError("maxcut: 'edges' list is empty")
|
| 707 |
+
names = []
|
| 708 |
+
parsed = []
|
| 709 |
+
for i, e in enumerate(edges):
|
| 710 |
+
if not isinstance(e, (list, tuple)) or len(e) not in (2, 3):
|
| 711 |
+
raise ValueError(
|
| 712 |
+
f"maxcut: edge {i} must be [u, v] or [u, v, w], got {e!r}"
|
| 713 |
+
)
|
| 714 |
+
u, v = e[0], e[1]
|
| 715 |
+
w = e[2] if len(e) == 3 else 1
|
| 716 |
+
if not isinstance(w, int) or w < 0:
|
| 717 |
+
raise ValueError(
|
| 718 |
+
f"maxcut: edge {i} weight must be non-negative int, got {w!r}"
|
| 719 |
+
)
|
| 720 |
+
for x in (u, v):
|
| 721 |
+
if not isinstance(x, (str, int)):
|
| 722 |
+
raise ValueError(f"maxcut: vertex {x!r} must be str or int")
|
| 723 |
+
if x not in names:
|
| 724 |
+
names.append(x)
|
| 725 |
+
parsed.append((u, v, w))
|
| 726 |
+
n = len(names)
|
| 727 |
+
index = {nm: i for i, nm in enumerate(names)}
|
| 728 |
+
W = [[0] * n for _ in range(n)]
|
| 729 |
+
for u, v, w in parsed:
|
| 730 |
+
i, j = index[u], index[v]
|
| 731 |
+
W[i][j] = w
|
| 732 |
+
W[j][i] = w
|
| 733 |
+
return W, names
|
| 734 |
+
|
| 735 |
+
|
| 736 |
+
def _validate_sat_schema(d):
|
| 737 |
+
clauses = _require(d, "clauses", list, "sat")
|
| 738 |
+
if not clauses:
|
| 739 |
+
raise ValueError("sat: 'clauses' list is empty")
|
| 740 |
+
n_vars = d.get("n_vars")
|
| 741 |
+
if n_vars is not None and not isinstance(n_vars, int):
|
| 742 |
+
raise ValueError("sat: 'n_vars' must be int if present")
|
| 743 |
+
max_var = 0
|
| 744 |
+
int_clauses = []
|
| 745 |
+
for i, c in enumerate(clauses):
|
| 746 |
+
if not isinstance(c, (list, tuple)) or len(c) < 1:
|
| 747 |
+
raise ValueError(f"sat: clause {i} must be a non-empty list")
|
| 748 |
+
clause = []
|
| 749 |
+
for lit in c:
|
| 750 |
+
if not isinstance(lit, int) or lit == 0:
|
| 751 |
+
raise ValueError(
|
| 752 |
+
f"sat: literal {lit!r} in clause {i} must be nonzero int"
|
| 753 |
+
)
|
| 754 |
+
max_var = max(max_var, abs(lit))
|
| 755 |
+
clause.append(lit)
|
| 756 |
+
int_clauses.append(tuple(clause))
|
| 757 |
+
if n_vars is None:
|
| 758 |
+
n_vars = max_var
|
| 759 |
+
elif n_vars < max_var:
|
| 760 |
+
raise ValueError(
|
| 761 |
+
f"sat: 'n_vars'={n_vars} but literal refers to var {max_var}"
|
| 762 |
+
)
|
| 763 |
+
return int_clauses, n_vars
|
| 764 |
+
|
| 765 |
+
|
| 766 |
+
def _validate_subset_sum_schema(d):
|
| 767 |
+
nums = _require(d, "numbers", list, "subset_sum")
|
| 768 |
+
if not nums:
|
| 769 |
+
raise ValueError("subset_sum: 'numbers' list is empty")
|
| 770 |
+
for i, x in enumerate(nums):
|
| 771 |
+
if not isinstance(x, int):
|
| 772 |
+
raise ValueError(f"subset_sum: number {i} must be int, got {x!r}")
|
| 773 |
+
target = d.get("target")
|
| 774 |
+
if target is None:
|
| 775 |
+
target = sum(nums) // 2
|
| 776 |
+
defaulted = True
|
| 777 |
+
else:
|
| 778 |
+
if not isinstance(target, int):
|
| 779 |
+
raise ValueError("subset_sum: 'target' must be int")
|
| 780 |
+
defaulted = False
|
| 781 |
+
return nums, target, defaulted
|
| 782 |
+
|
| 783 |
+
|
| 784 |
+
def _validate_tsp_schema(d):
|
| 785 |
+
pts = _require(d, "points", list, "tsp")
|
| 786 |
+
if len(pts) < 3:
|
| 787 |
+
raise ValueError("tsp: need at least 3 points")
|
| 788 |
+
out = []
|
| 789 |
+
for i, p in enumerate(pts):
|
| 790 |
+
if not isinstance(p, (list, tuple)) or len(p) != 2:
|
| 791 |
+
raise ValueError(f"tsp: point {i} must be [x, y], got {p!r}")
|
| 792 |
+
x, y = p
|
| 793 |
+
if not isinstance(x, (int, float)) or not isinstance(y, (int, float)):
|
| 794 |
+
raise ValueError(f"tsp: point {i} coords must be numeric")
|
| 795 |
+
out.append((float(x), float(y)))
|
| 796 |
+
return out
|
| 797 |
+
|
| 798 |
+
|
| 799 |
+
def dispatch_from_json(schema, verbose=True):
|
| 800 |
+
if not isinstance(schema, dict):
|
| 801 |
+
raise ValueError("schema must be a dict")
|
| 802 |
+
kind = schema.get("type")
|
| 803 |
+
if kind is None:
|
| 804 |
+
raise ValueError("schema missing 'type' field")
|
| 805 |
+
kind = str(kind).lower()
|
| 806 |
+
|
| 807 |
+
if kind == "maxcut":
|
| 808 |
+
W, names = _validate_maxcut_schema(schema)
|
| 809 |
+
if verbose:
|
| 810 |
+
n_edges = sum(1 for i in range(len(W))
|
| 811 |
+
for j in range(i + 1, len(W)) if W[i][j])
|
| 812 |
+
print(f" [json] maxcut: {len(names)} vertices, {n_edges} edges")
|
| 813 |
+
r = solve_maxcut(W, names, time_limit=30.0)
|
| 814 |
+
if verbose:
|
| 815 |
+
_report_maxcut(r)
|
| 816 |
+
return r
|
| 817 |
+
|
| 818 |
+
if kind == "sat":
|
| 819 |
+
clauses, n_vars = _validate_sat_schema(schema)
|
| 820 |
+
if verbose:
|
| 821 |
+
print(f" [json] sat: {n_vars} variables, {len(clauses)} clauses")
|
| 822 |
+
r = solve_sat(clauses, n_vars, time_limit=30.0)
|
| 823 |
+
if verbose:
|
| 824 |
+
_report_sat(r)
|
| 825 |
+
return r
|
| 826 |
+
|
| 827 |
+
if kind == "subset_sum":
|
| 828 |
+
nums, target, defaulted = _validate_subset_sum_schema(schema)
|
| 829 |
+
if verbose:
|
| 830 |
+
note = " (target defaulted to half-total)" if defaulted else ""
|
| 831 |
+
print(f" [json] subset_sum: {len(nums)} numbers, "
|
| 832 |
+
f"target={target}{note}")
|
| 833 |
+
r = solve_subset_sum(nums, target)
|
| 834 |
+
if verbose:
|
| 835 |
+
_report_subset_sum(r)
|
| 836 |
+
return r
|
| 837 |
+
|
| 838 |
+
if kind == "tsp":
|
| 839 |
+
pts = _validate_tsp_schema(schema)
|
| 840 |
+
if verbose:
|
| 841 |
+
print(f" [json] tsp: {len(pts)} points")
|
| 842 |
+
r = solve_tsp(pts, time_limit=60.0)
|
| 843 |
+
if verbose:
|
| 844 |
+
_report_tsp(r)
|
| 845 |
+
return r
|
| 846 |
+
|
| 847 |
+
raise ValueError(f"unknown schema type: {kind!r}")
|
| 848 |
+
|
| 849 |
+
|
| 850 |
+
# =============================================================================
|
| 851 |
+
# TEXT FRONT-END
|
| 852 |
+
# =============================================================================
|
| 853 |
+
|
| 854 |
+
def detect_kind(text):
|
| 855 |
+
t = text.lower()
|
| 856 |
+
if re.search(r"\b(max[\s-]?cut|maximum cut|maxcut)\b", t):
|
| 857 |
+
return "maxcut"
|
| 858 |
+
if re.search(r"\b(3[\s-]?sat|sat\b|satisfiab)", t):
|
| 859 |
+
return "sat"
|
| 860 |
+
if re.search(r"\b(subset[\s-]?sum|partition|sum to|target sum)\b", t):
|
| 861 |
+
return "subset_sum"
|
| 862 |
+
if re.search(r"\b(tsp|travell?ing salesman|shortest tour|best tour)\b", t):
|
| 863 |
+
return "tsp"
|
| 864 |
+
return None
|
| 865 |
+
|
| 866 |
+
|
| 867 |
+
def _report_maxcut(r):
|
| 868 |
+
print(f" method: {r['method']} ({r['seconds']:.2f}s)")
|
| 869 |
+
print(f" status: {r['status']}")
|
| 870 |
+
print(f" best cut value: {r['value']}")
|
| 871 |
+
print(f" partition S: {r['partition']['S']}")
|
| 872 |
+
print(f" partition T: {r['partition']['T']}")
|
| 873 |
+
print(f" verify: {'OK' if r['verify'] else 'FAIL'}")
|
| 874 |
+
|
| 875 |
+
|
| 876 |
+
def _report_sat(r):
|
| 877 |
+
print(f" method: {r['method']} ({r['seconds']:.2f}s)")
|
| 878 |
+
print(f" status: {r['status']}")
|
| 879 |
+
if r["satisfiable"]:
|
| 880 |
+
true_vars = [v for v in r["model"] if r["model"][v]]
|
| 881 |
+
print(f" model true vars: {true_vars}")
|
| 882 |
+
print(f" verify: {'OK' if r['verify'] else 'FAIL'}")
|
| 883 |
+
|
| 884 |
+
|
| 885 |
+
def _report_subset_sum(r):
|
| 886 |
+
print(f" method: {r['method']} ({r['seconds']:.2f}s)")
|
| 887 |
+
print(f" status: {r['status']}")
|
| 888 |
+
if r["found"]:
|
| 889 |
+
print(f" subset: {r['subset']}")
|
| 890 |
+
print(f" sum: {sum(r['subset'])}")
|
| 891 |
+
print(f" verify: {'OK' if r['verify'] else 'FAIL'}")
|
| 892 |
+
|
| 893 |
+
|
| 894 |
+
def _report_tsp(r):
|
| 895 |
+
print(f" method: {r['method']} ({r['seconds']:.2f}s)")
|
| 896 |
+
print(f" status: {r['status']}")
|
| 897 |
+
if r["tour"] is not None:
|
| 898 |
+
print(f" tour: {r['tour']}")
|
| 899 |
+
print(f" length: {r['length']:.4f}")
|
| 900 |
+
print(f" verify: {'OK' if r['verify'] else 'FAIL'}")
|
| 901 |
+
|
| 902 |
+
|
| 903 |
+
def dispatch(text, verbose=True):
|
| 904 |
+
kind = detect_kind(text)
|
| 905 |
+
if kind is None:
|
| 906 |
+
if verbose:
|
| 907 |
+
print(" Could not identify problem type.")
|
| 908 |
+
print(" Supported keywords:")
|
| 909 |
+
print(" 'max cut' / 'maxcut' -> MAX-CUT")
|
| 910 |
+
print(" 'sat' / '3-sat' -> Boolean satisfiability")
|
| 911 |
+
print(" 'subset sum' / 'target' -> SUBSET-SUM")
|
| 912 |
+
print(" 'tsp' / 'tour' -> Traveling salesman")
|
| 913 |
+
return None
|
| 914 |
+
|
| 915 |
+
if verbose:
|
| 916 |
+
print(f" detected: {kind.upper()}")
|
| 917 |
+
|
| 918 |
+
if kind == "maxcut":
|
| 919 |
+
try:
|
| 920 |
+
W, names = parse_maxcut(text)
|
| 921 |
+
except ParseError as e:
|
| 922 |
+
if verbose:
|
| 923 |
+
print(f" parse error: {e}")
|
| 924 |
+
return None
|
| 925 |
+
if verbose:
|
| 926 |
+
n_edges = sum(1 for i in range(len(W))
|
| 927 |
+
for j in range(i + 1, len(W)) if W[i][j])
|
| 928 |
+
print(f" parsed: {len(names)} vertices, {n_edges} edges")
|
| 929 |
+
r = solve_maxcut(W, names, time_limit=30.0)
|
| 930 |
+
if verbose:
|
| 931 |
+
_report_maxcut(r)
|
| 932 |
+
return r
|
| 933 |
+
|
| 934 |
+
if kind == "sat":
|
| 935 |
+
try:
|
| 936 |
+
clauses, n_vars, name_to_int = parse_sat(text)
|
| 937 |
+
except ParseError as e:
|
| 938 |
+
if verbose:
|
| 939 |
+
print(f" parse error: {e}")
|
| 940 |
+
return None
|
| 941 |
+
if verbose:
|
| 942 |
+
print(f" parsed: {n_vars} variables, {len(clauses)} clauses")
|
| 943 |
+
r = solve_sat(clauses, n_vars, time_limit=30.0)
|
| 944 |
+
if r["satisfiable"] and r["model"] is not None:
|
| 945 |
+
inv = {v: k for k, v in name_to_int.items()}
|
| 946 |
+
r["model"] = {inv[v]: r["model"][v] for v in r["model"]}
|
| 947 |
+
if verbose:
|
| 948 |
+
_report_sat(r)
|
| 949 |
+
return r
|
| 950 |
+
|
| 951 |
+
if kind == "subset_sum":
|
| 952 |
+
try:
|
| 953 |
+
nums, target, defaulted = parse_subset_sum(text)
|
| 954 |
+
except ParseError as e:
|
| 955 |
+
if verbose:
|
| 956 |
+
print(f" parse error: {e}")
|
| 957 |
+
return None
|
| 958 |
+
if verbose:
|
| 959 |
+
note = " (defaulted to half-total)" if defaulted else ""
|
| 960 |
+
print(f" parsed: {len(nums)} numbers, target = {target}{note}")
|
| 961 |
+
r = solve_subset_sum(nums, target)
|
| 962 |
+
if verbose:
|
| 963 |
+
_report_subset_sum(r)
|
| 964 |
+
return r
|
| 965 |
+
|
| 966 |
+
if kind == "tsp":
|
| 967 |
+
try:
|
| 968 |
+
pts = parse_tsp(text)
|
| 969 |
+
except ParseError as e:
|
| 970 |
+
if verbose:
|
| 971 |
+
print(f" parse error: {e}")
|
| 972 |
+
return None
|
| 973 |
+
if verbose:
|
| 974 |
+
print(f" parsed: {len(pts)} points")
|
| 975 |
+
r = solve_tsp(pts, time_limit=60.0)
|
| 976 |
+
if verbose:
|
| 977 |
+
_report_tsp(r)
|
| 978 |
+
return r
|
| 979 |
+
|
| 980 |
+
return None
|
| 981 |
+
|
| 982 |
+
|
| 983 |
+
# =============================================================================
|
| 984 |
+
# BENCH INSTANCES
|
| 985 |
+
# =============================================================================
|
| 986 |
+
|
| 987 |
+
def pigeonhole_clauses(m, n):
|
| 988 |
+
"""PHP(m, n): m pigeons, n holes. UNSAT iff m > n."""
|
| 989 |
+
def var(i, j):
|
| 990 |
+
return (i - 1) * n + j
|
| 991 |
+
clauses = []
|
| 992 |
+
for i in range(1, m + 1):
|
| 993 |
+
clauses.append([var(i, j) for j in range(1, n + 1)])
|
| 994 |
+
for j in range(1, n + 1):
|
| 995 |
+
for i1 in range(1, m + 1):
|
| 996 |
+
for i2 in range(i1 + 1, m + 1):
|
| 997 |
+
clauses.append([-var(i1, j), -var(i2, j)])
|
| 998 |
+
return clauses, m * n
|
| 999 |
+
|
| 1000 |
+
|
| 1001 |
+
def bench_maxcut():
|
| 1002 |
+
out = []
|
| 1003 |
+
|
| 1004 |
+
for n in [4, 5, 6, 7, 8]:
|
| 1005 |
+
names = [f"v{i}" for i in range(n)]
|
| 1006 |
+
edges = [[names[i], names[j], 1]
|
| 1007 |
+
for i in range(n) for j in range(i + 1, n)]
|
| 1008 |
+
out.append((f"K_{n} complete, unit weights",
|
| 1009 |
+
{"type": "maxcut", "edges": edges},
|
| 1010 |
+
(n * n) // 4,
|
| 1011 |
+
"analytic: floor(n^2/4)"))
|
| 1012 |
+
|
| 1013 |
+
for n in [4, 6, 8, 10]:
|
| 1014 |
+
names = [f"v{i}" for i in range(n)]
|
| 1015 |
+
edges = [[names[i], names[(i + 1) % n], 1] for i in range(n)]
|
| 1016 |
+
out.append((f"C_{n} cycle, unit weights",
|
| 1017 |
+
{"type": "maxcut", "edges": edges},
|
| 1018 |
+
n, "analytic: n for even cycle"))
|
| 1019 |
+
|
| 1020 |
+
for n in [5, 7, 9]:
|
| 1021 |
+
names = [f"v{i}" for i in range(n)]
|
| 1022 |
+
edges = [[names[i], names[(i + 1) % n], 1] for i in range(n)]
|
| 1023 |
+
out.append((f"C_{n} cycle, unit weights",
|
| 1024 |
+
{"type": "maxcut", "edges": edges},
|
| 1025 |
+
n - 1, "analytic: n-1 for odd cycle"))
|
| 1026 |
+
|
| 1027 |
+
for n in [5, 6, 7, 8]:
|
| 1028 |
+
names = [f"v{i}" for i in range(n)]
|
| 1029 |
+
edges = [[names[i], names[i + 1], 1] for i in range(n - 1)]
|
| 1030 |
+
out.append((f"P_{n} path, unit weights",
|
| 1031 |
+
{"type": "maxcut", "edges": edges},
|
| 1032 |
+
n - 1, "analytic: n-1"))
|
| 1033 |
+
|
| 1034 |
+
for m, n in [(3, 3), (4, 4), (3, 5)]:
|
| 1035 |
+
left = [f"L{i}" for i in range(m)]
|
| 1036 |
+
right = [f"R{j}" for j in range(n)]
|
| 1037 |
+
edges = [[li, rj, 1] for li in left for rj in right]
|
| 1038 |
+
out.append((f"K_{{{m},{n}}} complete bipartite",
|
| 1039 |
+
{"type": "maxcut", "edges": edges},
|
| 1040 |
+
m * n, "analytic: all edges cuttable"))
|
| 1041 |
+
|
| 1042 |
+
return out
|
| 1043 |
+
|
| 1044 |
+
|
| 1045 |
+
def bench_sat():
|
| 1046 |
+
out = []
|
| 1047 |
+
|
| 1048 |
+
out.append(("trivial unit clause (x1)",
|
| 1049 |
+
{"type": "sat", "n_vars": 1, "clauses": [[1]]},
|
| 1050 |
+
True, "x1=True"))
|
| 1051 |
+
|
| 1052 |
+
out.append(("(x1 | x2) & -x1",
|
| 1053 |
+
{"type": "sat", "n_vars": 2, "clauses": [[1, 2], [-1]]},
|
| 1054 |
+
True, "x1=False, x2=True"))
|
| 1055 |
+
|
| 1056 |
+
out.append(("(x1) & (-x1)",
|
| 1057 |
+
{"type": "sat", "n_vars": 1, "clauses": [[1], [-1]]},
|
| 1058 |
+
False, "contradiction"))
|
| 1059 |
+
|
| 1060 |
+
clauses, n_vars = pigeonhole_clauses(3, 2)
|
| 1061 |
+
out.append(("PHP(3,2) pigeonhole",
|
| 1062 |
+
{"type": "sat", "n_vars": n_vars, "clauses": clauses},
|
| 1063 |
+
False, "pigeonhole: 3 pigeons, 2 holes"))
|
| 1064 |
+
|
| 1065 |
+
clauses, n_vars = pigeonhole_clauses(4, 3)
|
| 1066 |
+
out.append(("PHP(4,3) pigeonhole",
|
| 1067 |
+
{"type": "sat", "n_vars": n_vars, "clauses": clauses},
|
| 1068 |
+
False, "pigeonhole: 4 pigeons, 3 holes"))
|
| 1069 |
+
|
| 1070 |
+
clauses = [[1, 2, 3], [-1, 2, 4], [-2, -3, 4]]
|
| 1071 |
+
out.append(("3-SAT with known model (4 vars)",
|
| 1072 |
+
{"type": "sat", "n_vars": 4, "clauses": clauses},
|
| 1073 |
+
True, "model 1=T 2=T 3=F 4=T"))
|
| 1074 |
+
|
| 1075 |
+
return out
|
| 1076 |
+
|
| 1077 |
+
|
| 1078 |
+
def bench_subset_sum():
|
| 1079 |
+
"""Instances with subset-sum answers provable by hand."""
|
| 1080 |
+
out = []
|
| 1081 |
+
|
| 1082 |
+
# Powers of 2: unique binary encoding for every target in [0, 127]
|
| 1083 |
+
nums = [1, 2, 4, 8, 16, 32, 64]
|
| 1084 |
+
out.append(("powers of 2, target=100",
|
| 1085 |
+
{"type": "subset_sum", "numbers": nums, "target": 100},
|
| 1086 |
+
True, "100 = 64 + 32 + 4"))
|
| 1087 |
+
out.append(("powers of 2, target=127",
|
| 1088 |
+
{"type": "subset_sum", "numbers": nums, "target": 127},
|
| 1089 |
+
True, "127 = 1+2+4+8+16+32+64"))
|
| 1090 |
+
out.append(("powers of 2, target=0",
|
| 1091 |
+
{"type": "subset_sum", "numbers": nums, "target": 0},
|
| 1092 |
+
True, "empty subset"))
|
| 1093 |
+
out.append(("powers of 2, target=200",
|
| 1094 |
+
{"type": "subset_sum", "numbers": nums, "target": 200},
|
| 1095 |
+
False, "200 > total(127)"))
|
| 1096 |
+
|
| 1097 |
+
# {1,...,10} is subset-sum complete: every integer in [0,55] achievable.
|
| 1098 |
+
# Only targets outside that range are provably UNSAT.
|
| 1099 |
+
nums = list(range(1, 11)) # total = 55
|
| 1100 |
+
out.append(("1..10, target=28",
|
| 1101 |
+
{"type": "subset_sum", "numbers": nums, "target": 28},
|
| 1102 |
+
True, "10 + 9 + 8 + 1 = 28"))
|
| 1103 |
+
out.append(("1..10, target=55",
|
| 1104 |
+
{"type": "subset_sum", "numbers": nums, "target": 55},
|
| 1105 |
+
True, "the entire set"))
|
| 1106 |
+
out.append(("1..10, target=60",
|
| 1107 |
+
{"type": "subset_sum", "numbers": nums, "target": 60},
|
| 1108 |
+
False, "60 > total(55)"))
|
| 1109 |
+
out.append(("1..10, target=-1",
|
| 1110 |
+
{"type": "subset_sum", "numbers": nums, "target": -1},
|
| 1111 |
+
False, "all numbers positive"))
|
| 1112 |
+
|
| 1113 |
+
return out
|
| 1114 |
+
|
| 1115 |
+
def bench_tsp():
|
| 1116 |
+
out = []
|
| 1117 |
+
|
| 1118 |
+
for a, b in [(10, 10), (10, 20), (5, 12)]:
|
| 1119 |
+
pts = [[0, 0], [a, 0], [a, b], [0, b]]
|
| 1120 |
+
out.append((f"rectangle {a}x{b} corners",
|
| 1121 |
+
{"type": "tsp", "points": pts},
|
| 1122 |
+
2.0 * (a + b),
|
| 1123 |
+
f"perimeter 2*({a}+{b})"))
|
| 1124 |
+
|
| 1125 |
+
for n in [4, 5, 6]:
|
| 1126 |
+
pts = [[i * 10, 0] for i in range(n)]
|
| 1127 |
+
out.append((f"{n} collinear, spacing 10",
|
| 1128 |
+
{"type": "tsp", "points": pts},
|
| 1129 |
+
2.0 * 10 * (n - 1),
|
| 1130 |
+
"2*(max-min) for collinear"))
|
| 1131 |
+
|
| 1132 |
+
return out
|
| 1133 |
+
|
| 1134 |
+
|
| 1135 |
+
# =============================================================================
|
| 1136 |
+
# BENCH MODE
|
| 1137 |
+
# =============================================================================
|
| 1138 |
+
|
| 1139 |
+
def run_bench(verbose=True):
|
| 1140 |
+
print()
|
| 1141 |
+
print("=" * 78)
|
| 1142 |
+
print("BENCH MODE -- instances with analytically-known optima")
|
| 1143 |
+
print("=" * 78)
|
| 1144 |
+
|
| 1145 |
+
total = 0
|
| 1146 |
+
passed = 0
|
| 1147 |
+
|
| 1148 |
+
print()
|
| 1149 |
+
print(" MAX-CUT:")
|
| 1150 |
+
for name, schema, expected, source in bench_maxcut():
|
| 1151 |
+
r = dispatch_from_json(schema, verbose=False)
|
| 1152 |
+
got = r["value"]
|
| 1153 |
+
ok = (got == expected)
|
| 1154 |
+
total += 1
|
| 1155 |
+
if ok:
|
| 1156 |
+
passed += 1
|
| 1157 |
+
if verbose:
|
| 1158 |
+
tag = "PASS" if ok else "FAIL"
|
| 1159 |
+
print(f" [{tag}] {name:<36} solver={got:>5} "
|
| 1160 |
+
f"expected={expected:>5} ({source})")
|
| 1161 |
+
|
| 1162 |
+
print()
|
| 1163 |
+
print(" SAT:")
|
| 1164 |
+
for name, schema, expected, source in bench_sat():
|
| 1165 |
+
r = dispatch_from_json(schema, verbose=False)
|
| 1166 |
+
got = r["satisfiable"]
|
| 1167 |
+
ok = (got == expected)
|
| 1168 |
+
total += 1
|
| 1169 |
+
if ok:
|
| 1170 |
+
passed += 1
|
| 1171 |
+
if verbose:
|
| 1172 |
+
tag = "PASS" if ok else "FAIL"
|
| 1173 |
+
print(f" [{tag}] {name:<36} solver={str(got):<5} "
|
| 1174 |
+
f"expected={str(expected):<5} ({source})")
|
| 1175 |
+
|
| 1176 |
+
print()
|
| 1177 |
+
print(" SUBSET-SUM:")
|
| 1178 |
+
for name, schema, expected, source in bench_subset_sum():
|
| 1179 |
+
r = dispatch_from_json(schema, verbose=False)
|
| 1180 |
+
got = r["found"]
|
| 1181 |
+
ok = (got == expected)
|
| 1182 |
+
total += 1
|
| 1183 |
+
if ok:
|
| 1184 |
+
passed += 1
|
| 1185 |
+
if verbose:
|
| 1186 |
+
tag = "PASS" if ok else "FAIL"
|
| 1187 |
+
print(f" [{tag}] {name:<36} solver={str(got):<5} "
|
| 1188 |
+
f"expected={str(expected):<5} ({source})")
|
| 1189 |
+
|
| 1190 |
+
print()
|
| 1191 |
+
print(" TSP:")
|
| 1192 |
+
for name, schema, expected, source in bench_tsp():
|
| 1193 |
+
r = dispatch_from_json(schema, verbose=False)
|
| 1194 |
+
got = r["length"]
|
| 1195 |
+
ok = (got is not None and abs(got - expected) < 1e-6)
|
| 1196 |
+
total += 1
|
| 1197 |
+
if ok:
|
| 1198 |
+
passed += 1
|
| 1199 |
+
if verbose:
|
| 1200 |
+
tag = "PASS" if ok else "FAIL"
|
| 1201 |
+
gs = f"{got:.4f}" if got is not None else "None"
|
| 1202 |
+
print(f" [{tag}] {name:<36} solver={gs:>9} "
|
| 1203 |
+
f"expected={expected:>9.4f} ({source})")
|
| 1204 |
+
|
| 1205 |
+
print()
|
| 1206 |
+
print(f" BENCH TOTAL: passed {passed}/{total}")
|
| 1207 |
+
print("=" * 78)
|
| 1208 |
+
return passed == total
|
| 1209 |
+
|
| 1210 |
+
|
| 1211 |
+
# =============================================================================
|
| 1212 |
+
# CROSS-VERIFY MODE
|
| 1213 |
+
# =============================================================================
|
| 1214 |
+
|
| 1215 |
+
def run_cross(sizes=None, seed=0, dfs_time_limit=60.0, bb_time_limit=60.0,
|
| 1216 |
+
verbose=True):
|
| 1217 |
+
"""Cross-verify MAX-CUT B&B against independent exhaustive DFS."""
|
| 1218 |
+
if sizes is None:
|
| 1219 |
+
sizes = [14, 16, 18, 20, 22]
|
| 1220 |
+
|
| 1221 |
+
print()
|
| 1222 |
+
print("=" * 78)
|
| 1223 |
+
print("CROSS-VERIFY -- MAX-CUT: B&B vs independent exhaustive DFS")
|
| 1224 |
+
print("=" * 78)
|
| 1225 |
+
print(f" B&B time limit per instance : {bb_time_limit:.0f}s")
|
| 1226 |
+
print(f" DFS time limit per instance : {dfs_time_limit:.0f}s")
|
| 1227 |
+
print()
|
| 1228 |
+
|
| 1229 |
+
rng = random.Random(seed)
|
| 1230 |
+
results = []
|
| 1231 |
+
|
| 1232 |
+
print(f" {'n':>3} {'edges':>6} {'B&B':>8} {'B&B(s)':>8} "
|
| 1233 |
+
f"{'DFS':>8} {'DFS(s)':>8} {'verdict':>10}")
|
| 1234 |
+
print(" " + "-" * 62)
|
| 1235 |
+
|
| 1236 |
+
for n in sizes:
|
| 1237 |
+
schema = _rand_maxcut(n, rng.randint(0, 10**9))
|
| 1238 |
+
W, _ = _validate_maxcut_schema(schema)
|
| 1239 |
+
edges = sum(1 for i in range(n) for j in range(i + 1, n) if W[i][j])
|
| 1240 |
+
|
| 1241 |
+
# B&B
|
| 1242 |
+
t0 = time.perf_counter()
|
| 1243 |
+
warm, warm_side = warm_start_maxcut(W, restarts=64, seed=0)
|
| 1244 |
+
bb_res = maxcut_bb(W, bb_time_limit, warm, warm_side)
|
| 1245 |
+
bb_time = time.perf_counter() - t0
|
| 1246 |
+
bb_val = bb_res["best"]
|
| 1247 |
+
bb_aborted = bb_res["aborted"]
|
| 1248 |
+
|
| 1249 |
+
# DFS
|
| 1250 |
+
t0 = time.perf_counter()
|
| 1251 |
+
dfs_val, _, dfs_aborted = maxcut_dfs_exact(W, time_limit=dfs_time_limit)
|
| 1252 |
+
dfs_time = time.perf_counter() - t0
|
| 1253 |
+
|
| 1254 |
+
if dfs_aborted and bb_aborted:
|
| 1255 |
+
verdict = "both to"
|
| 1256 |
+
agree = None
|
| 1257 |
+
elif dfs_aborted:
|
| 1258 |
+
verdict = "DFS to"
|
| 1259 |
+
agree = None
|
| 1260 |
+
elif bb_aborted:
|
| 1261 |
+
verdict = "B&B to"
|
| 1262 |
+
agree = None
|
| 1263 |
+
elif bb_val == dfs_val:
|
| 1264 |
+
verdict = "AGREE"
|
| 1265 |
+
agree = True
|
| 1266 |
+
else:
|
| 1267 |
+
verdict = "DISAGREE"
|
| 1268 |
+
agree = False
|
| 1269 |
+
|
| 1270 |
+
dfs_str = f"{dfs_val}" if not dfs_aborted else ">t"
|
| 1271 |
+
bb_str = f"{bb_val}"
|
| 1272 |
+
print(f" {n:>3} {edges:>6} {bb_str:>8} {bb_time:>8.2f} "
|
| 1273 |
+
f"{dfs_str:>8} {dfs_time:>8.2f} {verdict:>10}")
|
| 1274 |
+
|
| 1275 |
+
results.append((n, bb_val, dfs_val, bb_aborted, dfs_aborted, agree))
|
| 1276 |
+
|
| 1277 |
+
n_agree = sum(1 for r in results if r[5] is True)
|
| 1278 |
+
n_disagree = sum(1 for r in results if r[5] is False)
|
| 1279 |
+
n_timeout = sum(1 for r in results if r[5] is None)
|
| 1280 |
+
|
| 1281 |
+
print()
|
| 1282 |
+
print(f" AGREEMENT: {n_agree} DISAGREEMENT: {n_disagree} "
|
| 1283 |
+
f"TIMEOUT-ON-ONE-SIDE: {n_timeout}")
|
| 1284 |
+
print("=" * 78)
|
| 1285 |
+
return n_disagree == 0
|
| 1286 |
+
|
| 1287 |
+
|
| 1288 |
+
# =============================================================================
|
| 1289 |
+
# STRESS TEST HARNESS
|
| 1290 |
+
# =============================================================================
|
| 1291 |
+
|
| 1292 |
+
def _rand_maxcut(n, seed, wmax=10, density=1.0):
|
| 1293 |
+
rng = random.Random(seed)
|
| 1294 |
+
names = [f"v{i}" for i in range(n)]
|
| 1295 |
+
edges = []
|
| 1296 |
+
for i in range(n):
|
| 1297 |
+
for j in range(i + 1, n):
|
| 1298 |
+
if rng.random() < density:
|
| 1299 |
+
edges.append([names[i], names[j], rng.randint(1, wmax)])
|
| 1300 |
+
return {"type": "maxcut", "edges": edges}
|
| 1301 |
+
|
| 1302 |
+
|
| 1303 |
+
def _rand_sat(n, ratio, seed):
|
| 1304 |
+
rng = random.Random(seed)
|
| 1305 |
+
m = max(1, int(round(n * ratio)))
|
| 1306 |
+
clauses = []
|
| 1307 |
+
for _ in range(m):
|
| 1308 |
+
vs = rng.sample(range(1, n + 1), min(3, n))
|
| 1309 |
+
clauses.append([v if rng.random() < 0.5 else -v for v in vs])
|
| 1310 |
+
return {"type": "sat", "n_vars": n, "clauses": clauses}
|
| 1311 |
+
|
| 1312 |
+
|
| 1313 |
+
def _rand_subset_sum(n, maxval, seed):
|
| 1314 |
+
rng = random.Random(seed)
|
| 1315 |
+
nums = [rng.randint(1, maxval) for _ in range(n)]
|
| 1316 |
+
return {"type": "subset_sum", "numbers": nums, "target": sum(nums) // 2}
|
| 1317 |
+
|
| 1318 |
+
|
| 1319 |
+
def _rand_tsp(n, seed, span=100.0):
|
| 1320 |
+
rng = random.Random(seed)
|
| 1321 |
+
pts = [[rng.uniform(0, span), rng.uniform(0, span)] for _ in range(n)]
|
| 1322 |
+
return {"type": "tsp", "points": pts}
|
| 1323 |
+
|
| 1324 |
+
|
| 1325 |
+
def schema_to_text(schema):
|
| 1326 |
+
kind = schema["type"]
|
| 1327 |
+
if kind == "maxcut":
|
| 1328 |
+
parts = []
|
| 1329 |
+
for e in schema["edges"]:
|
| 1330 |
+
if len(e) == 3:
|
| 1331 |
+
parts.append(f"{e[0]}-{e[1]}:{e[2]}")
|
| 1332 |
+
else:
|
| 1333 |
+
parts.append(f"{e[0]}-{e[1]}")
|
| 1334 |
+
return "max cut on " + ", ".join(parts)
|
| 1335 |
+
if kind == "sat":
|
| 1336 |
+
clauses = ["(" + " or ".join(str(x) for x in c) + ")"
|
| 1337 |
+
for c in schema["clauses"]]
|
| 1338 |
+
return "sat " + " and ".join(clauses)
|
| 1339 |
+
if kind == "subset_sum":
|
| 1340 |
+
nums = ", ".join(str(x) for x in schema["numbers"])
|
| 1341 |
+
if "target" in schema and schema["target"] is not None:
|
| 1342 |
+
return f"subset sum [{nums}] target {schema['target']}"
|
| 1343 |
+
return f"subset sum [{nums}]"
|
| 1344 |
+
if kind == "tsp":
|
| 1345 |
+
pts = " ".join(f"({p[0]!r},{p[1]!r})" for p in schema["points"])
|
| 1346 |
+
return "tsp " + pts
|
| 1347 |
+
raise ValueError(f"unknown type: {kind!r}")
|
| 1348 |
+
|
| 1349 |
+
|
| 1350 |
+
def _schema_equivalent(a, b):
|
| 1351 |
+
if a["type"] != b["type"]:
|
| 1352 |
+
return False, "type mismatch"
|
| 1353 |
+
t = a["type"]
|
| 1354 |
+
|
| 1355 |
+
if t == "maxcut":
|
| 1356 |
+
def key(e):
|
| 1357 |
+
u, v = e[0], e[1]
|
| 1358 |
+
w = e[2] if len(e) == 3 else 1
|
| 1359 |
+
if str(u) > str(v):
|
| 1360 |
+
u, v = v, u
|
| 1361 |
+
return (str(u), str(v), w)
|
| 1362 |
+
return sorted(key(e) for e in a["edges"]) == \
|
| 1363 |
+
sorted(key(e) for e in b["edges"]), "edge set differs"
|
| 1364 |
+
|
| 1365 |
+
if t == "sat":
|
| 1366 |
+
def norm(c):
|
| 1367 |
+
return tuple(sorted(set(c)))
|
| 1368 |
+
ca = sorted(norm(c) for c in a["clauses"])
|
| 1369 |
+
cb = sorted(norm(c) for c in b["clauses"])
|
| 1370 |
+
na = a.get("n_vars") or max((abs(x) for c in a["clauses"] for x in c),
|
| 1371 |
+
default=0)
|
| 1372 |
+
nb = b.get("n_vars") or max((abs(x) for c in b["clauses"] for x in c),
|
| 1373 |
+
default=0)
|
| 1374 |
+
return ca == cb and na == nb, f"clauses or n_vars ({na} vs {nb})"
|
| 1375 |
+
|
| 1376 |
+
if t == "subset_sum":
|
| 1377 |
+
return a["numbers"] == b["numbers"] and a["target"] == b["target"], \
|
| 1378 |
+
"numbers or target differ"
|
| 1379 |
+
|
| 1380 |
+
if t == "tsp":
|
| 1381 |
+
pa = sorted((round(x, 12), round(y, 12)) for x, y in a["points"])
|
| 1382 |
+
pb = sorted((round(x, 12), round(y, 12)) for x, y in b["points"])
|
| 1383 |
+
return pa == pb, "point set differs"
|
| 1384 |
+
|
| 1385 |
+
return False, "unknown type"
|
| 1386 |
+
|
| 1387 |
+
|
| 1388 |
+
def stress_solvers(trials, seed, verbose):
|
| 1389 |
+
rng = random.Random(seed)
|
| 1390 |
+
report = {"passed": 0, "failed": 0, "notes": []}
|
| 1391 |
+
|
| 1392 |
+
for t in range(trials):
|
| 1393 |
+
n = rng.randint(4, 10)
|
| 1394 |
+
schema = _rand_maxcut(n, rng.randint(0, 10**9))
|
| 1395 |
+
W, names = _validate_maxcut_schema(schema)
|
| 1396 |
+
r = solve_maxcut(W, names, time_limit=5.0)
|
| 1397 |
+
bf, _ = brute_force_maxcut(W)
|
| 1398 |
+
if r["value"] != bf:
|
| 1399 |
+
report["failed"] += 1
|
| 1400 |
+
report["notes"].append(f"maxcut n={n}: {r['value']} vs {bf}")
|
| 1401 |
+
else:
|
| 1402 |
+
report["passed"] += 1
|
| 1403 |
+
if verbose:
|
| 1404 |
+
print(f" maxcut n={n:>2} solver={r['value']:>5} bf={bf:>5} "
|
| 1405 |
+
f"{'OK' if r['value']==bf else 'FAIL'}")
|
| 1406 |
+
|
| 1407 |
+
for t in range(trials):
|
| 1408 |
+
n = rng.randint(3, 9)
|
| 1409 |
+
ratio = rng.choice([3.0, 4.0, 4.26, 5.0])
|
| 1410 |
+
schema = _rand_sat(n, ratio, rng.randint(0, 10**9))
|
| 1411 |
+
clauses, n_vars = _validate_sat_schema(schema)
|
| 1412 |
+
r = solve_sat(clauses, n_vars, time_limit=5.0)
|
| 1413 |
+
bf_sat = False
|
| 1414 |
+
for mask in range(1 << n_vars):
|
| 1415 |
+
assign = {v + 1: bool(mask & (1 << v)) for v in range(n_vars)}
|
| 1416 |
+
if verify_sat(clauses, assign):
|
| 1417 |
+
bf_sat = True
|
| 1418 |
+
break
|
| 1419 |
+
if r["satisfiable"] != bf_sat:
|
| 1420 |
+
report["failed"] += 1
|
| 1421 |
+
report["notes"].append(
|
| 1422 |
+
f"sat n={n}: {r['satisfiable']} vs {bf_sat}")
|
| 1423 |
+
elif r["satisfiable"] and not r["verify"]:
|
| 1424 |
+
report["failed"] += 1
|
| 1425 |
+
report["notes"].append("sat: verify failed")
|
| 1426 |
+
else:
|
| 1427 |
+
report["passed"] += 1
|
| 1428 |
+
if verbose:
|
| 1429 |
+
print(f" sat n={n:>2} r={ratio:<5} "
|
| 1430 |
+
f"solver={r['satisfiable']} bf={bf_sat} "
|
| 1431 |
+
f"{'OK' if r['satisfiable']==bf_sat else 'FAIL'}")
|
| 1432 |
+
|
| 1433 |
+
for t in range(trials):
|
| 1434 |
+
n = rng.randint(4, 12)
|
| 1435 |
+
schema = _rand_subset_sum(n, 30, rng.randint(0, 10**9))
|
| 1436 |
+
nums, target, _ = _validate_subset_sum_schema(schema)
|
| 1437 |
+
r = solve_subset_sum(nums, target)
|
| 1438 |
+
bf_found = brute_force_subset_sum(nums, target) is not None
|
| 1439 |
+
if r["found"] != bf_found:
|
| 1440 |
+
report["failed"] += 1
|
| 1441 |
+
report["notes"].append(f"ssum n={n}: {r['found']} vs {bf_found}")
|
| 1442 |
+
elif r["found"] and not r["verify"]:
|
| 1443 |
+
report["failed"] += 1
|
| 1444 |
+
report["notes"].append("ssum: verify failed")
|
| 1445 |
+
else:
|
| 1446 |
+
report["passed"] += 1
|
| 1447 |
+
if verbose:
|
| 1448 |
+
print(f" ssum n={n:>2} solver={r['found']} "
|
| 1449 |
+
f"bf={bf_found} "
|
| 1450 |
+
f"{'OK' if r['found']==bf_found else 'FAIL'}")
|
| 1451 |
+
|
| 1452 |
+
for t in range(trials):
|
| 1453 |
+
n = rng.randint(4, 7)
|
| 1454 |
+
schema = _rand_tsp(n, rng.randint(0, 10**9))
|
| 1455 |
+
pts = _validate_tsp_schema(schema)
|
| 1456 |
+
r = solve_tsp(pts, time_limit=5.0)
|
| 1457 |
+
bf_cost, _ = brute_force_tsp(pts)
|
| 1458 |
+
if abs(r["length"] - bf_cost) > 1e-6:
|
| 1459 |
+
report["failed"] += 1
|
| 1460 |
+
report["notes"].append(
|
| 1461 |
+
f"tsp n={n}: {r['length']:.6f} vs {bf_cost:.6f}")
|
| 1462 |
+
else:
|
| 1463 |
+
report["passed"] += 1
|
| 1464 |
+
if verbose:
|
| 1465 |
+
print(f" tsp n={n:>2} solver={r['length']:.6f} "
|
| 1466 |
+
f"bf={bf_cost:.6f} "
|
| 1467 |
+
f"{'OK' if abs(r['length']-bf_cost)<1e-6 else 'FAIL'}")
|
| 1468 |
+
|
| 1469 |
+
return report
|
| 1470 |
+
|
| 1471 |
+
|
| 1472 |
+
def stress_parsers(trials, seed, verbose):
|
| 1473 |
+
rng = random.Random(seed)
|
| 1474 |
+
report = {"passed": 0, "failed": 0, "notes": []}
|
| 1475 |
+
|
| 1476 |
+
for t in range(trials):
|
| 1477 |
+
kind = rng.choice(["maxcut", "sat", "subset_sum", "tsp"])
|
| 1478 |
+
if kind == "maxcut":
|
| 1479 |
+
schema = _rand_maxcut(rng.randint(3, 8), rng.randint(0, 10**9))
|
| 1480 |
+
elif kind == "sat":
|
| 1481 |
+
schema = _rand_sat(rng.randint(3, 8),
|
| 1482 |
+
rng.choice([3.0, 4.0, 4.26]),
|
| 1483 |
+
rng.randint(0, 10**9))
|
| 1484 |
+
elif kind == "subset_sum":
|
| 1485 |
+
schema = _rand_subset_sum(rng.randint(3, 10), 50,
|
| 1486 |
+
rng.randint(0, 10**9))
|
| 1487 |
+
else:
|
| 1488 |
+
schema = _rand_tsp(rng.randint(3, 7), rng.randint(0, 10**9))
|
| 1489 |
+
|
| 1490 |
+
text = schema_to_text(schema)
|
| 1491 |
+
parsed = None
|
| 1492 |
+
try:
|
| 1493 |
+
if kind == "maxcut":
|
| 1494 |
+
W, names = parse_maxcut(text)
|
| 1495 |
+
parsed = {"type": "maxcut", "edges": []}
|
| 1496 |
+
for i in range(len(W)):
|
| 1497 |
+
for j in range(i + 1, len(W)):
|
| 1498 |
+
if W[i][j]:
|
| 1499 |
+
parsed["edges"].append([names[i], names[j],
|
| 1500 |
+
W[i][j]])
|
| 1501 |
+
elif kind == "sat":
|
| 1502 |
+
clauses, n_vars, _ = parse_sat(text)
|
| 1503 |
+
parsed = {"type": "sat", "n_vars": n_vars,
|
| 1504 |
+
"clauses": [list(c) for c in clauses]}
|
| 1505 |
+
elif kind == "subset_sum":
|
| 1506 |
+
nums, target, _ = parse_subset_sum(text)
|
| 1507 |
+
parsed = {"type": "subset_sum", "numbers": nums,
|
| 1508 |
+
"target": target}
|
| 1509 |
+
else:
|
| 1510 |
+
pts = parse_tsp(text)
|
| 1511 |
+
parsed = {"type": "tsp",
|
| 1512 |
+
"points": [[x, y] for x, y in pts]}
|
| 1513 |
+
ok, msg = _schema_equivalent(schema, parsed)
|
| 1514 |
+
except ParseError as e:
|
| 1515 |
+
ok, msg = False, f"parse error: {e}"
|
| 1516 |
+
|
| 1517 |
+
if ok:
|
| 1518 |
+
report["passed"] += 1
|
| 1519 |
+
else:
|
| 1520 |
+
report["failed"] += 1
|
| 1521 |
+
report["notes"].append(
|
| 1522 |
+
f"{kind} round-trip: {msg}\n"
|
| 1523 |
+
f" schema: {schema}\n"
|
| 1524 |
+
f" text: {text}\n"
|
| 1525 |
+
f" parsed: {parsed}"
|
| 1526 |
+
)
|
| 1527 |
+
if verbose:
|
| 1528 |
+
print(f" {kind:<10} {'OK' if ok else 'FAIL'} "
|
| 1529 |
+
f"{'' if ok else msg}")
|
| 1530 |
+
|
| 1531 |
+
return report
|
| 1532 |
+
|
| 1533 |
+
|
| 1534 |
+
def stress_json(trials, seed, verbose):
|
| 1535 |
+
rng = random.Random(seed)
|
| 1536 |
+
report = {"passed": 0, "failed": 0, "notes": []}
|
| 1537 |
+
|
| 1538 |
+
for t in range(trials):
|
| 1539 |
+
kind = rng.choice(["maxcut", "sat", "subset_sum", "tsp"])
|
| 1540 |
+
if kind == "maxcut":
|
| 1541 |
+
schema = _rand_maxcut(rng.randint(3, 8), rng.randint(0, 10**9))
|
| 1542 |
+
elif kind == "sat":
|
| 1543 |
+
schema = _rand_sat(rng.randint(3, 8), 4.0,
|
| 1544 |
+
rng.randint(0, 10**9))
|
| 1545 |
+
elif kind == "subset_sum":
|
| 1546 |
+
schema = _rand_subset_sum(rng.randint(3, 10), 50,
|
| 1547 |
+
rng.randint(0, 10**9))
|
| 1548 |
+
else:
|
| 1549 |
+
schema = _rand_tsp(rng.randint(3, 7), rng.randint(0, 10**9))
|
| 1550 |
+
|
| 1551 |
+
text = schema_to_text(schema)
|
| 1552 |
+
r_json = dispatch_from_json(schema, verbose=False)
|
| 1553 |
+
r_text = dispatch(text, verbose=False)
|
| 1554 |
+
|
| 1555 |
+
if kind == "maxcut":
|
| 1556 |
+
agree = r_json["value"] == r_text["value"]
|
| 1557 |
+
msg = f"values {r_json['value']} vs {r_text['value']}"
|
| 1558 |
+
elif kind == "sat":
|
| 1559 |
+
agree = r_json["satisfiable"] == r_text["satisfiable"]
|
| 1560 |
+
msg = (f"satisfiable {r_json['satisfiable']} vs "
|
| 1561 |
+
f"{r_text['satisfiable']}")
|
| 1562 |
+
elif kind == "subset_sum":
|
| 1563 |
+
agree = r_json["found"] == r_text["found"]
|
| 1564 |
+
msg = f"found {r_json['found']} vs {r_text['found']}"
|
| 1565 |
+
else:
|
| 1566 |
+
jl, tl = r_json["length"], r_text["length"]
|
| 1567 |
+
if jl is None or tl is None:
|
| 1568 |
+
agree = jl == tl
|
| 1569 |
+
else:
|
| 1570 |
+
agree = abs(jl - tl) < 1e-6
|
| 1571 |
+
msg = f"length {jl} vs {tl}"
|
| 1572 |
+
|
| 1573 |
+
if agree and r_json.get("verify"):
|
| 1574 |
+
report["passed"] += 1
|
| 1575 |
+
else:
|
| 1576 |
+
report["failed"] += 1
|
| 1577 |
+
report["notes"].append(
|
| 1578 |
+
f"{kind} json-vs-text: {msg} "
|
| 1579 |
+
f"(verify={r_json.get('verify')})"
|
| 1580 |
+
)
|
| 1581 |
+
if verbose:
|
| 1582 |
+
print(f" {kind:<10} "
|
| 1583 |
+
f"{'OK' if (agree and r_json.get('verify')) else 'FAIL'} "
|
| 1584 |
+
f"{msg}")
|
| 1585 |
+
|
| 1586 |
+
return report
|
| 1587 |
+
|
| 1588 |
+
|
| 1589 |
+
def run_stress(trials=20, seed=0, verbose=True):
|
| 1590 |
+
print()
|
| 1591 |
+
print("=" * 78)
|
| 1592 |
+
print(f"STRESS TEST trials per class per suite = {trials}")
|
| 1593 |
+
print("=" * 78)
|
| 1594 |
+
|
| 1595 |
+
print()
|
| 1596 |
+
print(" Suite 1: solvers vs brute force")
|
| 1597 |
+
r1 = stress_solvers(trials, seed, verbose)
|
| 1598 |
+
print(f" -> passed {r1['passed']} failed {r1['failed']}")
|
| 1599 |
+
for n in r1["notes"][:10]:
|
| 1600 |
+
print(f" ! {n}")
|
| 1601 |
+
|
| 1602 |
+
print()
|
| 1603 |
+
print(" Suite 2: schema -> text -> schema round-trip")
|
| 1604 |
+
r2 = stress_parsers(trials * 2, seed + 1, verbose)
|
| 1605 |
+
print(f" -> passed {r2['passed']} failed {r2['failed']}")
|
| 1606 |
+
for n in r2["notes"][:5]:
|
| 1607 |
+
print(f" ! {n}")
|
| 1608 |
+
|
| 1609 |
+
print()
|
| 1610 |
+
print(" Suite 3: dispatch_from_json vs dispatch")
|
| 1611 |
+
r3 = stress_json(trials, seed + 2, verbose)
|
| 1612 |
+
print(f" -> passed {r3['passed']} failed {r3['failed']}")
|
| 1613 |
+
for n in r3["notes"][:5]:
|
| 1614 |
+
print(f" ! {n}")
|
| 1615 |
+
|
| 1616 |
+
total_pass = r1["passed"] + r2["passed"] + r3["passed"]
|
| 1617 |
+
total_fail = r1["failed"] + r2["failed"] + r3["failed"]
|
| 1618 |
+
print()
|
| 1619 |
+
print(f" TOTAL: passed {total_pass} failed {total_fail}")
|
| 1620 |
+
print("=" * 78)
|
| 1621 |
+
return total_fail == 0
|
| 1622 |
+
|
| 1623 |
+
|
| 1624 |
+
# =============================================================================
|
| 1625 |
+
# DEMOS
|
| 1626 |
+
# =============================================================================
|
| 1627 |
+
|
| 1628 |
+
DEMOS = [
|
| 1629 |
+
("MAX-CUT, 6 vertices, weighted",
|
| 1630 |
+
"max cut on A-B:3, B-C:5, C-A:2, C-D:4, D-E:7, E-F:1, F-A:6, B-E:2"),
|
| 1631 |
+
("3-SAT, mixed variable names",
|
| 1632 |
+
"sat (x1 or -x2 or x3) and (a or x2 or -x3) and (-a or b or x1)"),
|
| 1633 |
+
("SUBSET-SUM, find exact half",
|
| 1634 |
+
"subset sum [3, 7, 1, 8, 2, 11, 5, 4] target 20"),
|
| 1635 |
+
("TSP, 6 cities in the plane",
|
| 1636 |
+
"tsp (0,0) (10,0) (10,10) (0,10) (5,5) (2,8)"),
|
| 1637 |
+
]
|
| 1638 |
+
|
| 1639 |
+
|
| 1640 |
+
def run_demo():
|
| 1641 |
+
print()
|
| 1642 |
+
print("=" * 78)
|
| 1643 |
+
print("DEMO -- one instance per problem class")
|
| 1644 |
+
print("=" * 78)
|
| 1645 |
+
for label, text in DEMOS:
|
| 1646 |
+
print()
|
| 1647 |
+
print(f"### {label}")
|
| 1648 |
+
print(f"input: {text}")
|
| 1649 |
+
print()
|
| 1650 |
+
dispatch(text, verbose=True)
|
| 1651 |
+
print()
|
| 1652 |
+
print("-" * 78)
|
| 1653 |
+
|
| 1654 |
+
|
| 1655 |
+
# =============================================================================
|
| 1656 |
+
# CLI
|
| 1657 |
+
# =============================================================================
|
| 1658 |
+
|
| 1659 |
+
BANNER = r"""
|
| 1660 |
+
+------------------------------------------------------------------------+
|
| 1661 |
+
| SOLVER-BACKED OPTIMIZATION ASSISTANT v3 |
|
| 1662 |
+
| |
|
| 1663 |
+
| NL front-end -> exact solvers -> verified answers. |
|
| 1664 |
+
| Bench mode with analytically-known optima. |
|
| 1665 |
+
| Cross-verifier: B&B vs independent DFS (MAX-CUT). |
|
| 1666 |
+
| |
|
| 1667 |
+
| Examples: |
|
| 1668 |
+
| max cut on A-B:3, B-C:5, C-A:2 |
|
| 1669 |
+
| sat (x1 or -x2 or x3) and (x2 or x3) and (-x1 or -x3) |
|
| 1670 |
+
| subset sum [3, 7, 1, 8, 2, 11] target 15 |
|
| 1671 |
+
| tsp (0,0) (10,0) (10,10) (0,10) (5,5) |
|
| 1672 |
+
| |
|
| 1673 |
+
| Commands: demo, stress [N], bench, cross [sizes], help, quit |
|
| 1674 |
+
+------------------------------------------------------------------------+
|
| 1675 |
+
"""
|
| 1676 |
+
|
| 1677 |
+
|
| 1678 |
+
def _parse_sizes_arg(s):
|
| 1679 |
+
return [int(x.strip()) for x in s.split(",") if x.strip()]
|
| 1680 |
+
|
| 1681 |
+
|
| 1682 |
+
def interactive():
|
| 1683 |
+
print(BANNER)
|
| 1684 |
+
while True:
|
| 1685 |
+
try:
|
| 1686 |
+
line = input("> ").strip()
|
| 1687 |
+
except (EOFError, KeyboardInterrupt):
|
| 1688 |
+
print()
|
| 1689 |
+
break
|
| 1690 |
+
if not line:
|
| 1691 |
+
continue
|
| 1692 |
+
low = line.lower()
|
| 1693 |
+
if low in ("quit", "exit", "q"):
|
| 1694 |
+
break
|
| 1695 |
+
if low == "help":
|
| 1696 |
+
print(BANNER)
|
| 1697 |
+
continue
|
| 1698 |
+
if low == "demo":
|
| 1699 |
+
run_demo()
|
| 1700 |
+
continue
|
| 1701 |
+
if low == "bench":
|
| 1702 |
+
run_bench(verbose=True)
|
| 1703 |
+
continue
|
| 1704 |
+
if low == "cross" or low.startswith("cross "):
|
| 1705 |
+
m = re.match(r"cross\s+([\d,\s]+)$", low)
|
| 1706 |
+
sizes = _parse_sizes_arg(m.group(1)) if m else None
|
| 1707 |
+
run_cross(sizes=sizes, verbose=True)
|
| 1708 |
+
continue
|
| 1709 |
+
if low.startswith("stress"):
|
| 1710 |
+
m = re.match(r"stress\s*(\d+)?", low)
|
| 1711 |
+
n = int(m.group(1)) if m and m.group(1) else 20
|
| 1712 |
+
run_stress(trials=n, seed=0, verbose=False)
|
| 1713 |
+
continue
|
| 1714 |
+
print()
|
| 1715 |
+
try:
|
| 1716 |
+
dispatch(line, verbose=True)
|
| 1717 |
+
except Exception as e:
|
| 1718 |
+
print(f" error: {type(e).__name__}: {e}")
|
| 1719 |
+
print()
|
| 1720 |
+
|
| 1721 |
+
|
| 1722 |
+
def main():
|
| 1723 |
+
args = sys.argv[1:]
|
| 1724 |
+
if not args:
|
| 1725 |
+
interactive()
|
| 1726 |
+
return
|
| 1727 |
+
|
| 1728 |
+
if args[0] in ("-h", "--help"):
|
| 1729 |
+
print(__doc__)
|
| 1730 |
+
return
|
| 1731 |
+
|
| 1732 |
+
if args[0] == "--demo":
|
| 1733 |
+
run_demo()
|
| 1734 |
+
return
|
| 1735 |
+
|
| 1736 |
+
if args[0] == "--bench":
|
| 1737 |
+
ok = run_bench(verbose=True)
|
| 1738 |
+
sys.exit(0 if ok else 1)
|
| 1739 |
+
|
| 1740 |
+
if args[0] == "--cross":
|
| 1741 |
+
sizes = None
|
| 1742 |
+
i = 1
|
| 1743 |
+
while i < len(args):
|
| 1744 |
+
if args[i] == "--sizes" and i + 1 < len(args):
|
| 1745 |
+
sizes = _parse_sizes_arg(args[i + 1])
|
| 1746 |
+
i += 2
|
| 1747 |
+
else:
|
| 1748 |
+
i += 1
|
| 1749 |
+
ok = run_cross(sizes=sizes, verbose=True)
|
| 1750 |
+
sys.exit(0 if ok else 1)
|
| 1751 |
+
|
| 1752 |
+
if args[0] == "--stress":
|
| 1753 |
+
trials = 20
|
| 1754 |
+
verbose = False
|
| 1755 |
+
i = 1
|
| 1756 |
+
while i < len(args):
|
| 1757 |
+
if args[i] == "--trials" and i + 1 < len(args):
|
| 1758 |
+
trials = int(args[i + 1])
|
| 1759 |
+
i += 2
|
| 1760 |
+
elif args[i] == "-v":
|
| 1761 |
+
verbose = True
|
| 1762 |
+
i += 1
|
| 1763 |
+
else:
|
| 1764 |
+
i += 1
|
| 1765 |
+
ok = run_stress(trials=trials, seed=0, verbose=verbose)
|
| 1766 |
+
sys.exit(0 if ok else 1)
|
| 1767 |
+
|
| 1768 |
+
if args[0] == "--json":
|
| 1769 |
+
if len(args) < 2:
|
| 1770 |
+
print("usage: --json '<schema>'")
|
| 1771 |
+
sys.exit(1)
|
| 1772 |
+
try:
|
| 1773 |
+
schema = json.loads(args[1])
|
| 1774 |
+
except json.JSONDecodeError as e:
|
| 1775 |
+
print(f"invalid JSON: {e}")
|
| 1776 |
+
sys.exit(1)
|
| 1777 |
+
try:
|
| 1778 |
+
dispatch_from_json(schema, verbose=True)
|
| 1779 |
+
except ValueError as e:
|
| 1780 |
+
print(f"schema error: {e}")
|
| 1781 |
+
sys.exit(1)
|
| 1782 |
+
return
|
| 1783 |
+
|
| 1784 |
+
dispatch(" ".join(args), verbose=True)
|
| 1785 |
+
|
| 1786 |
+
|
| 1787 |
+
if __name__ == "__main__":
|
| 1788 |
+
main()
|